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
package/dist/config.js
CHANGED
|
@@ -7,7 +7,10 @@ import { registerPricing } from './cost.js';
|
|
|
7
7
|
export const CONFIG_DIR = appHomePath();
|
|
8
8
|
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
9
9
|
const AUTH_PATH = join(CONFIG_DIR, 'auth.json'); // API keys (chmod 0600)
|
|
10
|
-
|
|
10
|
+
const AUTH_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
const RESERVED_AUTH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
12
|
+
const PricingKeySchema = z.string().regex(/^[^:\s]+:\S+$/, 'key ต้องเป็น provider:model');
|
|
13
|
+
export const PricingOverrideSchema = z.record(PricingKeySchema, z
|
|
11
14
|
.object({
|
|
12
15
|
input: z.number().finite().nonnegative().optional(),
|
|
13
16
|
output: z.number().finite().nonnegative().optional(),
|
|
@@ -33,52 +36,115 @@ export const ConfigSchema = z.object({
|
|
|
33
36
|
cacheTtl: z.enum(['5m', '1h']).catch('5m').default('5m'),
|
|
34
37
|
// วิธีบีบ context ตอนยาว: 'truncate' (default, zero-LLM) · 'summarize' (ใช้ model ถูกย่อ — จำ context ได้ดีกว่า)
|
|
35
38
|
compaction: z.enum(['truncate', 'summarize']).catch('truncate').default('truncate'),
|
|
39
|
+
// token reducer: off, local zero-LLM selective compressor, or optional Headroom proxy adapter.
|
|
40
|
+
contextCompression: z.enum(['off', 'selective', 'headroom']).catch('selective').default('selective'),
|
|
36
41
|
// extended thinking (Anthropic): false/ไม่ตั้ง = ปิด · true = budget default · number = budget tokens
|
|
37
42
|
thinking: z.union([z.boolean(), z.number().int().positive()]).optional().catch(undefined),
|
|
38
43
|
// model สำหรับย่อ (compaction=summarize) — ไม่ตั้ง = ใช้ fast-sibling ของ model หลัก (ค่ายเดียวกัน ถูกกว่า)
|
|
39
44
|
summaryModel: z.string().optional().catch(undefined),
|
|
45
|
+
// model สำหรับ semantic search embeddings (เช่น openai:text-embedding-3-small)
|
|
46
|
+
embeddingModel: z.string().optional().catch(undefined),
|
|
47
|
+
// Hermes-style /personality overlay (stored as a small named prompt)
|
|
48
|
+
personality: z.string().optional().catch(undefined),
|
|
49
|
+
/** UI + setup wizard language */
|
|
50
|
+
locale: z.enum(['en', 'th']).catch('th').default('th'),
|
|
40
51
|
});
|
|
41
52
|
const DEFAULT_THINKING_BUDGET = 4096;
|
|
53
|
+
function normalizeThinkingBudget(value) {
|
|
54
|
+
const budget = Math.floor(value);
|
|
55
|
+
return Number.isSafeInteger(budget) && budget > 0 ? budget : undefined;
|
|
56
|
+
}
|
|
42
57
|
/** parse thinking config (config field หรือ env) → budget tokens (undefined = ปิด) */
|
|
43
58
|
function parseThinking(v) {
|
|
44
|
-
if (typeof v === 'number' && v
|
|
45
|
-
return
|
|
59
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
60
|
+
return normalizeThinkingBudget(v);
|
|
46
61
|
if (v === true)
|
|
47
62
|
return DEFAULT_THINKING_BUDGET;
|
|
48
63
|
if (typeof v === 'string') {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
const clean = v.trim();
|
|
65
|
+
if (/^\d+$/.test(clean))
|
|
66
|
+
return normalizeThinkingBudget(Number(clean));
|
|
67
|
+
if (['on', 'true', '1', 'yes'].includes(clean.toLowerCase()))
|
|
52
68
|
return DEFAULT_THINKING_BUDGET;
|
|
53
69
|
}
|
|
54
70
|
return undefined;
|
|
55
71
|
}
|
|
72
|
+
function trimmedString(v) {
|
|
73
|
+
if (typeof v !== 'string')
|
|
74
|
+
return undefined;
|
|
75
|
+
const clean = v.trim();
|
|
76
|
+
return clean ? clean : undefined;
|
|
77
|
+
}
|
|
78
|
+
function parseCacheTtl(v) {
|
|
79
|
+
const clean = trimmedString(v);
|
|
80
|
+
return clean === '5m' || clean === '1h' ? clean : undefined;
|
|
81
|
+
}
|
|
82
|
+
function parseCompaction(v) {
|
|
83
|
+
const clean = trimmedString(v);
|
|
84
|
+
return clean === 'truncate' || clean === 'summarize' ? clean : undefined;
|
|
85
|
+
}
|
|
86
|
+
function parseContextCompression(v) {
|
|
87
|
+
const clean = trimmedString(v);
|
|
88
|
+
return clean === 'off' || clean === 'selective' || clean === 'headroom' ? clean : undefined;
|
|
89
|
+
}
|
|
56
90
|
/**
|
|
57
91
|
* อ่าน tuning knobs (cache TTL / thinking / compaction / summary model) จาก global config.json
|
|
58
|
-
* + env override (SANOOK_CACHE_TTL / SANOOK_THINKING / SANOOK_COMPACTION / SANOOK_SUMMARY_MODEL).
|
|
92
|
+
* + env override (SANOOK_CACHE_TTL / SANOOK_THINKING / SANOOK_COMPACTION / SANOOK_CONTEXT_COMPRESSION / SANOOK_SUMMARY_MODEL).
|
|
59
93
|
* อ่านตรงจาก config.json (เลี่ยง thread ผ่าน call stack ลึก) — เบา, เรียกครั้งเดียวต่อ turn.
|
|
60
94
|
*/
|
|
61
95
|
export async function agentTuning() {
|
|
62
96
|
const raw = await readGlobalConfigRaw();
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const summaryModel = process.env.SANOOK_SUMMARY_MODEL ?? (
|
|
68
|
-
return { cacheTtl, thinkingBudget, compaction, summaryModel };
|
|
97
|
+
const cacheTtl = parseCacheTtl(process.env.SANOOK_CACHE_TTL) ?? parseCacheTtl(raw.cacheTtl) ?? '5m';
|
|
98
|
+
const thinkingBudget = parseThinking(trimmedString(process.env.SANOOK_THINKING) ?? raw.thinking);
|
|
99
|
+
const compaction = parseCompaction(process.env.SANOOK_COMPACTION) ?? parseCompaction(raw.compaction) ?? 'truncate';
|
|
100
|
+
const contextCompression = parseContextCompression(process.env.SANOOK_CONTEXT_COMPRESSION) ?? parseContextCompression(raw.contextCompression) ?? 'selective';
|
|
101
|
+
const summaryModel = trimmedString(process.env.SANOOK_SUMMARY_MODEL) ?? trimmedString(raw.summaryModel);
|
|
102
|
+
return { cacheTtl, thinkingBudget, compaction, contextCompression, summaryModel };
|
|
103
|
+
}
|
|
104
|
+
const warnedBadConfigKeys = new Set();
|
|
105
|
+
/**
|
|
106
|
+
* Validate the merged config, but degrade gracefully: a malformed strict field (bad model/maxSteps/
|
|
107
|
+
* permissionMode/budgetUsd/pricing in a hand-edited config.json) is dropped to its default with a
|
|
108
|
+
* one-time stderr warning instead of throwing and crashing boot. Security-sensitive fields drop to
|
|
109
|
+
* the SAFE default (budgetUsd→no cap is still surfaced by the warning; pricing→none).
|
|
110
|
+
*/
|
|
111
|
+
function parseConfigGraceful(merged) {
|
|
112
|
+
const first = ConfigSchema.safeParse(merged);
|
|
113
|
+
if (first.success)
|
|
114
|
+
return first.data;
|
|
115
|
+
const badKeys = [...new Set(first.error.issues.map((i) => String(i.path[0])).filter(Boolean))];
|
|
116
|
+
const cleaned = { ...merged };
|
|
117
|
+
for (const k of badKeys)
|
|
118
|
+
delete cleaned[k];
|
|
119
|
+
const fresh = badKeys.filter((k) => !warnedBadConfigKeys.has(k));
|
|
120
|
+
if (fresh.length) {
|
|
121
|
+
fresh.forEach((k) => warnedBadConfigKeys.add(k));
|
|
122
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ ละเลย config ที่ค่าผิด (ใช้ค่า default แทน): ${fresh.join(', ')}\n`);
|
|
123
|
+
}
|
|
124
|
+
const second = ConfigSchema.safeParse(cleaned);
|
|
125
|
+
return second.success ? second.data : ConfigSchema.parse({});
|
|
69
126
|
}
|
|
70
127
|
async function readJson(path) {
|
|
71
128
|
try {
|
|
72
129
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
73
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
130
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
74
131
|
}
|
|
75
132
|
catch {
|
|
76
133
|
return {}; // ไม่มีไฟล์ / parse ไม่ได้ = ใช้ default
|
|
77
134
|
}
|
|
78
135
|
}
|
|
136
|
+
// key ที่ untrusted project ตั้งไม่ได้ (ต้อง `sanook trust` ก่อน):
|
|
137
|
+
// - permissionMode: auto = auto-approve mutation (รัน bash/แก้ไฟล์ไม่ถาม) — อันตรายสุด
|
|
138
|
+
// - budgetUsd: repo อันตรายตั้งสูงๆ = ปิด spend cap ของ user (เปลืองเงินจริง)
|
|
139
|
+
// - pricing: ตั้งราคาปลอม = ทำให้ budget cap ไม่ trigger (ซ่อน cost / bypass cap)
|
|
140
|
+
// (model/maxSteps/embeddingModel ฯลฯ ปล่อยได้ — เป็น preference ที่ user เห็น/override ได้ และตอนนี้ถูกคุมด้วย budget จริงของ user)
|
|
141
|
+
const UNTRUSTED_PROJECT_DENY = new Set(['permissionMode', 'budgetUsd', 'pricing']);
|
|
79
142
|
function sanitizeUntrustedProjectConfig(cfg) {
|
|
80
|
-
const out = {
|
|
81
|
-
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
145
|
+
if (!UNTRUSTED_PROJECT_DENY.has(k))
|
|
146
|
+
out[k] = v;
|
|
147
|
+
}
|
|
82
148
|
return out;
|
|
83
149
|
}
|
|
84
150
|
/**
|
|
@@ -93,15 +159,16 @@ export async function loadConfig(overrides = {}, cwd = process.cwd()) {
|
|
|
93
159
|
const trust = await projectTrustStatus(root);
|
|
94
160
|
const project = trust.trusted ? projectRaw : sanitizeUntrustedProjectConfig(projectRaw);
|
|
95
161
|
const envConfig = {};
|
|
96
|
-
|
|
97
|
-
|
|
162
|
+
const envModel = trimmedString(process.env[BRAND.modelEnvVar]);
|
|
163
|
+
if (envModel)
|
|
164
|
+
envConfig.model = envModel;
|
|
98
165
|
const cleanOverrides = {};
|
|
99
166
|
for (const [k, v] of Object.entries(overrides)) {
|
|
100
167
|
if (v !== undefined)
|
|
101
168
|
cleanOverrides[k] = v;
|
|
102
169
|
}
|
|
103
170
|
const merged = { ...global, ...project, ...envConfig, ...cleanOverrides };
|
|
104
|
-
const config =
|
|
171
|
+
const config = parseConfigGraceful(merged);
|
|
105
172
|
// pricing override: config.pricing + env SANOOK_PRICING (JSON) → ลงทะเบียนเข้า cost table
|
|
106
173
|
registerPricing(config.pricing);
|
|
107
174
|
registerPricing(parseEnvPricing());
|
|
@@ -113,16 +180,29 @@ function parseEnvPricing() {
|
|
|
113
180
|
if (!raw)
|
|
114
181
|
return undefined;
|
|
115
182
|
try {
|
|
116
|
-
|
|
117
|
-
const res = PricingOverrideSchema.safeParse(parsed);
|
|
118
|
-
return res.success ? res.data : undefined;
|
|
183
|
+
return parsePricingOverride(raw);
|
|
119
184
|
}
|
|
120
185
|
catch {
|
|
121
186
|
return undefined; // JSON ไม่ถูก = ข้าม (ไม่ทำให้ boot ล้ม)
|
|
122
187
|
}
|
|
123
188
|
}
|
|
124
189
|
export function parsePricingOverride(raw) {
|
|
125
|
-
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(raw);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new Error('pricing JSON parse ไม่สำเร็จ');
|
|
196
|
+
}
|
|
197
|
+
const res = PricingOverrideSchema.safeParse(parsed);
|
|
198
|
+
if (!res.success) {
|
|
199
|
+
const details = res.error.issues
|
|
200
|
+
.slice(0, 3)
|
|
201
|
+
.map((issue) => `${issue.path.length ? issue.path.join('.') : 'pricing'}: ${issue.message}`)
|
|
202
|
+
.join('; ');
|
|
203
|
+
throw new Error(`pricing schema ไม่ถูกต้อง${details ? ` — ${details}` : ''}`);
|
|
204
|
+
}
|
|
205
|
+
return res.data;
|
|
126
206
|
}
|
|
127
207
|
/** ครั้งแรกที่รัน (ยังไม่มี global config) → ต้องทำ setup wizard */
|
|
128
208
|
export async function isFirstRun() {
|
|
@@ -134,7 +214,7 @@ export async function isFirstRun() {
|
|
|
134
214
|
return true;
|
|
135
215
|
}
|
|
136
216
|
}
|
|
137
|
-
/** บันทึก global config (model/provider ที่เลือกตอน setup) */
|
|
217
|
+
/** บันทึก global config (model/provider/locale ที่เลือกตอน setup) */
|
|
138
218
|
export async function saveGlobalConfig(cfg) {
|
|
139
219
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
140
220
|
const existing = await readJson(CONFIG_PATH);
|
|
@@ -152,6 +232,23 @@ export async function saveBrainPath(path) {
|
|
|
152
232
|
export async function readGlobalConfigRaw() {
|
|
153
233
|
return readJson(CONFIG_PATH);
|
|
154
234
|
}
|
|
235
|
+
/** path ของ auth.json (ใช้โชว์ใน CLI เท่านั้น; ห้าม print raw secret) */
|
|
236
|
+
export function authConfigPath() {
|
|
237
|
+
return AUTH_PATH;
|
|
238
|
+
}
|
|
239
|
+
function isSafeAuthEnvVarName(name) {
|
|
240
|
+
return AUTH_ENV_VAR_RE.test(name) && !RESERVED_AUTH_KEYS.has(name);
|
|
241
|
+
}
|
|
242
|
+
/** อ่าน auth.json ดิบแบบกรองเฉพาะ string values — caller ต้อง redact ก่อนโชว์ */
|
|
243
|
+
export async function readStoredAuthRaw() {
|
|
244
|
+
const raw = await readJson(AUTH_PATH);
|
|
245
|
+
const auth = {};
|
|
246
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
247
|
+
if (isSafeAuthEnvVarName(k) && typeof v === 'string')
|
|
248
|
+
auth[k] = v;
|
|
249
|
+
}
|
|
250
|
+
return auth;
|
|
251
|
+
}
|
|
155
252
|
/** merge patch ลง config.json (สำหรับ `sanook config set`) */
|
|
156
253
|
export async function patchGlobalConfig(patch) {
|
|
157
254
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
@@ -161,25 +258,44 @@ export async function patchGlobalConfig(patch) {
|
|
|
161
258
|
}
|
|
162
259
|
/** บันทึก API key ลง ~/.sanook/auth.json (chmod 0600) + set env ทันทีสำหรับ session นี้ */
|
|
163
260
|
export async function saveKey(envVar, key) {
|
|
261
|
+
if (!isSafeAuthEnvVarName(envVar))
|
|
262
|
+
throw new Error(`env var ไม่ถูกต้อง: ${envVar}`);
|
|
164
263
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
auth = JSON.parse(await readFile(AUTH_PATH, 'utf8'));
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
/* ยังไม่มีไฟล์ */
|
|
171
|
-
}
|
|
264
|
+
const auth = await readStoredAuthRaw();
|
|
172
265
|
auth[envVar] = key;
|
|
173
266
|
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
174
267
|
await chmod(AUTH_PATH, 0o600); // เจ้าของอ่าน/เขียนเท่านั้น
|
|
175
268
|
process.env[envVar] = key;
|
|
176
269
|
}
|
|
270
|
+
/** ลบ key ที่ Sanook เก็บไว้ใน auth.json (ไม่แตะ env จริงของ shell ภายนอก) */
|
|
271
|
+
export async function removeStoredKey(envVar) {
|
|
272
|
+
if (!isSafeAuthEnvVarName(envVar))
|
|
273
|
+
return false;
|
|
274
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
275
|
+
const auth = await readStoredAuthRaw();
|
|
276
|
+
if (!Object.prototype.hasOwnProperty.call(auth, envVar))
|
|
277
|
+
return false;
|
|
278
|
+
delete auth[envVar];
|
|
279
|
+
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
280
|
+
await chmod(AUTH_PATH, 0o600).catch(() => { });
|
|
281
|
+
delete process.env[envVar];
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
/** ล้าง auth.json ที่ Sanook เก็บไว้ทั้งหมด */
|
|
285
|
+
export async function clearStoredAuth() {
|
|
286
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
287
|
+
const auth = await readStoredAuthRaw();
|
|
288
|
+
for (const envVar of Object.keys(auth))
|
|
289
|
+
delete process.env[envVar];
|
|
290
|
+
await writeFile(AUTH_PATH, '{}\n', { mode: 0o600 });
|
|
291
|
+
await chmod(AUTH_PATH, 0o600).catch(() => { });
|
|
292
|
+
}
|
|
177
293
|
/** โหลด key จาก auth.json เข้า env ตอน boot (ไม่ override env ที่ตั้งไว้แล้ว) */
|
|
178
294
|
export async function loadKeysIntoEnv() {
|
|
179
295
|
try {
|
|
180
|
-
const auth =
|
|
296
|
+
const auth = await readStoredAuthRaw();
|
|
181
297
|
for (const [k, v] of Object.entries(auth)) {
|
|
182
|
-
if (!process.env[k]
|
|
298
|
+
if (!process.env[k])
|
|
183
299
|
process.env[k] = v;
|
|
184
300
|
}
|
|
185
301
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const DEFAULT_TARGET_CHARS = 6_000;
|
|
2
|
+
const DEFAULT_MIN_CHARS = 8_000;
|
|
3
|
+
const DEFAULT_MAX_LINE_CHARS = 800;
|
|
4
|
+
const IMPORTANT_RE = /\b(error|exception|fail(?:ed|ure)?|warning|warn|timeout|denied|unauthorized|traceback|panic|regression|todo|fixme)\b/i;
|
|
5
|
+
const CODE_RE = /^\s*(?:import|export|function|class|interface|type|const|let|var|async|await|return|if|for|while|switch|case)\b/;
|
|
6
|
+
const DIFF_RE = /^\s*(?:diff --git|@@|\+\+\+|---|\+|-)/;
|
|
7
|
+
const PATH_RE = /(?:^|\s)[\w@./-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|py|rs|go|java|css|scss|html|yml|yaml|toml)(?::\d+)?\b/;
|
|
8
|
+
const STRUCTURE_RE = /^\s*(?:#{1,6}\s|\*|-|\d+\.|["'][^"']+["']\s*:)/;
|
|
9
|
+
const TOKEN_RE = /[\p{L}\p{N}_./:-]{2,}/gu;
|
|
10
|
+
function clampPositive(value, fallback) {
|
|
11
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
12
|
+
}
|
|
13
|
+
function tokens(line) {
|
|
14
|
+
return [...line.toLowerCase().matchAll(TOKEN_RE)].map((match) => match[0]);
|
|
15
|
+
}
|
|
16
|
+
function queryTokens(query) {
|
|
17
|
+
const out = new Set();
|
|
18
|
+
for (const token of tokens(query ?? '')) {
|
|
19
|
+
if (token.length >= 3)
|
|
20
|
+
out.add(token);
|
|
21
|
+
const leaf = token.split(/[/:\\]/).pop();
|
|
22
|
+
if (leaf && leaf.length >= 3)
|
|
23
|
+
out.add(leaf);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function shrinkLine(line, maxChars) {
|
|
28
|
+
if (line.length <= maxChars)
|
|
29
|
+
return line;
|
|
30
|
+
const head = Math.max(80, Math.floor(maxChars * 0.55));
|
|
31
|
+
const tail = Math.max(60, maxChars - head - 48);
|
|
32
|
+
return `${line.slice(0, head)} ... [line pruned ${line.length - head - tail} chars] ... ${line.slice(-tail)}`;
|
|
33
|
+
}
|
|
34
|
+
function addAnchors(lines, budget, fromEnd = false) {
|
|
35
|
+
const selected = new Set();
|
|
36
|
+
let chars = 0;
|
|
37
|
+
for (let step = 0; step < lines.length; step++) {
|
|
38
|
+
const index = fromEnd ? lines.length - 1 - step : step;
|
|
39
|
+
if (index < 0 || index >= lines.length)
|
|
40
|
+
break;
|
|
41
|
+
if (chars >= budget)
|
|
42
|
+
break;
|
|
43
|
+
selected.add(index);
|
|
44
|
+
chars += lines[index].length + 1;
|
|
45
|
+
}
|
|
46
|
+
return selected;
|
|
47
|
+
}
|
|
48
|
+
function selectedChars(lines, selected, maxLineChars) {
|
|
49
|
+
let chars = 0;
|
|
50
|
+
for (const index of selected)
|
|
51
|
+
chars += Math.min(lines[index].length, maxLineChars) + 1;
|
|
52
|
+
return chars;
|
|
53
|
+
}
|
|
54
|
+
function lineScores(lines, selected, query) {
|
|
55
|
+
const candidates = lines
|
|
56
|
+
.map((line, index) => ({ line, index }))
|
|
57
|
+
.filter(({ line, index }) => !selected.has(index) && line.trim());
|
|
58
|
+
const df = new Map();
|
|
59
|
+
for (const { line } of candidates) {
|
|
60
|
+
for (const token of new Set(tokens(line)))
|
|
61
|
+
df.set(token, (df.get(token) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
const total = Math.max(1, candidates.length);
|
|
64
|
+
const seenLines = new Map();
|
|
65
|
+
const scores = new Map();
|
|
66
|
+
for (const { line, index } of candidates) {
|
|
67
|
+
const clean = line.trim();
|
|
68
|
+
const lineTokens = tokens(clean);
|
|
69
|
+
let score = 0;
|
|
70
|
+
for (const token of lineTokens)
|
|
71
|
+
score += Math.log((total + 1) / ((df.get(token) ?? 0) + 1));
|
|
72
|
+
score = score / Math.sqrt(Math.max(1, lineTokens.length));
|
|
73
|
+
if (query.size) {
|
|
74
|
+
let overlap = 0;
|
|
75
|
+
for (const token of new Set(lineTokens)) {
|
|
76
|
+
const leaf = token.split(/[/:\\]/).pop() ?? token;
|
|
77
|
+
if (query.has(token) || query.has(leaf))
|
|
78
|
+
overlap += 1;
|
|
79
|
+
}
|
|
80
|
+
if (overlap)
|
|
81
|
+
score += Math.min(40, overlap * 18);
|
|
82
|
+
}
|
|
83
|
+
if (IMPORTANT_RE.test(clean))
|
|
84
|
+
score += 8;
|
|
85
|
+
if (PATH_RE.test(clean))
|
|
86
|
+
score += 5;
|
|
87
|
+
if (DIFF_RE.test(clean))
|
|
88
|
+
score += 4;
|
|
89
|
+
if (CODE_RE.test(clean))
|
|
90
|
+
score += 3;
|
|
91
|
+
if (STRUCTURE_RE.test(clean))
|
|
92
|
+
score += 2;
|
|
93
|
+
if (/https?:\/\//i.test(clean))
|
|
94
|
+
score += 2;
|
|
95
|
+
if (/^\s*[}\])],?\s*$/.test(clean))
|
|
96
|
+
score -= 1;
|
|
97
|
+
if (clean.length > 500 && !/\s/.test(clean))
|
|
98
|
+
score -= 5;
|
|
99
|
+
const repeated = seenLines.get(clean) ?? 0;
|
|
100
|
+
if (repeated)
|
|
101
|
+
score -= Math.min(8, repeated * 2);
|
|
102
|
+
seenLines.set(clean, repeated + 1);
|
|
103
|
+
scores.set(index, score);
|
|
104
|
+
}
|
|
105
|
+
return scores;
|
|
106
|
+
}
|
|
107
|
+
function renderSelected(lines, selected, maxLineChars) {
|
|
108
|
+
const out = [];
|
|
109
|
+
let omittedLines = 0;
|
|
110
|
+
let omittedChars = 0;
|
|
111
|
+
let gapLines = 0;
|
|
112
|
+
let gapChars = 0;
|
|
113
|
+
const flushGap = () => {
|
|
114
|
+
if (!gapLines)
|
|
115
|
+
return;
|
|
116
|
+
out.push(`... [selective context compression: omitted ${gapLines} line(s), ${gapChars} chars] ...`);
|
|
117
|
+
omittedLines += gapLines;
|
|
118
|
+
omittedChars += gapChars;
|
|
119
|
+
gapLines = 0;
|
|
120
|
+
gapChars = 0;
|
|
121
|
+
};
|
|
122
|
+
for (let index = 0; index < lines.length; index++) {
|
|
123
|
+
if (selected.has(index)) {
|
|
124
|
+
flushGap();
|
|
125
|
+
out.push(shrinkLine(lines[index], maxLineChars));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
gapLines += 1;
|
|
129
|
+
gapChars += lines[index].length + 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
flushGap();
|
|
133
|
+
return { text: out.join('\n'), omittedLines, omittedChars };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Zero-LLM selective context compression inspired by Selective Context / Headroom:
|
|
137
|
+
* keep anchors plus high-information lines (errors, paths, code structure, rare terms),
|
|
138
|
+
* then preserve original order with omission markers.
|
|
139
|
+
*/
|
|
140
|
+
export function selectiveCompressText(input, options = {}) {
|
|
141
|
+
const originalChars = input.length;
|
|
142
|
+
const targetChars = clampPositive(options.targetChars, DEFAULT_TARGET_CHARS);
|
|
143
|
+
const minChars = clampPositive(options.minChars, DEFAULT_MIN_CHARS);
|
|
144
|
+
const maxLineChars = clampPositive(options.maxLineChars, DEFAULT_MAX_LINE_CHARS);
|
|
145
|
+
if (originalChars <= minChars || originalChars <= targetChars) {
|
|
146
|
+
return { text: input, changed: false, originalChars, compressedChars: originalChars, omittedLines: 0, omittedChars: 0 };
|
|
147
|
+
}
|
|
148
|
+
const lines = input.split(/\r?\n/);
|
|
149
|
+
if (lines.length <= 4) {
|
|
150
|
+
const text = shrinkLine(input, targetChars);
|
|
151
|
+
return {
|
|
152
|
+
text,
|
|
153
|
+
changed: text !== input,
|
|
154
|
+
originalChars,
|
|
155
|
+
compressedChars: text.length,
|
|
156
|
+
omittedLines: text === input ? 0 : 1,
|
|
157
|
+
omittedChars: Math.max(0, originalChars - text.length),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const headBudget = Math.min(clampPositive(options.headChars, Math.floor(targetChars * 0.18)), Math.floor(targetChars * 0.35));
|
|
161
|
+
const tailBudget = Math.min(clampPositive(options.tailChars, Math.floor(targetChars * 0.25)), Math.floor(targetChars * 0.45));
|
|
162
|
+
const selected = new Set([
|
|
163
|
+
...addAnchors(lines, headBudget),
|
|
164
|
+
...addAnchors(lines, tailBudget, true),
|
|
165
|
+
]);
|
|
166
|
+
const scores = lineScores(lines, selected, queryTokens(options.query));
|
|
167
|
+
const ranked = [...scores.entries()].sort((a, b) => b[1] - a[1]);
|
|
168
|
+
let used = selectedChars(lines, selected, maxLineChars);
|
|
169
|
+
const softBudget = Math.max(400, targetChars - 600);
|
|
170
|
+
for (const [index] of ranked) {
|
|
171
|
+
const nextCost = Math.min(lines[index].length, maxLineChars) + 1;
|
|
172
|
+
if (used + nextCost > softBudget && selected.size > 0)
|
|
173
|
+
continue;
|
|
174
|
+
selected.add(index);
|
|
175
|
+
used += nextCost;
|
|
176
|
+
if (used >= softBudget)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const rendered = renderSelected(lines, selected, maxLineChars);
|
|
180
|
+
if (rendered.text.length >= originalChars) {
|
|
181
|
+
return { text: input, changed: false, originalChars, compressedChars: originalChars, omittedLines: 0, omittedChars: 0 };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
text: rendered.text,
|
|
185
|
+
changed: true,
|
|
186
|
+
originalChars,
|
|
187
|
+
compressedChars: rendered.text.length,
|
|
188
|
+
omittedLines: rendered.omittedLines,
|
|
189
|
+
omittedChars: rendered.omittedChars,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { termList } from './search/index-core.js';
|
|
4
|
+
const PACK_DIR = 'Shared/Context-Packs';
|
|
5
|
+
const MIN_SCORE = 0.35;
|
|
6
|
+
const DEFAULT_MAX_CHARS = 1200;
|
|
7
|
+
/** Known packs + retrieval signals (aligned with Shared/Context-Packs/_Index.md). */
|
|
8
|
+
const PACK_CATALOG = [
|
|
9
|
+
{
|
|
10
|
+
slug: 'second-brain-maintenance',
|
|
11
|
+
title: 'Second-Brain Maintenance',
|
|
12
|
+
description: 'vault structure, routing rules, memory policy, indexes, runbooks, agent adapters',
|
|
13
|
+
signalTerms: [
|
|
14
|
+
'vault',
|
|
15
|
+
'structure',
|
|
16
|
+
'routing',
|
|
17
|
+
'memory',
|
|
18
|
+
'policy',
|
|
19
|
+
'index',
|
|
20
|
+
'runbook',
|
|
21
|
+
'agent',
|
|
22
|
+
'adapter',
|
|
23
|
+
'framework',
|
|
24
|
+
'obsidian',
|
|
25
|
+
'maintenance',
|
|
26
|
+
'brain',
|
|
27
|
+
'scaffold',
|
|
28
|
+
'frontmatter',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: 'coding-release',
|
|
33
|
+
title: 'Coding & Release',
|
|
34
|
+
description: 'source code, tests, build/release, CLI commands, runtime scripts',
|
|
35
|
+
signalTerms: [
|
|
36
|
+
'code',
|
|
37
|
+
'coding',
|
|
38
|
+
'test',
|
|
39
|
+
'tests',
|
|
40
|
+
'build',
|
|
41
|
+
'release',
|
|
42
|
+
'cli',
|
|
43
|
+
'script',
|
|
44
|
+
'implement',
|
|
45
|
+
'fix',
|
|
46
|
+
'bug',
|
|
47
|
+
'typecheck',
|
|
48
|
+
'npm',
|
|
49
|
+
'ship',
|
|
50
|
+
'deploy',
|
|
51
|
+
'refactor',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
slug: 'research-to-framework',
|
|
56
|
+
title: 'Research To Framework',
|
|
57
|
+
description: 'research, experiment, comparison, promote findings into framework',
|
|
58
|
+
signalTerms: [
|
|
59
|
+
'research',
|
|
60
|
+
'experiment',
|
|
61
|
+
'framework',
|
|
62
|
+
'benchmark',
|
|
63
|
+
'eval',
|
|
64
|
+
'hypothesis',
|
|
65
|
+
'promote',
|
|
66
|
+
'distillation',
|
|
67
|
+
'comparison',
|
|
68
|
+
'method',
|
|
69
|
+
'sota',
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
function catalogEntry(slug) {
|
|
74
|
+
const base = PACK_CATALOG.find((item) => item.slug === slug);
|
|
75
|
+
if (!base)
|
|
76
|
+
throw new Error(`unknown context pack slug: ${slug}`);
|
|
77
|
+
return { ...base, relPath: `${PACK_DIR}/${slug}.md` };
|
|
78
|
+
}
|
|
79
|
+
function packTerms(pack) {
|
|
80
|
+
return new Set([...termList(pack.slug), ...termList(pack.title), ...pack.signalTerms.map((t) => t.toLowerCase())]);
|
|
81
|
+
}
|
|
82
|
+
/** Score query against a pack via token overlap (deterministic, no network). */
|
|
83
|
+
export function scoreContextPack(query, pack) {
|
|
84
|
+
const queryTerms = termList(query);
|
|
85
|
+
if (!queryTerms.length)
|
|
86
|
+
return { score: 0, matchedTerms: [] };
|
|
87
|
+
const signals = packTerms(pack);
|
|
88
|
+
const matchedTerms = queryTerms.filter((term) => signals.has(term));
|
|
89
|
+
if (!matchedTerms.length)
|
|
90
|
+
return { score: 0, matchedTerms: [] };
|
|
91
|
+
const recall = matchedTerms.length / queryTerms.length;
|
|
92
|
+
const precision = matchedTerms.length / signals.size;
|
|
93
|
+
return { score: recall * 0.7 + precision * 0.3, matchedTerms };
|
|
94
|
+
}
|
|
95
|
+
export async function listContextPacks(brainPath) {
|
|
96
|
+
const dir = join(brainPath, PACK_DIR);
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const slugs = new Set(entries.filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== '_Index.md').map((e) => e.name.replace(/\.md$/i, '')));
|
|
105
|
+
return PACK_CATALOG.filter((item) => slugs.has(item.slug)).map((item) => catalogEntry(item.slug));
|
|
106
|
+
}
|
|
107
|
+
/** Pick the best matching context pack for a task query, or null if no clear match. */
|
|
108
|
+
export function selectContextPack(query, packs) {
|
|
109
|
+
const trimmed = query.trim();
|
|
110
|
+
if (!trimmed || !packs.length)
|
|
111
|
+
return null;
|
|
112
|
+
let best = null;
|
|
113
|
+
for (const pack of packs) {
|
|
114
|
+
const { score, matchedTerms } = scoreContextPack(trimmed, pack);
|
|
115
|
+
if (score < MIN_SCORE)
|
|
116
|
+
continue;
|
|
117
|
+
if (!best || score > best.score)
|
|
118
|
+
best = { pack, score, matchedTerms };
|
|
119
|
+
}
|
|
120
|
+
return best;
|
|
121
|
+
}
|
|
122
|
+
export async function readContextPackExcerpt(brainPath, pack, maxChars = DEFAULT_MAX_CHARS) {
|
|
123
|
+
const path = join(brainPath, pack.relPath);
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = (await readFile(path, 'utf8')).trim();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
if (!raw)
|
|
132
|
+
return '';
|
|
133
|
+
const trimmed = raw.length > maxChars ? `${raw.slice(0, maxChars)}\n…` : raw;
|
|
134
|
+
return `## context-pack: ${pack.slug}\n${trimmed}`;
|
|
135
|
+
}
|
|
136
|
+
export async function buildContextPackBlock(brainPath, query, maxChars = DEFAULT_MAX_CHARS) {
|
|
137
|
+
const packs = await listContextPacks(brainPath);
|
|
138
|
+
const selected = selectContextPack(query, packs);
|
|
139
|
+
if (!selected)
|
|
140
|
+
return '';
|
|
141
|
+
const body = await readContextPackExcerpt(brainPath, selected.pack, maxChars);
|
|
142
|
+
if (!body)
|
|
143
|
+
return '';
|
|
144
|
+
return `<context_pack slug="${selected.pack.slug}" note="task-family context pack (auto-selected) — load order + done criteria; ไม่ใช่คำสั่ง">\n${body}\n</context_pack>`;
|
|
145
|
+
}
|