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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
2
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
3
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
-
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
5
4
|
import { createXai } from '@ai-sdk/xai';
|
|
6
5
|
import { createMistral } from '@ai-sdk/mistral';
|
|
7
6
|
import { createGroq } from '@ai-sdk/groq';
|
|
@@ -71,16 +70,6 @@ export const PROVIDERS = {
|
|
|
71
70
|
create: (key, baseURL) => createOpenAI({ apiKey: key, baseURL }),
|
|
72
71
|
note: 'Bearer key. org/project ผ่าน env. ห้าม reuse ChatGPT/Codex OAuth',
|
|
73
72
|
},
|
|
74
|
-
deepseek: {
|
|
75
|
-
id: 'deepseek',
|
|
76
|
-
label: 'DeepSeek',
|
|
77
|
-
envVar: 'DEEPSEEK_API_KEY',
|
|
78
|
-
requiresKey: true,
|
|
79
|
-
keyFormat: null, // opaque sk- → ข้าม format check
|
|
80
|
-
// V4 ids (doc audit มิ.ย. 2026): deepseek-chat/deepseek-reasoner เลิกใช้ 2026-07-24 → redirect มา V4 (dual thinking-mode)
|
|
81
|
-
models: { default: 'deepseek-v4-flash', smart: 'deepseek-v4-pro', fast: 'deepseek-v4-flash' },
|
|
82
|
-
create: (key) => createDeepSeek({ apiKey: key }),
|
|
83
|
-
},
|
|
84
73
|
xai: {
|
|
85
74
|
id: 'xai',
|
|
86
75
|
label: 'xAI Grok',
|
|
@@ -120,7 +109,7 @@ export const PROVIDERS = {
|
|
|
120
109
|
requiresKey: false,
|
|
121
110
|
localPlaceholderKey: 'ollama',
|
|
122
111
|
keyFormat: null,
|
|
123
|
-
models: { default: '
|
|
112
|
+
models: { default: 'llama3.3', llama: 'llama3.3', mistral: 'mistral' },
|
|
124
113
|
create: (key, baseURL) => createOpenAICompatible({ name: 'ollama', apiKey: key, baseURL: baseURL ?? 'http://localhost:11434/v1' }),
|
|
125
114
|
note: 'OpenAI-compat /v1 endpoint. ไม่ต้อง key',
|
|
126
115
|
},
|
|
@@ -136,32 +125,6 @@ export const PROVIDERS = {
|
|
|
136
125
|
create: (key, baseURL) => createOpenAICompatible({ name: 'lmstudio', apiKey: key, baseURL: baseURL ?? 'http://localhost:1234/v1' }),
|
|
137
126
|
note: 'ต้อง Start Server ในแอปก่อน; โหลด model เดียว ใส่ id อะไรก็ serve ตัวนั้น',
|
|
138
127
|
},
|
|
139
|
-
// ── Cloud BYOK (OpenAI-compatible, จีน — data residency, ไม่มี OAuth landmine) ──
|
|
140
|
-
minimax: {
|
|
141
|
-
id: 'minimax',
|
|
142
|
-
label: 'MiniMax',
|
|
143
|
-
envVar: 'MINIMAX_API_KEY',
|
|
144
|
-
baseURL: 'https://api.minimax.io/v1',
|
|
145
|
-
requiresKey: true,
|
|
146
|
-
keyFormat: null, // opaque
|
|
147
|
-
models: { default: 'MiniMax-M2.7', smart: 'MiniMax-M3', fast: 'MiniMax-M2.7' },
|
|
148
|
-
create: (key, baseURL) => createOpenAICompatible({ name: 'minimax', apiKey: key, baseURL: baseURL ?? 'https://api.minimax.io/v1' }),
|
|
149
|
-
note: 'OpenAI-compat /v1. data จีน. MINIMAX_BASE_URL override (intl ↔ api.minimaxi.com/v1)',
|
|
150
|
-
},
|
|
151
|
-
glm: {
|
|
152
|
-
id: 'glm',
|
|
153
|
-
label: 'GLM (z.ai / Zhipu Coding Plan)',
|
|
154
|
-
envVar: 'ZHIPU_API_KEY',
|
|
155
|
-
envFallbacks: ['ZAI_API_KEY', 'GLM_API_KEY'],
|
|
156
|
-
// Coding Plan (subscription) ใช้ Anthropic Messages API — เหมือนที่ต่อกับ Claude Code.
|
|
157
|
-
// pay-as-you-go /paas/v4 (OpenAI-compat) มีแค่ glm-4.5-flash ฟรี ที่เหลือ 429 ถ้าไม่มี balance
|
|
158
|
-
baseURL: 'https://api.z.ai/api/anthropic/v1',
|
|
159
|
-
requiresKey: true,
|
|
160
|
-
keyFormat: null, // opaque ({id}.{secret})
|
|
161
|
-
models: { default: 'glm-4.6', smart: 'glm-5.1', air: 'glm-4.5-air', glm: 'glm-4.6' },
|
|
162
|
-
create: (key, baseURL) => createAnthropic({ apiKey: key, baseURL: baseURL ?? 'https://api.z.ai/api/anthropic/v1' }),
|
|
163
|
-
note: 'z.ai Coding Plan ผ่าน Anthropic Messages API. GLM_BASE_URL override → open.bigmodel.cn/api/anthropic/v1 (จีน)',
|
|
164
|
-
},
|
|
165
128
|
// ── Delegate: OpenAI Codex ผ่าน ChatGPT plan quota (wrap official codex CLI, ToS-safe) ──
|
|
166
129
|
codex: {
|
|
167
130
|
id: 'codex',
|
|
@@ -191,13 +154,10 @@ const GLOBAL_ALIAS = {
|
|
|
191
154
|
gemini: { provider: 'google', alias: 'gemini' },
|
|
192
155
|
flash: { provider: 'google', alias: 'flash' },
|
|
193
156
|
grok: { provider: 'xai', alias: 'grok' },
|
|
194
|
-
deepseek: { provider: 'deepseek', alias: 'default' },
|
|
195
157
|
mistral: { provider: 'mistral', alias: 'default' },
|
|
196
158
|
groq: { provider: 'groq', alias: 'default' },
|
|
197
159
|
ollama: { provider: 'ollama', alias: 'default' },
|
|
198
160
|
lmstudio: { provider: 'lmstudio', alias: 'default' },
|
|
199
|
-
glm: { provider: 'glm', alias: 'default' },
|
|
200
|
-
minimax: { provider: 'minimax', alias: 'default' },
|
|
201
161
|
};
|
|
202
162
|
/** parse "provider:model" | "provider:alias" | alias | "model" (default anthropic) */
|
|
203
163
|
export function parseSpec(spec) {
|
|
@@ -221,17 +181,18 @@ export function specKey(spec) {
|
|
|
221
181
|
const { provider, model } = parseSpec(spec);
|
|
222
182
|
return `${provider}:${model}`;
|
|
223
183
|
}
|
|
184
|
+
/** canonical display/state spec: aliases become "provider:model-id" before reaching the REPL state. */
|
|
185
|
+
export function canonicalSpec(spec) {
|
|
186
|
+
return specKey(spec.trim());
|
|
187
|
+
}
|
|
224
188
|
/** หน้า console ที่ใช้สร้าง API key ต่อ provider — โชว์ในข้อความ error/wizard ("ไปเอา key ที่ไหน") */
|
|
225
189
|
const CONSOLE_URLS = {
|
|
226
190
|
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
227
191
|
google: 'https://aistudio.google.com/apikey',
|
|
228
192
|
openai: 'https://platform.openai.com/api-keys',
|
|
229
|
-
deepseek: 'https://platform.deepseek.com/api_keys',
|
|
230
193
|
xai: 'https://console.x.ai',
|
|
231
194
|
mistral: 'https://console.mistral.ai/api-keys',
|
|
232
195
|
groq: 'https://console.groq.com/keys',
|
|
233
|
-
minimax: 'https://platform.minimax.io',
|
|
234
|
-
glm: 'https://z.ai/manage-apikey/apikey-list',
|
|
235
196
|
};
|
|
236
197
|
export function consoleUrl(provider) {
|
|
237
198
|
return CONSOLE_URLS[provider];
|
|
@@ -245,6 +206,8 @@ export function hasUsableEnvKey(provider) {
|
|
|
245
206
|
const cfg = PROVIDERS[provider];
|
|
246
207
|
if (!cfg)
|
|
247
208
|
return false;
|
|
209
|
+
if (cfg.kind === 'delegate')
|
|
210
|
+
return false; // ต้องเช็ก readiness แยก (เช่น codex CLI installed + logged in)
|
|
248
211
|
if (!cfg.requiresKey)
|
|
249
212
|
return true; // local — ไม่ต้อง key
|
|
250
213
|
const k = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
@@ -260,7 +223,7 @@ export function hasUsableEnvKey(provider) {
|
|
|
260
223
|
}
|
|
261
224
|
/** หา provider ที่ "มี key ใช้ได้จริงใน env" (cloud, ตามลำดับนิยม) — ใช้ทำ first-run smart skip + แนะ headless */
|
|
262
225
|
export function detectEnvProvider() {
|
|
263
|
-
for (const id of ['anthropic', 'openai', 'google', '
|
|
226
|
+
for (const id of ['anthropic', 'openai', 'google', 'xai', 'mistral', 'groq']) {
|
|
264
227
|
const cfg = PROVIDERS[id];
|
|
265
228
|
if (cfg?.requiresKey && hasUsableEnvKey(id)) {
|
|
266
229
|
return { provider: id, label: cfg.label, envVar: cfg.envVar, model: cfg.models.default };
|
|
@@ -292,10 +255,14 @@ export function resolveModel(spec) {
|
|
|
292
255
|
const found = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
293
256
|
if (!found) {
|
|
294
257
|
const url = consoleUrl(provider);
|
|
258
|
+
const codexHint = provider === 'openai'
|
|
259
|
+
? `\n • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: เลือก \`/model codex\` แล้วรัน \`codex login\``
|
|
260
|
+
: '';
|
|
295
261
|
throw new Error(`ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar})\n` +
|
|
296
262
|
(url ? ` • เอา key ที่: ${url}\n` : '') +
|
|
297
263
|
` • ตั้ง: export ${cfg.envVar}="..." ` +
|
|
298
|
-
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard`
|
|
264
|
+
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard` +
|
|
265
|
+
codexHint);
|
|
299
266
|
}
|
|
300
267
|
assertDirectApiKey(cfg, found); // reject OAuth/subscription token + format ผิด
|
|
301
268
|
key = found;
|
|
@@ -303,7 +270,7 @@ export function resolveModel(spec) {
|
|
|
303
270
|
else {
|
|
304
271
|
key = resolveKeyFromEnv(cfg.envVar) ?? cfg.localPlaceholderKey ?? 'local';
|
|
305
272
|
}
|
|
306
|
-
// <PROVIDER>_BASE_URL env → override (สลับ region
|
|
273
|
+
// <PROVIDER>_BASE_URL env → override (สลับ region); ไม่งั้น local อ่าน env, cloud ใช้ default
|
|
307
274
|
const baseURL = process.env[`${cfg.id.toUpperCase()}_BASE_URL`] ??
|
|
308
275
|
(cfg.requiresKey ? cfg.baseURL : process.env[cfg.envVar] ?? cfg.baseURL);
|
|
309
276
|
return cfg.create(key, baseURL)(model);
|
package/dist/search/chunk.js
CHANGED
|
@@ -12,15 +12,11 @@
|
|
|
12
12
|
// or a stray [[ inside a code fence degrade to "no frontmatter / no links"
|
|
13
13
|
// rather than throwing. We must never block indexing a real, messy vault file.
|
|
14
14
|
// ============================================================================
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
15
16
|
const MIN_CHARS = 120; // sections shorter than this fold into the next chunk
|
|
16
|
-
/** deterministic
|
|
17
|
+
/** deterministic path hash — SHA-256 prefix keeps chunk ids short without 32-bit collision risk. */
|
|
17
18
|
export function pathHash(path) {
|
|
18
|
-
|
|
19
|
-
for (let i = 0; i < path.length; i++) {
|
|
20
|
-
h ^= path.charCodeAt(i);
|
|
21
|
-
h = Math.imul(h, 0x01000193);
|
|
22
|
-
}
|
|
23
|
-
return (h >>> 0).toString(36);
|
|
19
|
+
return createHash('sha256').update(path).digest('hex').slice(0, 16);
|
|
24
20
|
}
|
|
25
21
|
/** split a leading `---\n…\n---` frontmatter block from the body. Defensive: no block ⇒ {} + full md. */
|
|
26
22
|
export function parseFrontmatter(md) {
|
|
@@ -31,7 +27,10 @@ export function parseFrontmatter(md) {
|
|
|
31
27
|
if (end === -1)
|
|
32
28
|
return { data: empty, body: md };
|
|
33
29
|
const block = md.slice(3, end).trim();
|
|
34
|
-
|
|
30
|
+
// หา newline หลัง closing fence; ถ้าไม่มี (frontmatter-only ไม่มี trailing newline) body = '' ไม่ใช่ทั้งไฟล์
|
|
31
|
+
// (indexOf คืน -1 → slice(0) = ทั้งไฟล์ → frontmatter รั่วเข้า body ทำ index/search เพี้ยน)
|
|
32
|
+
const afterFence = md.indexOf('\n', end + 1);
|
|
33
|
+
const body = (afterFence === -1 ? '' : md.slice(afterFence + 1)).replace(/^\n+/, '');
|
|
35
34
|
const data = { tags: [] };
|
|
36
35
|
const lines = block.split('\n');
|
|
37
36
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { inlineValue, takeValue } from '../cli-option-values.js';
|
|
2
|
+
import { SEARCH_SOURCES } from './index-core.js';
|
|
3
|
+
const SEARCH_MODES = ['auto', 'fts', 'semantic', 'hybrid'];
|
|
4
|
+
function isSearchMode(v) {
|
|
5
|
+
return SEARCH_MODES.includes(v);
|
|
6
|
+
}
|
|
7
|
+
function isSearchSource(v) {
|
|
8
|
+
return SEARCH_SOURCES.includes(v);
|
|
9
|
+
}
|
|
10
|
+
function parsePositiveInteger(raw) {
|
|
11
|
+
if (!raw || !/^[1-9]\d*$/.test(raw))
|
|
12
|
+
return undefined;
|
|
13
|
+
const n = Number(raw);
|
|
14
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
15
|
+
}
|
|
16
|
+
function inlineSourceValue(value) {
|
|
17
|
+
return inlineValue('--source', value) ?? inlineValue('--sources', value);
|
|
18
|
+
}
|
|
19
|
+
export function parseSearchArgs(args) {
|
|
20
|
+
const queryParts = [];
|
|
21
|
+
let mode = 'auto';
|
|
22
|
+
let modeSet = false;
|
|
23
|
+
let limit = 8;
|
|
24
|
+
let limitSet = false;
|
|
25
|
+
let sources;
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (a === '--') {
|
|
29
|
+
queryParts.push(...args.slice(i + 1));
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
else if (a === '--mode' || a.startsWith('--mode=')) {
|
|
33
|
+
const next = a === '--mode' ? takeValue(args, i) : undefined;
|
|
34
|
+
const v = next ? next.value : inlineValue('--mode', a);
|
|
35
|
+
if (next)
|
|
36
|
+
i = next.nextIndex;
|
|
37
|
+
if (!v)
|
|
38
|
+
return { ok: false, message: `--mode ต้องระบุค่าเป็น ${SEARCH_MODES.join('|')}` };
|
|
39
|
+
if (!isSearchMode(v))
|
|
40
|
+
return { ok: false, message: `--mode ต้องเป็น ${SEARCH_MODES.join('|')}` };
|
|
41
|
+
if (modeSet)
|
|
42
|
+
return { ok: false, message: 'ใช้ --mode เพียงครั้งเดียว' };
|
|
43
|
+
mode = v;
|
|
44
|
+
modeSet = true;
|
|
45
|
+
}
|
|
46
|
+
else if (a === '--limit' || a.startsWith('--limit=')) {
|
|
47
|
+
const next = a === '--limit' ? takeValue(args, i) : undefined;
|
|
48
|
+
const raw = next ? next.value : inlineValue('--limit', a);
|
|
49
|
+
if (next)
|
|
50
|
+
i = next.nextIndex;
|
|
51
|
+
if (!raw)
|
|
52
|
+
return { ok: false, message: '--limit ต้องระบุค่าเป็น integer บวก เช่น 8' };
|
|
53
|
+
const n = parsePositiveInteger(raw);
|
|
54
|
+
if (n === undefined)
|
|
55
|
+
return { ok: false, message: '--limit ต้องเป็น integer บวก เช่น 8' };
|
|
56
|
+
if (limitSet)
|
|
57
|
+
return { ok: false, message: 'ใช้ --limit เพียงครั้งเดียว' };
|
|
58
|
+
limit = n;
|
|
59
|
+
limitSet = true;
|
|
60
|
+
}
|
|
61
|
+
else if (a === '--source' || a === '--sources' || a.startsWith('--source=') || a.startsWith('--sources=')) {
|
|
62
|
+
const next = a === '--source' || a === '--sources' ? takeValue(args, i) : undefined;
|
|
63
|
+
const raw = next ? next.value : inlineSourceValue(a);
|
|
64
|
+
if (next)
|
|
65
|
+
i = next.nextIndex;
|
|
66
|
+
const requested = (raw ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
67
|
+
const bad = requested.filter((s) => !isSearchSource(s));
|
|
68
|
+
if (!requested.length) {
|
|
69
|
+
return { ok: false, message: `--source ต้องระบุค่าเป็น ${SEARCH_SOURCES.join(',')} (คั่นหลายค่าได้ด้วย comma)` };
|
|
70
|
+
}
|
|
71
|
+
if (bad.length)
|
|
72
|
+
return { ok: false, message: `--source ต้องเป็น ${SEARCH_SOURCES.join(',')} (คั่นหลายค่าได้ด้วย comma)` };
|
|
73
|
+
sources = [...new Set([...(sources ?? []), ...requested])];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
queryParts.push(a);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const query = queryParts.join(' ').trim();
|
|
80
|
+
if (!query)
|
|
81
|
+
return { ok: false, message: 'ต้องใส่ query สำหรับค้นหา' };
|
|
82
|
+
return { ok: true, value: { query, mode, limit, sources } };
|
|
83
|
+
}
|
|
@@ -139,6 +139,9 @@ export async function saveVectors(vi) {
|
|
|
139
139
|
throw e;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
export function invalidateVectors(tag = '') {
|
|
143
|
+
return saveVectors(emptyVectors(tag));
|
|
144
|
+
}
|
|
142
145
|
export async function vectorsMtimeMs() {
|
|
143
146
|
try {
|
|
144
147
|
return (await stat(VECTORS_PATH)).mtimeMs;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { appHomePath } from '../brand.js';
|
|
3
|
+
const EMBEDDING_MODEL_ENV = 'SANOOK_EMBEDDING_MODEL';
|
|
4
|
+
export function cleanEmbeddingModelSpec(v) {
|
|
5
|
+
if (typeof v !== 'string')
|
|
6
|
+
return undefined;
|
|
7
|
+
const clean = v.trim();
|
|
8
|
+
return clean ? clean : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** read an optional embeddingModel spec from ~/.sanook/config.json. */
|
|
11
|
+
export async function configEmbeddingModel() {
|
|
12
|
+
try {
|
|
13
|
+
const cfg = JSON.parse(await readFile(appHomePath('config.json'), 'utf8'));
|
|
14
|
+
return cleanEmbeddingModelSpec(cfg.embeddingModel);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function embeddingModelSpec(override) {
|
|
21
|
+
return cleanEmbeddingModelSpec(override) ?? cleanEmbeddingModelSpec(process.env[EMBEDDING_MODEL_ENV]) ?? (await configEmbeddingModel());
|
|
22
|
+
}
|
package/dist/search/engine.js
CHANGED
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
// lazily, and on ANY embedding error degrades to BM25 with a `degraded` flag —
|
|
15
15
|
// search must never throw at the floor.
|
|
16
16
|
// ============================================================================
|
|
17
|
-
import { readFile } from 'node:fs/promises';
|
|
18
|
-
import { appHomePath } from '../brand.js';
|
|
19
17
|
import { bm25Search, termList } from './index-core.js';
|
|
20
18
|
import { rrfFuse } from './fuse.js';
|
|
21
19
|
import { cosineTopK, embedQuery, getEmbedder, loadVectors, vectorsMtimeMs, } from './embed-store.js';
|
|
20
|
+
import { embeddingModelSpec } from './embedding-config.js';
|
|
22
21
|
import { indexMtimeMs, loadIndex } from './store.js';
|
|
23
22
|
const CAND = 60; // candidate pool depth per leg before fusion/limit
|
|
24
23
|
const SNIPPET_WIDTH = 64;
|
|
@@ -138,16 +137,6 @@ async function cachedVectors() {
|
|
|
138
137
|
}
|
|
139
138
|
return vectorCache.vectors;
|
|
140
139
|
}
|
|
141
|
-
/** read an optional embeddingModel spec from ~/.sanook/config.json. */
|
|
142
|
-
async function configEmbeddingModel() {
|
|
143
|
-
try {
|
|
144
|
-
const cfg = JSON.parse(await readFile(appHomePath('config.json'), 'utf8'));
|
|
145
|
-
return cfg.embeddingModel;
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
140
|
/** drop in-process caches (tests + after a reindex in the same process). */
|
|
152
141
|
export function resetSearchCaches() {
|
|
153
142
|
indexCache = null;
|
|
@@ -165,7 +154,7 @@ export async function search(query, opts = {}) {
|
|
|
165
154
|
const mode = opts.mode ?? 'auto';
|
|
166
155
|
if (mode === 'fts')
|
|
167
156
|
return rankSearch(index, query, opts);
|
|
168
|
-
const spec = opts.embeddingModel
|
|
157
|
+
const spec = await embeddingModelSpec(opts.embeddingModel);
|
|
169
158
|
const embedder = getEmbedder(spec);
|
|
170
159
|
if (!embedder) {
|
|
171
160
|
const res = rankSearch(index, query, opts);
|
package/dist/search/indexer.js
CHANGED
|
@@ -24,7 +24,9 @@ import { loadSkills } from '../skills.js';
|
|
|
24
24
|
import { activeFacts, effImportance, loadStore } from '../memory-store.js';
|
|
25
25
|
import { chunkMarkdown } from './chunk.js';
|
|
26
26
|
import { addDoc, removeDoc, removeSource } from './index-core.js';
|
|
27
|
+
import { embeddingModelSpec } from './embedding-config.js';
|
|
27
28
|
import { loadIndex, saveIndex } from './store.js';
|
|
29
|
+
import { buildVectorIndex, embedTexts, getEmbedder, invalidateVectors, saveVectors } from './embed-store.js';
|
|
28
30
|
/** strip a .md path to a human title fallback when a chunk has no heading. */
|
|
29
31
|
function fileTitle(rel) {
|
|
30
32
|
return (rel.split('/').pop() ?? rel).replace(/\.md$/i, '');
|
|
@@ -37,6 +39,14 @@ export async function indexVaultFiles(index, manifest, fs) {
|
|
|
37
39
|
const next = {};
|
|
38
40
|
const diff = { added: 0, updated: 0, removed: 0, skipped: 0 };
|
|
39
41
|
const paths = await fs.listMarkdown();
|
|
42
|
+
// Guard against a momentarily-unreadable vault (unmounted drive / perms blip / wrong cwd): walk()
|
|
43
|
+
// swallows readdir errors and returns [], which would otherwise evict the ENTIRE persisted index
|
|
44
|
+
// via the deletion sweep below. If a non-empty manifest just lost >50% of its files, treat it as a
|
|
45
|
+
// transient read failure and keep the existing index+manifest untouched (recovers on the next pass).
|
|
46
|
+
const manifestSize = Object.keys(manifest).length;
|
|
47
|
+
if (manifestSize > 0 && paths.length < Math.ceil(manifestSize * 0.5)) {
|
|
48
|
+
return { manifest, diff: { added: 0, updated: 0, removed: 0, skipped: manifestSize } };
|
|
49
|
+
}
|
|
40
50
|
const seenExisting = new Set();
|
|
41
51
|
for (const rel of paths) {
|
|
42
52
|
const fp = await fs.fingerprint(rel);
|
|
@@ -130,6 +140,21 @@ export function foldSkills(index, skills) {
|
|
|
130
140
|
}
|
|
131
141
|
return skills.length;
|
|
132
142
|
}
|
|
143
|
+
function docEmbeddingText(doc) {
|
|
144
|
+
return [doc.title?.trim(), doc.text.trim()].filter(Boolean).join('\n').slice(0, 4000);
|
|
145
|
+
}
|
|
146
|
+
export async function vectorizeIndex(index, tag, embed) {
|
|
147
|
+
const docs = [...index.docs.values()]
|
|
148
|
+
.filter((d) => d.text.trim())
|
|
149
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
150
|
+
if (!docs.length)
|
|
151
|
+
return buildVectorIndex(tag, []);
|
|
152
|
+
const vectors = await embed(docs.map(docEmbeddingText));
|
|
153
|
+
if (vectors.length !== docs.length) {
|
|
154
|
+
throw new Error(`embedding count mismatch: expected ${docs.length}, got ${vectors.length}`);
|
|
155
|
+
}
|
|
156
|
+
return buildVectorIndex(tag, docs.map((d, i) => ({ id: d.id, vec: vectors[i] })));
|
|
157
|
+
}
|
|
133
158
|
// ---- real-filesystem wiring ------------------------------------------------
|
|
134
159
|
const IGNORE_DIRS = new Set([
|
|
135
160
|
'node_modules', 'dist', 'build', 'coverage', '.next', '.cache', '.git',
|
|
@@ -237,5 +262,23 @@ export async function reindex(now = Date.now()) {
|
|
|
237
262
|
text: `${s.description} ${s.whenToUse ?? ''}`.trim(),
|
|
238
263
|
})));
|
|
239
264
|
await saveIndex(index, nextManifest);
|
|
240
|
-
|
|
265
|
+
let vectors = 0;
|
|
266
|
+
const embedder = getEmbedder(await embeddingModelSpec());
|
|
267
|
+
if (!embedder) {
|
|
268
|
+
await invalidateVectors().catch(() => { });
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
const vi = await vectorizeIndex(index, embedder.tag, (texts) => embedTexts(embedder, texts));
|
|
273
|
+
await saveVectors(vi);
|
|
274
|
+
vectors = vi.ids.length;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Semantic search is optional. A provider/network failure must never break
|
|
278
|
+
// the BM25 floor. Clear stale vectors so hybrid/semantic cannot rank the
|
|
279
|
+
// freshly-saved BM25 index with embeddings from an older corpus.
|
|
280
|
+
await invalidateVectors(embedder.tag).catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { ...diff, memory, sessions, skills, vectors, vaultPath: brain ?? null };
|
|
241
284
|
}
|
package/dist/search/store.js
CHANGED
|
@@ -34,6 +34,28 @@ export async function indexMtimeMs() {
|
|
|
34
34
|
return 0;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
export function sanitizeManifest(raw) {
|
|
38
|
+
const out = Object.create(null);
|
|
39
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
40
|
+
return out;
|
|
41
|
+
for (const [path, entry] of Object.entries(raw)) {
|
|
42
|
+
if (!path || ['__proto__', 'prototype', 'constructor'].includes(path))
|
|
43
|
+
continue;
|
|
44
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
45
|
+
continue;
|
|
46
|
+
const r = entry;
|
|
47
|
+
const mtimeMs = Number(r.mtimeMs);
|
|
48
|
+
const size = Number(r.size);
|
|
49
|
+
if (!Number.isFinite(mtimeMs) || mtimeMs < 0 || !Number.isFinite(size) || size < 0)
|
|
50
|
+
continue;
|
|
51
|
+
if (typeof r.sha !== 'string' || !r.sha || r.sha.length > 256)
|
|
52
|
+
continue;
|
|
53
|
+
if (!Array.isArray(r.ids) || !r.ids.every((id) => typeof id === 'string' && id.length > 0 && id.length < 1024))
|
|
54
|
+
continue;
|
|
55
|
+
out[path] = { mtimeMs, size, sha: r.sha, ids: [...r.ids] };
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* Load the persisted index + manifest. Pure read: a missing or malformed file
|
|
39
61
|
* degrades to an empty index rather than throwing, so a corrupt cache never
|
|
@@ -43,7 +65,7 @@ export async function loadIndex() {
|
|
|
43
65
|
try {
|
|
44
66
|
const raw = JSON.parse(await readFile(INDEX_PATH, 'utf8'));
|
|
45
67
|
if (raw && raw.v === FILE_VERSION) {
|
|
46
|
-
return { index: indexFromJSON(raw.index), manifest: raw.manifest
|
|
68
|
+
return { index: indexFromJSON(raw.index), manifest: sanitizeManifest(raw.manifest) };
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
71
|
catch {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Session knowledge distiller — extracts DURABLE facts (decisions, gotchas, preferences,
|
|
2
|
+
// constraints) from a finished session transcript so they can be folded into the memory store
|
|
3
|
+
// WITHOUT the model voluntarily calling `remember`. Pure + deterministic (heuristic): the offline,
|
|
4
|
+
// zero-cost fallback. An LLM-based extractor can layer on top when a model is available.
|
|
5
|
+
// Signal patterns — a sentence is a candidate only if it matches one (keeps precision up).
|
|
6
|
+
const SIGNALS = [
|
|
7
|
+
{ kind: 'decision', re: /\b(decided|we['’]?ll use|we will use|we use|going with|chose|switch(?:ed|ing)? to|standardiz(?:e|ed|ing) on|settled on|agreed to)\b/i },
|
|
8
|
+
{ kind: 'preference', re: /\b(prefer(?:s|red)?|convention(?: is|:)|by convention|coding style|likes? to|always (?:use|run|prefer|name)|we name)\b/i },
|
|
9
|
+
{ kind: 'constraint', re: /\b(must not|must|never|do ?n['’]?t|don['’]t|required to|is required|only (?:use|run|allow)|forbidden|not allowed|has to)\b/i },
|
|
10
|
+
{ kind: 'gotcha', re: /\b(gotcha|caveat|watch out|the (?:bug|issue|problem|error) (?:was|is)|turned out|root cause|fix(?:ed)? (?:was|by|it by)|fails? (?:if|when|because)|breaks? (?:if|when)|broke because|because the|note that|important:|heads up)\b/i },
|
|
11
|
+
];
|
|
12
|
+
// Strong "X not Y" / "X instead of Y" decision signal (e.g. "pnpm not npm", "tabs over spaces").
|
|
13
|
+
const X_NOT_Y = /\b[\w.@/+-]{2,}\s+(?:not|instead of|over|rather than)\s+[\w.@/+-]{2,}\b/i;
|
|
14
|
+
const MAX_CANDIDATES = 12;
|
|
15
|
+
const MIN_WORDS = 4;
|
|
16
|
+
const MAX_WORDS = 45;
|
|
17
|
+
function looksLikeCodeOrLog(s) {
|
|
18
|
+
if (/^\s*[$#>]/.test(s))
|
|
19
|
+
return true; // shell prompt / diff marker
|
|
20
|
+
if (/[{};=]\s*$/.test(s) && /[(){}\[\]=;]/.test(s))
|
|
21
|
+
return true; // code-ish line
|
|
22
|
+
if (/\b(at |Error:|Traceback|stack trace|node_modules\/)/.test(s) && /:\d+/.test(s))
|
|
23
|
+
return true; // stack trace
|
|
24
|
+
const symbolRatio = (s.replace(/[\w\s]/g, '').length || 0) / Math.max(1, s.length);
|
|
25
|
+
return symbolRatio > 0.3; // mostly punctuation/symbols
|
|
26
|
+
}
|
|
27
|
+
function splitSentences(text) {
|
|
28
|
+
return text
|
|
29
|
+
.split(/(?<=[.!?])\s+|\n+/)
|
|
30
|
+
.map((s) => s.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
function normalize(s) {
|
|
34
|
+
return s.replace(/\s+/g, ' ').replace(/^[-*•\d.)\s]+/, '').trim();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract durable-fact candidates from a transcript. Skips questions, chit-chat, code/log lines,
|
|
38
|
+
* and too-short/too-long sentences; requires a decision/gotcha/preference/constraint signal.
|
|
39
|
+
*/
|
|
40
|
+
export function distillSession(messages) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const msg of messages) {
|
|
44
|
+
if (msg.role !== 'user' && msg.role !== 'assistant')
|
|
45
|
+
continue;
|
|
46
|
+
for (const raw of splitSentences(msg.text)) {
|
|
47
|
+
const s = normalize(raw);
|
|
48
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
49
|
+
if (words.length < MIN_WORDS || words.length > MAX_WORDS)
|
|
50
|
+
continue;
|
|
51
|
+
if (s.endsWith('?'))
|
|
52
|
+
continue; // questions aren't durable facts
|
|
53
|
+
if (looksLikeCodeOrLog(s))
|
|
54
|
+
continue;
|
|
55
|
+
const signal = SIGNALS.find((sig) => sig.re.test(s));
|
|
56
|
+
const kind = signal?.kind ?? (X_NOT_Y.test(s) ? 'decision' : undefined);
|
|
57
|
+
if (!kind)
|
|
58
|
+
continue;
|
|
59
|
+
const key = s.toLowerCase().replace(/[^a-z0-9 ]/g, '').trim();
|
|
60
|
+
if (!key || seen.has(key))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
out.push({ text: s, kind });
|
|
64
|
+
if (out.length >= MAX_CANDIDATES)
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** flatten an AI-SDK ModelMessage content (string | parts[]) to its plain text. */
|
|
71
|
+
function messageText(content) {
|
|
72
|
+
if (typeof content === 'string')
|
|
73
|
+
return content;
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return '';
|
|
76
|
+
return content
|
|
77
|
+
.map((p) => (p && typeof p === 'object' && 'text' in p && typeof p.text === 'string' ? p.text : ''))
|
|
78
|
+
.join(' ')
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
81
|
+
/** distill durable-fact texts from a finished conversation (ModelMessage[]-shaped). Pure. */
|
|
82
|
+
export function distilledFactsFromMessages(messages) {
|
|
83
|
+
return distillSession(messages.map((m) => ({ role: m.role, text: messageText(m.content) }))).map((c) => c.text);
|
|
84
|
+
}
|