sanook-cli 0.4.0 → 0.5.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 +19 -0
- package/CHANGELOG.md +144 -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 +394 -51
- 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 +2 -2
- package/dist/providers/keys.js +3 -2
- package/dist/providers/registry.js +133 -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 +218 -27
- package/dist/ui/banner.js +4 -9
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/setup.js +6 -5
- 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/config.js
CHANGED
|
@@ -1,19 +1,72 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
|
-
|
|
4
|
+
import { appHomePath, appProjectPath, BRAND } from './brand.js';
|
|
5
|
+
import { projectRoot, projectTrustStatus } from './trust.js';
|
|
6
|
+
import { registerPricing } from './cost.js';
|
|
7
|
+
export const CONFIG_DIR = appHomePath();
|
|
6
8
|
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
7
9
|
const AUTH_PATH = join(CONFIG_DIR, 'auth.json'); // API keys (chmod 0600)
|
|
10
|
+
export const PricingOverrideSchema = z.record(z.string(), z
|
|
11
|
+
.object({
|
|
12
|
+
input: z.number().finite().nonnegative().optional(),
|
|
13
|
+
output: z.number().finite().nonnegative().optional(),
|
|
14
|
+
cacheWrite: z.number().finite().nonnegative().optional(),
|
|
15
|
+
cacheRead: z.number().finite().nonnegative().optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.refine((v) => Object.keys(v).length > 0, 'ต้องใส่ราคาอย่างน้อยหนึ่ง field'));
|
|
8
19
|
export const ConfigSchema = z.object({
|
|
9
20
|
model: z.string().default('sonnet'),
|
|
21
|
+
/** model สำรองเมื่อ model หลักล้ม (rate-limit/billing) — ตั้งด้วย sanook config set fallbackModel <spec> */
|
|
22
|
+
fallbackModel: z.string().optional(),
|
|
10
23
|
budgetUsd: z.number().positive().optional(),
|
|
11
24
|
maxSteps: z.number().int().positive().default(20),
|
|
12
25
|
// auto = รัน tool เลย (act-first) · ask = ขออนุมัติก่อน write/bash/commit
|
|
13
|
-
permissionMode: z.enum(['auto', 'ask']).default('
|
|
26
|
+
permissionMode: z.enum(['auto', 'ask']).default('ask'),
|
|
14
27
|
// path ของ second-brain workspace ที่ scaffold ไว้ (sanook brain) — optional
|
|
15
28
|
brainPath: z.string().optional(),
|
|
29
|
+
// pricing override/extension per "provider:model" → ทำให้ budget cap ใช้ได้กับ model ที่ยังไม่มีในตาราง
|
|
30
|
+
pricing: PricingOverrideSchema.optional(),
|
|
31
|
+
// ── token/cost tuning (ดู agentTuning) — .catch กันค่า config.json ผิดทำ boot พัง (agentTuning อ่าน raw + coerce เองด้วย) ──
|
|
32
|
+
// prompt-cache TTL: '5m' (default, ephemeral) · '1h' (จ่าย write 2x แต่ cache อยู่ยาว — คุ้มเมื่อ session หยุดๆทำๆ)
|
|
33
|
+
cacheTtl: z.enum(['5m', '1h']).catch('5m').default('5m'),
|
|
34
|
+
// วิธีบีบ context ตอนยาว: 'truncate' (default, zero-LLM) · 'summarize' (ใช้ model ถูกย่อ — จำ context ได้ดีกว่า)
|
|
35
|
+
compaction: z.enum(['truncate', 'summarize']).catch('truncate').default('truncate'),
|
|
36
|
+
// extended thinking (Anthropic): false/ไม่ตั้ง = ปิด · true = budget default · number = budget tokens
|
|
37
|
+
thinking: z.union([z.boolean(), z.number().int().positive()]).optional().catch(undefined),
|
|
38
|
+
// model สำหรับย่อ (compaction=summarize) — ไม่ตั้ง = ใช้ fast-sibling ของ model หลัก (ค่ายเดียวกัน ถูกกว่า)
|
|
39
|
+
summaryModel: z.string().optional().catch(undefined),
|
|
16
40
|
});
|
|
41
|
+
const DEFAULT_THINKING_BUDGET = 4096;
|
|
42
|
+
/** parse thinking config (config field หรือ env) → budget tokens (undefined = ปิด) */
|
|
43
|
+
function parseThinking(v) {
|
|
44
|
+
if (typeof v === 'number' && v > 0)
|
|
45
|
+
return Math.floor(v);
|
|
46
|
+
if (v === true)
|
|
47
|
+
return DEFAULT_THINKING_BUDGET;
|
|
48
|
+
if (typeof v === 'string') {
|
|
49
|
+
if (/^\d+$/.test(v))
|
|
50
|
+
return Number.parseInt(v, 10);
|
|
51
|
+
if (['on', 'true', '1', 'yes'].includes(v.toLowerCase()))
|
|
52
|
+
return DEFAULT_THINKING_BUDGET;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* อ่าน tuning knobs (cache TTL / thinking / compaction / summary model) จาก global config.json
|
|
58
|
+
* + env override (SANOOK_CACHE_TTL / SANOOK_THINKING / SANOOK_COMPACTION / SANOOK_SUMMARY_MODEL).
|
|
59
|
+
* อ่านตรงจาก config.json (เลี่ยง thread ผ่าน call stack ลึก) — เบา, เรียกครั้งเดียวต่อ turn.
|
|
60
|
+
*/
|
|
61
|
+
export async function agentTuning() {
|
|
62
|
+
const raw = await readGlobalConfigRaw();
|
|
63
|
+
const envTtl = process.env.SANOOK_CACHE_TTL;
|
|
64
|
+
const cacheTtl = envTtl === '1h' || (envTtl !== '5m' && raw.cacheTtl === '1h') ? '1h' : '5m';
|
|
65
|
+
const thinkingBudget = parseThinking(process.env.SANOOK_THINKING ?? raw.thinking);
|
|
66
|
+
const compaction = (process.env.SANOOK_COMPACTION ?? raw.compaction) === 'summarize' ? 'summarize' : 'truncate';
|
|
67
|
+
const summaryModel = process.env.SANOOK_SUMMARY_MODEL ?? (typeof raw.summaryModel === 'string' ? raw.summaryModel : undefined);
|
|
68
|
+
return { cacheTtl, thinkingBudget, compaction, summaryModel };
|
|
69
|
+
}
|
|
17
70
|
async function readJson(path) {
|
|
18
71
|
try {
|
|
19
72
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -23,21 +76,53 @@ async function readJson(path) {
|
|
|
23
76
|
return {}; // ไม่มีไฟล์ / parse ไม่ได้ = ใช้ default
|
|
24
77
|
}
|
|
25
78
|
}
|
|
79
|
+
function sanitizeUntrustedProjectConfig(cfg) {
|
|
80
|
+
const out = { ...cfg };
|
|
81
|
+
delete out.permissionMode;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
26
84
|
/**
|
|
27
|
-
* โหลด config แบบ layered: global (~/.sanook) < project (.sanook) < CLI overrides
|
|
85
|
+
* โหลด config แบบ layered: global (~/.sanook) < project (.sanook) < env < CLI overrides
|
|
28
86
|
* merge raw ทุกชั้นก่อน แล้ว validate zod ทีเดียวที่ merged สุดท้าย
|
|
29
87
|
* (config flat — shallow merge พอ; strip undefined ใน overrides กัน override ทับ default)
|
|
30
88
|
*/
|
|
31
89
|
export async function loadConfig(overrides = {}, cwd = process.cwd()) {
|
|
32
|
-
const global = await readJson(
|
|
33
|
-
const
|
|
90
|
+
const global = await readJson(CONFIG_PATH);
|
|
91
|
+
const root = await projectRoot(cwd);
|
|
92
|
+
const projectRaw = await readJson(appProjectPath(root, 'config.json'));
|
|
93
|
+
const trust = await projectTrustStatus(root);
|
|
94
|
+
const project = trust.trusted ? projectRaw : sanitizeUntrustedProjectConfig(projectRaw);
|
|
95
|
+
const envConfig = {};
|
|
96
|
+
if (process.env[BRAND.modelEnvVar])
|
|
97
|
+
envConfig.model = process.env[BRAND.modelEnvVar];
|
|
34
98
|
const cleanOverrides = {};
|
|
35
99
|
for (const [k, v] of Object.entries(overrides)) {
|
|
36
100
|
if (v !== undefined)
|
|
37
101
|
cleanOverrides[k] = v;
|
|
38
102
|
}
|
|
39
|
-
const merged = { ...global, ...project, ...cleanOverrides };
|
|
40
|
-
|
|
103
|
+
const merged = { ...global, ...project, ...envConfig, ...cleanOverrides };
|
|
104
|
+
const config = ConfigSchema.parse(merged);
|
|
105
|
+
// pricing override: config.pricing + env SANOOK_PRICING (JSON) → ลงทะเบียนเข้า cost table
|
|
106
|
+
registerPricing(config.pricing);
|
|
107
|
+
registerPricing(parseEnvPricing());
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
/** env SANOOK_PRICING = JSON ของ { "provider:model": { input, output, ... } } */
|
|
111
|
+
function parseEnvPricing() {
|
|
112
|
+
const raw = process.env.SANOOK_PRICING;
|
|
113
|
+
if (!raw)
|
|
114
|
+
return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(raw);
|
|
117
|
+
const res = PricingOverrideSchema.safeParse(parsed);
|
|
118
|
+
return res.success ? res.data : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined; // JSON ไม่ถูก = ข้าม (ไม่ทำให้ boot ล้ม)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function parsePricingOverride(raw) {
|
|
125
|
+
return PricingOverrideSchema.parse(JSON.parse(raw));
|
|
41
126
|
}
|
|
42
127
|
/** ครั้งแรกที่รัน (ยังไม่มี global config) → ต้องทำ setup wizard */
|
|
43
128
|
export async function isFirstRun() {
|
|
@@ -53,13 +138,15 @@ export async function isFirstRun() {
|
|
|
53
138
|
export async function saveGlobalConfig(cfg) {
|
|
54
139
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
55
140
|
const existing = await readJson(CONFIG_PATH);
|
|
56
|
-
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, ...cfg }, null, 2)}\n
|
|
141
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, ...cfg }, null, 2)}\n`, { mode: 0o600 });
|
|
142
|
+
await chmod(CONFIG_PATH, 0o600).catch(() => { });
|
|
57
143
|
}
|
|
58
144
|
/** บันทึก path ของ second-brain workspace ลง global config (merge — ไม่ทับ field อื่น) */
|
|
59
145
|
export async function saveBrainPath(path) {
|
|
60
146
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
61
147
|
const existing = await readJson(CONFIG_PATH);
|
|
62
|
-
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, brainPath: path }, null, 2)}\n
|
|
148
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, brainPath: path }, null, 2)}\n`, { mode: 0o600 });
|
|
149
|
+
await chmod(CONFIG_PATH, 0o600).catch(() => { });
|
|
63
150
|
}
|
|
64
151
|
/** อ่าน config.json ดิบ (ไม่ apply default/schema) — สำหรับ `sanook config` */
|
|
65
152
|
export async function readGlobalConfigRaw() {
|
|
@@ -69,7 +156,8 @@ export async function readGlobalConfigRaw() {
|
|
|
69
156
|
export async function patchGlobalConfig(patch) {
|
|
70
157
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
71
158
|
const existing = await readJson(CONFIG_PATH);
|
|
72
|
-
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n
|
|
159
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, { mode: 0o600 });
|
|
160
|
+
await chmod(CONFIG_PATH, 0o600).catch(() => { });
|
|
73
161
|
}
|
|
74
162
|
/** บันทึก API key ลง ~/.sanook/auth.json (chmod 0600) + set env ทันทีสำหรับ session นี้ */
|
|
75
163
|
export async function saveKey(envVar, key) {
|
|
@@ -82,7 +170,7 @@ export async function saveKey(envVar, key) {
|
|
|
82
170
|
/* ยังไม่มีไฟล์ */
|
|
83
171
|
}
|
|
84
172
|
auth[envVar] = key;
|
|
85
|
-
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n
|
|
173
|
+
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
86
174
|
await chmod(AUTH_PATH, 0o600); // เจ้าของอ่าน/เขียนเท่านั้น
|
|
87
175
|
process.env[envVar] = key;
|
|
88
176
|
}
|
package/dist/cost.js
CHANGED
|
@@ -1,10 +1,59 @@
|
|
|
1
|
-
// key = specKey() = "
|
|
1
|
+
// key = specKey() = "<provider>:<model id>" — ต้องตรงกับ id ใน registry (มี test กัน drift)
|
|
2
|
+
// Anthropic = ราคา verified (มิ.ย. 2026). ที่เหลือ = published list price โดยประมาณ — override ได้
|
|
2
3
|
export const PRICING = {
|
|
4
|
+
// ── Anthropic (verified) ────────────────────────────────────────────────
|
|
3
5
|
'anthropic:claude-opus-4-8': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
|
|
4
6
|
'anthropic:claude-sonnet-4-6': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
5
7
|
'anthropic:claude-haiku-4-5': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
|
|
6
8
|
'anthropic:claude-fable-5': { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1 },
|
|
9
|
+
// ── ราคาประมาณ (published list price ต่อ 1M tokens) — อาจคลาดเคลื่อน, override ได้ด้วย
|
|
10
|
+
// `sanook config set pricing '{"openai:gpt-5.5":{"input":1.25,...}}'` หรือ env SANOOK_PRICING
|
|
11
|
+
// OpenAI
|
|
12
|
+
'openai:gpt-5.5': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
13
|
+
'openai:gpt-5.4-mini': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
14
|
+
'openai:gpt-5.3-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
15
|
+
// Google Gemini (≤200k context tier)
|
|
16
|
+
'google:gemini-2.5-pro': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.31 },
|
|
17
|
+
'google:gemini-2.5-flash': { input: 0.3, output: 2.5, cacheWrite: 0.3, cacheRead: 0.075 },
|
|
18
|
+
// DeepSeek V4
|
|
19
|
+
'deepseek:deepseek-v4-flash': { input: 0.28, output: 0.42, cacheWrite: 0.28, cacheRead: 0.028 },
|
|
20
|
+
'deepseek:deepseek-v4-pro': { input: 0.55, output: 2.19, cacheWrite: 0.55, cacheRead: 0.055 },
|
|
21
|
+
// xAI Grok
|
|
22
|
+
'xai:grok-4.3': { input: 3, output: 15, cacheWrite: 3, cacheRead: 0.75 },
|
|
23
|
+
// Mistral
|
|
24
|
+
'mistral:mistral-large-latest': { input: 2, output: 6, cacheWrite: 2, cacheRead: 0.2 },
|
|
25
|
+
'mistral:mistral-small-latest': { input: 0.2, output: 0.6, cacheWrite: 0.2, cacheRead: 0.02 },
|
|
26
|
+
// Groq
|
|
27
|
+
'groq:llama-3.3-70b-versatile': { input: 0.59, output: 0.79, cacheWrite: 0.59, cacheRead: 0.059 },
|
|
7
28
|
};
|
|
29
|
+
/** true ถ้ามี pricing สำหรับ specKey นี้ (ใช้เตือนตอน budget cap ตั้งไว้แต่คิดเงินไม่ได้) */
|
|
30
|
+
export function hasPricingForKey(specKey) {
|
|
31
|
+
return specKey in PRICING;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* merge pricing เพิ่ม/override (จาก config `pricing` หรือ env SANOOK_PRICING)
|
|
35
|
+
* — ให้ budget cap ใช้ได้กับ provider ที่ยังไม่มีในตาราง โดยไม่ต้องแก้โค้ด
|
|
36
|
+
*/
|
|
37
|
+
export function registerPricing(extra) {
|
|
38
|
+
if (!extra)
|
|
39
|
+
return;
|
|
40
|
+
for (const [key, p] of Object.entries(extra)) {
|
|
41
|
+
if (p == null || typeof p !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
const base = PRICING[key] ?? { input: 0, output: 0, cacheWrite: 0, cacheRead: 0 };
|
|
44
|
+
const inputRate = Number(p.input ?? base.input);
|
|
45
|
+
const next = {
|
|
46
|
+
input: inputRate,
|
|
47
|
+
output: Number(p.output ?? base.output),
|
|
48
|
+
// override ที่ใส่แค่ input/output (ตามที่ hint แนะนำ) → cache rate อนุมานจาก input แทน 0 (กัน undercount)
|
|
49
|
+
cacheWrite: Number(p.cacheWrite ?? p.input ?? base.cacheWrite),
|
|
50
|
+
cacheRead: Number(p.cacheRead ?? (base.cacheRead || inputRate * 0.1)),
|
|
51
|
+
};
|
|
52
|
+
if (Object.values(next).some((n) => !Number.isFinite(n) || n < 0))
|
|
53
|
+
continue;
|
|
54
|
+
PRICING[key] = next;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
8
57
|
export class CostMeter {
|
|
9
58
|
specKey;
|
|
10
59
|
budgetUsd;
|
|
@@ -40,15 +89,23 @@ export class CostMeter {
|
|
|
40
89
|
(cacheWriteTokens / 1e6) * p.cacheWrite;
|
|
41
90
|
}
|
|
42
91
|
}
|
|
92
|
+
/** รวม token + cost จาก meter อีกตัว (เช่น primary model ก่อน fallback) — กัน usage หาย/budget reset */
|
|
93
|
+
merge(other) {
|
|
94
|
+
this.inTok += other.inTok;
|
|
95
|
+
this.outTok += other.outTok;
|
|
96
|
+
this.cacheReadTok += other.cacheReadTok;
|
|
97
|
+
this.cacheWriteTok += other.cacheWriteTok;
|
|
98
|
+
this.spent += other.spent;
|
|
99
|
+
}
|
|
43
100
|
get totalUsd() {
|
|
44
101
|
return this.spent;
|
|
45
102
|
}
|
|
46
103
|
get hasPricing() {
|
|
47
104
|
return this.specKey in PRICING;
|
|
48
105
|
}
|
|
49
|
-
/** true เมื่อใช้เกิน budget (เช็คก่อนยิง request ถัดไป) */
|
|
106
|
+
/** true เมื่อใช้เกิน budget (เช็คก่อนยิง request ถัดไป) — no-op ถ้าไม่มี pricing (เตือนที่ entry point) */
|
|
50
107
|
get overBudget() {
|
|
51
|
-
return this.budgetUsd != null && this.spent >= this.budgetUsd;
|
|
108
|
+
return this.budgetUsd != null && this.hasPricing && this.spent >= this.budgetUsd;
|
|
52
109
|
}
|
|
53
110
|
summary() {
|
|
54
111
|
const total = this.inTok + this.outTok + this.cacheReadTok + this.cacheWriteTok;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join, resolve, delimiter } from 'node:path';
|
|
5
|
+
import { BRAND } from './brand.js';
|
|
6
|
+
const execFileP = promisify(execFile);
|
|
7
|
+
function normDir(d) {
|
|
8
|
+
try {
|
|
9
|
+
const r = resolve(d).replace(/[\\/]+$/, '');
|
|
10
|
+
return process.platform === 'win32' ? r.toLowerCase() : r; // Windows PATH is case-insensitive
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return process.platform === 'win32' ? d.toLowerCase() : d;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** binDir อยู่ใน PATH ไหม — normalize (ตัด trailing slash, case-insensitive บน Windows) ก่อนเทียบ */
|
|
17
|
+
export function isOnPath(binDir, pathEnv) {
|
|
18
|
+
if (!binDir)
|
|
19
|
+
return false;
|
|
20
|
+
const target = normDir(binDir);
|
|
21
|
+
return (pathEnv ?? '')
|
|
22
|
+
.split(delimiter)
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map(normDir)
|
|
25
|
+
.includes(target);
|
|
26
|
+
}
|
|
27
|
+
/** เก็บข้อมูลการติดตั้งจริงจากเครื่อง (Node version, npm global bin, shim, PATH, local install) */
|
|
28
|
+
export async function diagnose() {
|
|
29
|
+
const isWin = process.platform === 'win32';
|
|
30
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
31
|
+
let prefix = '';
|
|
32
|
+
try {
|
|
33
|
+
// Windows: npm = npm.cmd → ต้อง shell:true ไม่งั้น ENOENT
|
|
34
|
+
prefix = (await execFileP('npm', ['config', 'get', 'prefix'], { shell: isWin })).stdout.trim();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* npm หาไม่เจอใน PATH */
|
|
38
|
+
}
|
|
39
|
+
// global bin: บน Windows = prefix เอง (มี npx.cmd อยู่ตรงนั้น), บน Unix = prefix/bin
|
|
40
|
+
const binDir = prefix ? (isWin ? prefix : join(prefix, 'bin')) : '';
|
|
41
|
+
const shimNames = isWin ? [`${BRAND.cliName}.cmd`, `${BRAND.cliName}.ps1`, BRAND.cliName] : [BRAND.cliName];
|
|
42
|
+
const globalInstalled = !!binDir && shimNames.some((s) => existsSync(join(binDir, s)));
|
|
43
|
+
const localInstall = shimNames.some((s) => existsSync(join(process.cwd(), 'node_modules', '.bin', s)));
|
|
44
|
+
return {
|
|
45
|
+
node: process.version,
|
|
46
|
+
nodeOk: Number.isFinite(major) && major >= 22,
|
|
47
|
+
binDir,
|
|
48
|
+
globalInstalled,
|
|
49
|
+
onPath: isOnPath(binDir, process.env.PATH),
|
|
50
|
+
localInstall,
|
|
51
|
+
isWin,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** รายงาน + วิธีแก้ที่ปลอดภัยต่อ OS (ไม่ใช้ setx %PATH% ซึ่งเป็น footgun ตัด PATH 1024 ตัว) */
|
|
55
|
+
export function formatReport(r, pkgName) {
|
|
56
|
+
const ok = (b) => (b ? '✓' : '✗');
|
|
57
|
+
const lines = [
|
|
58
|
+
`${BRAND.productName} doctor — ตรวจการติดตั้ง`,
|
|
59
|
+
'',
|
|
60
|
+
` ${ok(r.nodeOk)} Node ${r.node}${r.nodeOk ? '' : ' ← ต้อง ≥ 22 (อัปเดตที่ https://nodejs.org)'}`,
|
|
61
|
+
` ${ok(!!r.binDir)} npm global bin: ${r.binDir || '(หาไม่เจอ — npm อยู่ใน PATH ไหม?)'}`,
|
|
62
|
+
` ${ok(r.globalInstalled)} ติดตั้ง global "${BRAND.cliName}": ${r.globalInstalled ? 'ใช่' : 'ยัง'}`,
|
|
63
|
+
` ${ok(r.onPath)} bin อยู่ใน PATH: ${r.onPath ? 'ใช่' : 'ไม่'}`,
|
|
64
|
+
];
|
|
65
|
+
if (r.localInstall)
|
|
66
|
+
lines.push(` ℹ เจอ local install ที่โฟลเดอร์นี้ → ใช้ได้เลยด้วย: npx ${BRAND.cliName}`);
|
|
67
|
+
lines.push('', 'สรุป:');
|
|
68
|
+
if (r.globalInstalled && r.onPath) {
|
|
69
|
+
lines.push(` ✅ พร้อมใช้ — พิมพ์ "${BRAND.cliName}" ได้เลย (ยังไม่เจอ? ปิด-เปิด terminal ใหม่)`);
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
if (!r.globalInstalled) {
|
|
73
|
+
lines.push(` • ลงแบบ global: npm install -g ${pkgName} → แล้วพิมพ์ "${BRAND.cliName}" ได้`);
|
|
74
|
+
}
|
|
75
|
+
if (r.globalInstalled && !r.onPath && r.binDir) {
|
|
76
|
+
if (r.isWin) {
|
|
77
|
+
lines.push(' • bin ไม่อยู่ใน PATH — เพิ่มเข้า user PATH (วางใน PowerShell · ปลอดภัย แก้เฉพาะ user):');
|
|
78
|
+
lines.push(` [Environment]::SetEnvironmentVariable('Path',[Environment]::GetEnvironmentVariable('Path','User')+';${r.binDir}','User')`);
|
|
79
|
+
lines.push(' แล้วปิด-เปิด terminal ใหม่');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const rc = process.env.SHELL?.includes('zsh') ? '~/.zshrc' : '~/.bashrc';
|
|
83
|
+
lines.push(` • bin ไม่อยู่ใน PATH — เพิ่มใน ${rc}: export PATH="$PATH:${r.binDir}" (แล้วเปิด shell ใหม่)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (r.localInstall)
|
|
87
|
+
lines.push(` • หรือใช้ทันทีโดยไม่แก้ PATH: npx ${BRAND.cliName}`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
export async function runDoctor(pkgName) {
|
|
91
|
+
return formatReport(await diagnose(), pkgName);
|
|
92
|
+
}
|
package/dist/gateway/auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
5
|
-
|
|
4
|
+
import { appHomePath } from '../brand.js';
|
|
5
|
+
const GATEWAY_DIR = appHomePath('gateway');
|
|
6
6
|
const TOKEN_FILE = join(GATEWAY_DIR, 'token');
|
|
7
7
|
/** โหลด bearer token ของ gateway; ไม่มี → สร้าง 256-bit ใหม่ เก็บ chmod 600 */
|
|
8
8
|
export async function loadOrCreateToken() {
|
package/dist/gateway/ledger.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile, writeFile, rename, mkdir, chmod } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { withFileLock } from './lock.js';
|
|
5
|
+
import { appHomePath } from '../brand.js';
|
|
6
6
|
// task-ledger = งานที่ gateway ต้องทำ (cron / message / one-shot) — Hermes "Kanban" / OpenClaw "Task Brain"
|
|
7
7
|
// เก็บเป็น JSON (zero native dep) แทน SQLite; ทุก mutation = locked read-modify-write (atomic ต่อ op)
|
|
8
8
|
// → กัน lost-write จากหลาย writer (server enqueue / scheduler update / cron CLI) ที่ยิงไฟล์เดียวกัน
|
|
9
|
-
const GATEWAY_DIR =
|
|
9
|
+
const GATEWAY_DIR = appHomePath('gateway');
|
|
10
10
|
const TASKS_FILE = join(GATEWAY_DIR, 'tasks.json');
|
|
11
11
|
const LOCK_FILE = join(GATEWAY_DIR, 'tasks.lock');
|
|
12
12
|
// ── low-level: read ตรงจากไฟล์ทุกครั้ง (ไม่ cache snapshot → ไม่มี stale-overwrite) ──
|
package/dist/gateway/serve.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { acquireSingleton } from './lock.js';
|
|
5
4
|
import { loadOrCreateToken } from './auth.js';
|
|
6
5
|
import { startServer } from './server.js';
|
|
7
6
|
import { startScheduler } from './scheduler.js';
|
|
8
|
-
|
|
7
|
+
import { appHomePath, BRAND, BRAND_ENV, envFlag } from '../brand.js';
|
|
8
|
+
const GATEWAY_DIR = appHomePath('gateway');
|
|
9
9
|
const SERVE_LOCK = join(GATEWAY_DIR, 'serve.lock');
|
|
10
10
|
/**
|
|
11
11
|
* จุดเดียวที่ start ทั้ง gateway: HTTP server (รับ request 24/7) + scheduler (cron tick)
|
|
@@ -18,7 +18,7 @@ export async function startGateway(opts) {
|
|
|
18
18
|
await mkdir(GATEWAY_DIR, { recursive: true });
|
|
19
19
|
const release = await acquireSingleton(SERVE_LOCK);
|
|
20
20
|
if (!release) {
|
|
21
|
-
throw new Error(
|
|
21
|
+
throw new Error(`มี ${BRAND.cliName} gateway รันอยู่แล้ว (เจอ serve.lock) — ปิดตัวเดิมก่อน หรือถ้าค้างให้ลบ ${appHomePath('gateway', 'serve.lock')}`);
|
|
22
22
|
}
|
|
23
23
|
const token = await loadOrCreateToken();
|
|
24
24
|
const stopServer = startServer({
|
|
@@ -26,11 +26,13 @@ export async function startGateway(opts) {
|
|
|
26
26
|
token,
|
|
27
27
|
defaultModel: opts.model,
|
|
28
28
|
budgetUsd: opts.budgetUsd,
|
|
29
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
29
30
|
onLog: log,
|
|
30
31
|
});
|
|
31
32
|
const stopScheduler = startScheduler({
|
|
32
33
|
defaultModel: opts.model,
|
|
33
34
|
budgetUsd: opts.budgetUsd,
|
|
35
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
34
36
|
tickMs: opts.tickMs,
|
|
35
37
|
onLog: log,
|
|
36
38
|
});
|
|
@@ -47,7 +49,7 @@ export async function startGateway(opts) {
|
|
|
47
49
|
});
|
|
48
50
|
// หมายเหตุ: log "เริ่มแล้ว" อยู่ใน startTelegram (success path) — ถ้า fail-closed จะ log "ไม่เริ่ม" แทน
|
|
49
51
|
}
|
|
50
|
-
log(`scheduler tick ทุก ${(opts.tickMs ?? 60_000) / 1000}s · token:
|
|
52
|
+
log(`scheduler tick ทุก ${(opts.tickMs ?? 60_000) / 1000}s · token: ${appHomePath('gateway', 'token')} (chmod 600)`);
|
|
51
53
|
return () => {
|
|
52
54
|
stopServer();
|
|
53
55
|
stopScheduler();
|
package/dist/gateway/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseSchedule } from './schedule.js';
|
|
|
4
4
|
import { tokenMatches } from './auth.js';
|
|
5
5
|
import { runAgent } from '../loop.js';
|
|
6
6
|
import { redactKey } from '../providers/keys.js';
|
|
7
|
+
import { BRAND } from '../brand.js';
|
|
7
8
|
function send(res, status, body) {
|
|
8
9
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
9
10
|
res.end(JSON.stringify(body));
|
|
@@ -42,7 +43,7 @@ async function handle(req, res, opts) {
|
|
|
42
43
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
43
44
|
// /health = public (เช็คว่า process alive โดยไม่ต้องมี token)
|
|
44
45
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
45
|
-
return send(res, 200, { ok: true, service:
|
|
46
|
+
return send(res, 200, { ok: true, service: BRAND.gatewayServiceName });
|
|
46
47
|
}
|
|
47
48
|
// ทุก endpoint อื่น → bearer token
|
|
48
49
|
const auth = req.headers.authorization ?? '';
|
|
@@ -63,7 +64,14 @@ async function handle(req, res, opts) {
|
|
|
63
64
|
.slice(0, lastUserIdx)
|
|
64
65
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
65
66
|
const model = typeof body.model === 'string' && body.model ? body.model : opts.defaultModel;
|
|
66
|
-
const { text } = await runAgent({
|
|
67
|
+
const { text } = await runAgent({
|
|
68
|
+
model,
|
|
69
|
+
prompt,
|
|
70
|
+
history,
|
|
71
|
+
maxSteps: 20,
|
|
72
|
+
budgetUsd: opts.budgetUsd,
|
|
73
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
74
|
+
});
|
|
67
75
|
return send(res, 200, {
|
|
68
76
|
object: 'chat.completion',
|
|
69
77
|
model,
|
package/dist/git.js
CHANGED
|
@@ -3,8 +3,17 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
4
|
// git helper — execFile('git', args[]) ไม่ผ่าน shell (บทเรียนจาก grep RCE: ไม่ interpolate เข้า shell string)
|
|
5
5
|
export async function runGit(args, cwd = process.cwd()) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
try {
|
|
7
|
+
const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
8
|
+
return stdout;
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
// git ไม่ได้ติดตั้ง/ไม่อยู่ใน PATH → ข้อความชัดแทน "spawn git ENOENT" งงๆ (ทุกแพลตฟอร์ม)
|
|
12
|
+
if (e.code === 'ENOENT') {
|
|
13
|
+
throw new Error('ไม่พบ git ใน PATH — ติดตั้งจาก https://git-scm.com แล้วเปิด terminal ใหม่');
|
|
14
|
+
}
|
|
15
|
+
throw e;
|
|
16
|
+
}
|
|
8
17
|
}
|
|
9
18
|
export async function isGitRepo(cwd = process.cwd()) {
|
|
10
19
|
try {
|
package/dist/hooks.js
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
2
|
import { spawn } from 'node:child_process';
|
|
5
|
-
|
|
3
|
+
import { appHomePath, BRAND_ENV, envFlag } from './brand.js';
|
|
4
|
+
import { hasUntrustedProjectConfig, projectConfigPathIfTrusted, projectRoot } from './trust.js';
|
|
5
|
+
const SAFE_ENV_KEYS = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR', 'APPDATA'];
|
|
6
|
+
function safeEnv() {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const k of SAFE_ENV_KEYS) {
|
|
9
|
+
const v = process.env[k];
|
|
10
|
+
if (v != null)
|
|
11
|
+
out[k] = v;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
async function readHooksFile(path, merged) {
|
|
16
|
+
const cfg = JSON.parse(await readFile(path, 'utf8'));
|
|
17
|
+
const valid = (h) => Boolean(h &&
|
|
18
|
+
typeof h === 'object' &&
|
|
19
|
+
typeof h.matcher === 'string' &&
|
|
20
|
+
typeof h.command === 'string');
|
|
21
|
+
if (Array.isArray(cfg.PreToolUse))
|
|
22
|
+
merged.PreToolUse.push(...cfg.PreToolUse.filter(valid));
|
|
23
|
+
if (Array.isArray(cfg.PostToolUse))
|
|
24
|
+
merged.PostToolUse.push(...cfg.PostToolUse.filter(valid));
|
|
25
|
+
}
|
|
26
|
+
export async function loadHooksConfig(cwd = process.cwd()) {
|
|
6
27
|
const merged = { PreToolUse: [], PostToolUse: [] };
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
try {
|
|
29
|
+
await readHooksFile(appHomePath('hooks.json'), merged);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ไม่มี global config = ข้าม */
|
|
33
|
+
}
|
|
34
|
+
const root = await projectRoot(cwd);
|
|
35
|
+
const projectPath = await projectConfigPathIfTrusted('hooks.json', root);
|
|
36
|
+
if (projectPath) {
|
|
37
|
+
await readHooksFile(projectPath, merged);
|
|
38
|
+
}
|
|
39
|
+
else if (await hasUntrustedProjectConfig('hooks.json', root)) {
|
|
40
|
+
/* project hook config มีอยู่แต่ยังไม่ trusted = ข้ามแบบ fail-closed */
|
|
18
41
|
}
|
|
19
42
|
return merged;
|
|
20
43
|
}
|
|
@@ -31,7 +54,10 @@ export function matches(matcher, tool) {
|
|
|
31
54
|
/** รัน command — payload เข้า stdin (เป็น DATA ไม่ใช่ shell arg → กัน injection); command = config ของ user (trusted) */
|
|
32
55
|
function runCommand(command, payload, timeoutMs = 10_000) {
|
|
33
56
|
return new Promise((resolve) => {
|
|
34
|
-
const child = spawn(command, {
|
|
57
|
+
const child = spawn(command, {
|
|
58
|
+
shell: true,
|
|
59
|
+
env: envFlag(BRAND_ENV.hooksInheritEnv) ? process.env : safeEnv(),
|
|
60
|
+
});
|
|
35
61
|
let stdout = '';
|
|
36
62
|
let stderr = '';
|
|
37
63
|
const timer = setTimeout(() => {
|
|
@@ -96,8 +122,8 @@ function wrapToolsWithHooks(tools, cfg) {
|
|
|
96
122
|
return out;
|
|
97
123
|
}
|
|
98
124
|
/** wrap tools ด้วย hooks ถ้ามี config (ไม่มี → คืน tools เดิม zero overhead) */
|
|
99
|
-
export async function maybeWrapHooks(tools) {
|
|
100
|
-
const cfg = await loadHooksConfig();
|
|
125
|
+
export async function maybeWrapHooks(tools, cwd = process.cwd()) {
|
|
126
|
+
const cfg = await loadHooksConfig(cwd);
|
|
101
127
|
if (!(cfg.PreToolUse?.length || cfg.PostToolUse?.length))
|
|
102
128
|
return tools;
|
|
103
129
|
return wrapToolsWithHooks(tools, cfg);
|