sanook-cli 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { inferRegistryServerRisk, formatMcpRiskLabel } from './mcp-risk.js';
|
|
2
|
+
import { inlineValue, takeValue } from './cli-option-values.js';
|
|
3
|
+
export const MCP_REGISTRY_BASE_URL = 'https://registry.modelcontextprotocol.io/v0';
|
|
4
|
+
export const MCP_PRESETS = [
|
|
5
|
+
{
|
|
6
|
+
name: 'dev',
|
|
7
|
+
description: 'Repo/issues/releases, error debugging, and versioned docs.',
|
|
8
|
+
servers: ['com.gitlab/mcp', 'com.mcparmory/github', 'com.mcparmory/sentry', 'ai.smithery/renCosta2025-context7fork'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'research',
|
|
12
|
+
description: 'Web/doc fetching, search, and knowledge intake.',
|
|
13
|
+
servers: ['ai.smithery/smithery-ai-fetch', 'ai.groundroute/web-search', 'ai.smithery/arjunkmrm-brave-search-mcp-server', 'ai.smithery/sunub-obsidian-mcp-server'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'pm',
|
|
17
|
+
description: 'Issue tracking, planning, and team/workspace context.',
|
|
18
|
+
servers: ['app.linear/linear', 'ai.waystation/jira', 'ai.waystation/slack', 'com.mcparmory/notion'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'ops',
|
|
22
|
+
description: 'Read-only data inspection, production errors, and infra helpers.',
|
|
23
|
+
servers: ['capital.hove/read-only-local-postgres-mcp-server', 'com.mcparmory/sentry', 'io.github.CSOAI-ORG/docker-helper-ai-mcp'],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const REGISTRY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
const registryCache = new Map();
|
|
28
|
+
export function clearMcpRegistryCache() {
|
|
29
|
+
registryCache.clear();
|
|
30
|
+
}
|
|
31
|
+
function cacheKey(url) {
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
function readRegistryCache(url) {
|
|
35
|
+
const entry = registryCache.get(cacheKey(url));
|
|
36
|
+
if (!entry)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (Date.now() >= entry.expiresAt) {
|
|
39
|
+
registryCache.delete(cacheKey(url));
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return entry.value;
|
|
43
|
+
}
|
|
44
|
+
function writeRegistryCache(url, value) {
|
|
45
|
+
registryCache.set(cacheKey(url), { expiresAt: Date.now() + REGISTRY_CACHE_TTL_MS, value });
|
|
46
|
+
}
|
|
47
|
+
export { REGISTRY_CACHE_TTL_MS };
|
|
48
|
+
export function parseKeyValueList(values) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
const parsed = parseKeyValueEntry(value);
|
|
52
|
+
out[parsed.key] = parsed.value;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function parseKeyValueEntry(value) {
|
|
57
|
+
const idx = value.indexOf('=');
|
|
58
|
+
if (idx <= 0)
|
|
59
|
+
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
60
|
+
const key = value.slice(0, idx).trim();
|
|
61
|
+
if (!key)
|
|
62
|
+
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
63
|
+
return { key, value: value.slice(idx + 1) };
|
|
64
|
+
}
|
|
65
|
+
function parseRegistrySearchLimit(raw) {
|
|
66
|
+
if (!raw || !/^[1-9]\d*$/.test(raw))
|
|
67
|
+
return undefined;
|
|
68
|
+
const n = Number(raw);
|
|
69
|
+
return Number.isSafeInteger(n) && n <= 50 ? n : undefined;
|
|
70
|
+
}
|
|
71
|
+
export function parseMcpRegistrySearchArgs(args) {
|
|
72
|
+
const query = [];
|
|
73
|
+
let limit = 10;
|
|
74
|
+
let limitSet = false;
|
|
75
|
+
let cursor;
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const a = args[i];
|
|
78
|
+
if (a === '--') {
|
|
79
|
+
query.push(...args.slice(i + 1));
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
if (a === '--limit' || a.startsWith('--limit=')) {
|
|
83
|
+
const next = a === '--limit' ? takeValue(args, i) : undefined;
|
|
84
|
+
const raw = next ? next.value : inlineValue('--limit', a);
|
|
85
|
+
if (next)
|
|
86
|
+
i = next.nextIndex;
|
|
87
|
+
const parsed = parseRegistrySearchLimit(raw);
|
|
88
|
+
if (parsed === undefined)
|
|
89
|
+
return { ok: false, message: '--limit ต้องเป็นจำนวนเต็ม 1-50' };
|
|
90
|
+
if (limitSet)
|
|
91
|
+
return { ok: false, message: 'ใช้ --limit เพียงครั้งเดียว' };
|
|
92
|
+
limit = parsed;
|
|
93
|
+
limitSet = true;
|
|
94
|
+
}
|
|
95
|
+
else if (a === '--cursor' || a.startsWith('--cursor=')) {
|
|
96
|
+
const next = a === '--cursor' ? takeValue(args, i) : undefined;
|
|
97
|
+
const raw = next ? next.value : inlineValue('--cursor', a);
|
|
98
|
+
if (next)
|
|
99
|
+
i = next.nextIndex;
|
|
100
|
+
const parsed = raw?.trim();
|
|
101
|
+
if (!parsed)
|
|
102
|
+
return { ok: false, message: '--cursor ต้องระบุค่า' };
|
|
103
|
+
if (cursor !== undefined)
|
|
104
|
+
return { ok: false, message: 'ใช้ --cursor เพียงครั้งเดียว' };
|
|
105
|
+
cursor = parsed;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
query.push(a);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, value: { query: query.join(' ').trim(), limit, cursor } };
|
|
112
|
+
}
|
|
113
|
+
function parseInstallOptionValue(args, index, flag) {
|
|
114
|
+
const arg = args[index];
|
|
115
|
+
if (arg === flag)
|
|
116
|
+
return takeValue(args, index);
|
|
117
|
+
return { value: inlineValue(flag, arg), nextIndex: index };
|
|
118
|
+
}
|
|
119
|
+
export function parseMcpRegistryInstallArgs(args) {
|
|
120
|
+
const positionals = [];
|
|
121
|
+
const env = [];
|
|
122
|
+
const headers = [];
|
|
123
|
+
let alias;
|
|
124
|
+
let transport;
|
|
125
|
+
let version;
|
|
126
|
+
let project = false;
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const a = args[i];
|
|
129
|
+
if (a === '--') {
|
|
130
|
+
positionals.push(...args.slice(i + 1));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (a === '--project') {
|
|
134
|
+
project = true;
|
|
135
|
+
}
|
|
136
|
+
else if (a === '--name' || a.startsWith('--name=')) {
|
|
137
|
+
const next = parseInstallOptionValue(args, i, '--name');
|
|
138
|
+
if (next.nextIndex !== i)
|
|
139
|
+
i = next.nextIndex;
|
|
140
|
+
const value = next.value?.trim();
|
|
141
|
+
if (!value)
|
|
142
|
+
return { ok: false, message: '--name ต้องระบุค่า' };
|
|
143
|
+
if (alias !== undefined)
|
|
144
|
+
return { ok: false, message: 'ใช้ --name เพียงครั้งเดียว' };
|
|
145
|
+
alias = value;
|
|
146
|
+
}
|
|
147
|
+
else if (a === '--transport' || a.startsWith('--transport=')) {
|
|
148
|
+
const next = parseInstallOptionValue(args, i, '--transport');
|
|
149
|
+
if (next.nextIndex !== i)
|
|
150
|
+
i = next.nextIndex;
|
|
151
|
+
const value = next.value?.trim();
|
|
152
|
+
if (!value)
|
|
153
|
+
return { ok: false, message: '--transport ต้องระบุค่า' };
|
|
154
|
+
if (!['auto', 'remote', 'stdio'].includes(value)) {
|
|
155
|
+
return { ok: false, message: '--transport ต้องเป็น auto, remote, หรือ stdio' };
|
|
156
|
+
}
|
|
157
|
+
if (transport !== undefined)
|
|
158
|
+
return { ok: false, message: 'ใช้ --transport เพียงครั้งเดียว' };
|
|
159
|
+
transport = value;
|
|
160
|
+
}
|
|
161
|
+
else if (a === '--version' || a.startsWith('--version=')) {
|
|
162
|
+
const next = parseInstallOptionValue(args, i, '--version');
|
|
163
|
+
if (next.nextIndex !== i)
|
|
164
|
+
i = next.nextIndex;
|
|
165
|
+
const value = next.value?.trim();
|
|
166
|
+
if (!value)
|
|
167
|
+
return { ok: false, message: '--version ต้องระบุค่า' };
|
|
168
|
+
if (version !== undefined)
|
|
169
|
+
return { ok: false, message: 'ใช้ --version เพียงครั้งเดียว' };
|
|
170
|
+
version = value;
|
|
171
|
+
}
|
|
172
|
+
else if (a === '--env' || a.startsWith('--env=')) {
|
|
173
|
+
const next = parseInstallOptionValue(args, i, '--env');
|
|
174
|
+
if (next.nextIndex !== i)
|
|
175
|
+
i = next.nextIndex;
|
|
176
|
+
const value = next.value;
|
|
177
|
+
if (!value?.trim())
|
|
178
|
+
return { ok: false, message: '--env ต้องระบุ KEY=value' };
|
|
179
|
+
try {
|
|
180
|
+
parseKeyValueEntry(value);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { ok: false, message: `--env ต้องใช้รูปแบบ KEY=value: ${value}` };
|
|
184
|
+
}
|
|
185
|
+
env.push(value);
|
|
186
|
+
}
|
|
187
|
+
else if (a === '--header' || a.startsWith('--header=')) {
|
|
188
|
+
const next = parseInstallOptionValue(args, i, '--header');
|
|
189
|
+
if (next.nextIndex !== i)
|
|
190
|
+
i = next.nextIndex;
|
|
191
|
+
const value = next.value;
|
|
192
|
+
if (!value?.trim())
|
|
193
|
+
return { ok: false, message: '--header ต้องระบุ KEY=value' };
|
|
194
|
+
try {
|
|
195
|
+
parseKeyValueEntry(value);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { ok: false, message: `--header ต้องใช้รูปแบบ KEY=value: ${value}` };
|
|
199
|
+
}
|
|
200
|
+
headers.push(value);
|
|
201
|
+
}
|
|
202
|
+
else if (a.startsWith('-')) {
|
|
203
|
+
return { ok: false, message: `ไม่รู้จัก option: ${a}` };
|
|
204
|
+
}
|
|
205
|
+
else if (!a.startsWith('-')) {
|
|
206
|
+
positionals.push(a);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const name = positionals[0];
|
|
210
|
+
if (!name) {
|
|
211
|
+
return { ok: false, message: 'ใช้: sanook mcp install <registry-server-name> [--name alias] [--transport auto|remote|stdio] [--env KEY=value] [--header KEY=value] [--project]' };
|
|
212
|
+
}
|
|
213
|
+
if (positionals.length > 1) {
|
|
214
|
+
return { ok: false, message: `ระบุ registry server ได้เพียงชื่อเดียว: ${positionals.slice(1).join(' ')}` };
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, value: { name, alias, transport, version, env, headers, project } };
|
|
217
|
+
}
|
|
218
|
+
export function aliasFromRegistryName(name) {
|
|
219
|
+
const [scope = '', rawLeaf = name] = name.split('/');
|
|
220
|
+
const leaf = rawLeaf
|
|
221
|
+
.replace(/^mcp[-_]?/i, '')
|
|
222
|
+
.replace(/[-_]?mcp[-_]?server$/i, '')
|
|
223
|
+
.replace(/[-_]?server$/i, '')
|
|
224
|
+
.replace(/^smithery[-_]?ai[-_]?/i, '');
|
|
225
|
+
const scopeParts = scope.split('.').filter(Boolean);
|
|
226
|
+
const fallback = scopeParts.length > 1 ? scopeParts[1] : scopeParts[0] || name;
|
|
227
|
+
const candidate = leaf && leaf.toLowerCase() !== 'mcp' ? leaf : fallback;
|
|
228
|
+
const alias = candidate.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
229
|
+
return alias || 'mcp-server';
|
|
230
|
+
}
|
|
231
|
+
export async function searchMcpRegistry(query, options = {}) {
|
|
232
|
+
const params = new URLSearchParams();
|
|
233
|
+
if (query.trim())
|
|
234
|
+
params.set('search', query.trim());
|
|
235
|
+
params.set('limit', String(options.limit ?? 10));
|
|
236
|
+
if (options.cursor)
|
|
237
|
+
params.set('cursor', options.cursor);
|
|
238
|
+
const json = await fetchRegistryJson(`${options.baseUrl ?? MCP_REGISTRY_BASE_URL}/servers?${params}`, options.fetchImpl);
|
|
239
|
+
const entries = Array.isArray(json.servers) ? json.servers : [];
|
|
240
|
+
return {
|
|
241
|
+
servers: latestOnly(entries.map(normalizeRegistryEntry).filter((item) => !!item)),
|
|
242
|
+
nextCursor: typeof json.metadata?.nextCursor === 'string' ? json.metadata.nextCursor : undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export async function getMcpRegistryServer(name, options = {}) {
|
|
246
|
+
const encoded = encodeURIComponent(name);
|
|
247
|
+
const base = options.baseUrl ?? MCP_REGISTRY_BASE_URL;
|
|
248
|
+
if (options.version) {
|
|
249
|
+
const raw = await fetchRegistryJson(`${base}/servers/${encoded}/versions/${encodeURIComponent(options.version)}`, options.fetchImpl);
|
|
250
|
+
return normalizeRegistryEntry(raw);
|
|
251
|
+
}
|
|
252
|
+
const json = await fetchRegistryJson(`${base}/servers/${encoded}/versions`, options.fetchImpl);
|
|
253
|
+
const entries = Array.isArray(json.servers) ? json.servers : [];
|
|
254
|
+
const servers = entries.map(normalizeRegistryEntry).filter((item) => !!item);
|
|
255
|
+
return servers.find((server) => server.isLatest) ?? servers.at(-1);
|
|
256
|
+
}
|
|
257
|
+
export function buildMcpInstallPlan(server, options = {}) {
|
|
258
|
+
const alias = options.alias ?? aliasFromRegistryName(server.name);
|
|
259
|
+
const warnings = [];
|
|
260
|
+
const requirements = [];
|
|
261
|
+
const preferred = options.transport ?? 'auto';
|
|
262
|
+
if (preferred !== 'stdio') {
|
|
263
|
+
const remote = server.remotes.find((item) => item.type === 'streamable-http' && item.url) ?? server.remotes.find((item) => item.url);
|
|
264
|
+
if (remote?.url) {
|
|
265
|
+
const headers = resolveHeaders(remote.headers, options.headers ?? {});
|
|
266
|
+
requirements.push(...headers.requirements);
|
|
267
|
+
if (headers.missing.length)
|
|
268
|
+
return { ok: false, alias, missing: headers.missing, warnings, requirements };
|
|
269
|
+
if (remote.type && remote.type !== 'streamable-http')
|
|
270
|
+
warnings.push(`remote transport เป็น ${remote.type}; Sanook รองรับ Streamable HTTP เป็นหลัก`);
|
|
271
|
+
return { ok: true, alias, config: { url: remote.url, ...(Object.keys(headers.values).length ? { headers: headers.values } : {}) }, source: 'remote', warnings, requirements };
|
|
272
|
+
}
|
|
273
|
+
if (preferred === 'remote') {
|
|
274
|
+
warnings.push('server นี้ไม่มี remote URL ที่ install ได้');
|
|
275
|
+
return { ok: false, alias, missing: [], warnings, requirements };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const pkg = choosePackage(server.packages);
|
|
279
|
+
if (!pkg)
|
|
280
|
+
return { ok: false, alias, missing: [], warnings: [...warnings, 'ไม่พบ package/remote ที่ Sanook install อัตโนมัติได้'], requirements };
|
|
281
|
+
const env = resolveEnv(pkg.environmentVariables ?? [], options.env ?? {});
|
|
282
|
+
requirements.push(...env.requirements);
|
|
283
|
+
if (env.missing.length)
|
|
284
|
+
return { ok: false, alias, missing: env.missing, warnings, requirements };
|
|
285
|
+
const commandArgs = packageCommand(pkg);
|
|
286
|
+
if (!commandArgs)
|
|
287
|
+
return { ok: false, alias, missing: [], warnings: [...warnings, `ยังไม่รองรับ package runtime: ${pkg.runtimeHint ?? pkg.registryType ?? '(unknown)'}`], requirements };
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
alias,
|
|
291
|
+
config: { command: commandArgs.command, args: commandArgs.args, ...(Object.keys(env.values).length ? { env: env.values } : {}) },
|
|
292
|
+
source: 'package',
|
|
293
|
+
warnings,
|
|
294
|
+
requirements,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export function formatRegistrySearch(result) {
|
|
298
|
+
const lines = ['MCP registry search'];
|
|
299
|
+
if (!result.servers.length)
|
|
300
|
+
return `${lines[0]}\n(no matches)`;
|
|
301
|
+
for (const server of result.servers) {
|
|
302
|
+
lines.push(`${server.name}${server.version ? `@${server.version}` : ''} — ${server.description ?? '(no description)'}`);
|
|
303
|
+
lines.push(` transport: ${transportSummary(server)} · risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}${server.repositoryUrl ? ` · repo: ${server.repositoryUrl}` : ''}`);
|
|
304
|
+
}
|
|
305
|
+
if (result.nextCursor)
|
|
306
|
+
lines.push(`next: --cursor ${result.nextCursor}`);
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
309
|
+
export function formatRegistryInfo(server) {
|
|
310
|
+
const lines = [`${server.name}${server.version ? `@${server.version}` : ''}`, server.description ?? '(no description)'];
|
|
311
|
+
if (server.repositoryUrl)
|
|
312
|
+
lines.push(`repo: ${server.repositoryUrl}`);
|
|
313
|
+
if (server.websiteUrl)
|
|
314
|
+
lines.push(`website: ${server.websiteUrl}`);
|
|
315
|
+
lines.push(`transport: ${transportSummary(server)}`);
|
|
316
|
+
lines.push(`risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}`);
|
|
317
|
+
if (server.remotes.length) {
|
|
318
|
+
lines.push('remotes:');
|
|
319
|
+
for (const remote of server.remotes) {
|
|
320
|
+
lines.push(` - ${remote.type ?? 'remote'} ${remote.url ?? '(missing url)'}`);
|
|
321
|
+
for (const req of inputSummaries(inputArray(remote.headers)))
|
|
322
|
+
lines.push(` ${req}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (server.packages.length) {
|
|
326
|
+
lines.push('packages:');
|
|
327
|
+
for (const pkg of server.packages) {
|
|
328
|
+
lines.push(` - ${pkg.registryType ?? 'pkg'} ${pkg.identifier ?? '(missing identifier)'}${pkg.version ? `@${pkg.version}` : ''}${pkg.runtimeHint ? ` via ${pkg.runtimeHint}` : ''}`);
|
|
329
|
+
for (const req of inputSummaries(pkg.environmentVariables ?? []))
|
|
330
|
+
lines.push(` ${req}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
lines.push(`install: sanook mcp install ${server.name} --name ${aliasFromRegistryName(server.name)}`);
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
}
|
|
336
|
+
export function formatPreset(name) {
|
|
337
|
+
if (!name) {
|
|
338
|
+
return ['MCP presets', ...MCP_PRESETS.map((preset) => ` ${preset.name.padEnd(8)} ${preset.description}`)].join('\n');
|
|
339
|
+
}
|
|
340
|
+
const preset = MCP_PRESETS.find((item) => item.name === name);
|
|
341
|
+
if (!preset)
|
|
342
|
+
return `ไม่เจอ preset: ${name}\nมีให้เลือก: ${MCP_PRESETS.map((item) => item.name).join(', ')}`;
|
|
343
|
+
return [
|
|
344
|
+
`MCP preset: ${preset.name}`,
|
|
345
|
+
preset.description,
|
|
346
|
+
'',
|
|
347
|
+
...preset.servers.map((server) => `- ${server}\n sanook mcp info ${server}\n sanook mcp install ${server} --name ${aliasFromRegistryName(server)}`),
|
|
348
|
+
].join('\n');
|
|
349
|
+
}
|
|
350
|
+
function normalizeRegistryEntry(raw) {
|
|
351
|
+
const server = raw.server;
|
|
352
|
+
if (!server?.name)
|
|
353
|
+
return undefined;
|
|
354
|
+
return {
|
|
355
|
+
name: server.name,
|
|
356
|
+
title: server.title,
|
|
357
|
+
description: server.description,
|
|
358
|
+
version: server.version,
|
|
359
|
+
repositoryUrl: server.repository?.url,
|
|
360
|
+
websiteUrl: server.websiteUrl,
|
|
361
|
+
isLatest: raw._meta?.['io.modelcontextprotocol.registry/official']?.isLatest !== false,
|
|
362
|
+
remotes: Array.isArray(server.remotes) ? server.remotes : [],
|
|
363
|
+
packages: Array.isArray(server.packages) ? server.packages : [],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function latestOnly(servers) {
|
|
367
|
+
const out = new Map();
|
|
368
|
+
for (const server of servers) {
|
|
369
|
+
const current = out.get(server.name);
|
|
370
|
+
if (!current || server.isLatest)
|
|
371
|
+
out.set(server.name, server);
|
|
372
|
+
}
|
|
373
|
+
return [...out.values()];
|
|
374
|
+
}
|
|
375
|
+
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
376
|
+
const cached = readRegistryCache(url);
|
|
377
|
+
if (cached)
|
|
378
|
+
return cached;
|
|
379
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
380
|
+
if (!res.ok)
|
|
381
|
+
throw new Error(`registry ${res.status} ${res.statusText}`);
|
|
382
|
+
const json = (await res.json());
|
|
383
|
+
writeRegistryCache(url, json);
|
|
384
|
+
return json;
|
|
385
|
+
}
|
|
386
|
+
function transportSummary(server) {
|
|
387
|
+
const transports = [
|
|
388
|
+
...server.remotes.map((remote) => `remote:${remote.type ?? 'unknown'}`),
|
|
389
|
+
...server.packages.map((pkg) => `${pkg.registryType ?? 'pkg'}:${pkg.runtimeHint ?? 'package'}`),
|
|
390
|
+
];
|
|
391
|
+
return transports.length ? transports.join(', ') : 'none listed';
|
|
392
|
+
}
|
|
393
|
+
function inputArray(value) {
|
|
394
|
+
if (!value)
|
|
395
|
+
return [];
|
|
396
|
+
if (Array.isArray(value))
|
|
397
|
+
return value;
|
|
398
|
+
return Object.entries(value).map(([name, input]) => ({ name, value: String(input) }));
|
|
399
|
+
}
|
|
400
|
+
function inputSummaries(inputs) {
|
|
401
|
+
return inputs.map((input) => {
|
|
402
|
+
const name = input.name ?? '(positional)';
|
|
403
|
+
const flags = [input.isRequired ? 'required' : undefined, input.isSecret ? 'secret' : undefined].filter(Boolean).join(', ');
|
|
404
|
+
return `${name}${flags ? ` (${flags})` : ''}${input.description ? ` — ${input.description}` : ''}`;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function resolveHeaders(raw, provided) {
|
|
408
|
+
const values = {};
|
|
409
|
+
const missing = [];
|
|
410
|
+
const requirements = [];
|
|
411
|
+
for (const input of inputArray(raw)) {
|
|
412
|
+
const name = input.name;
|
|
413
|
+
if (!name)
|
|
414
|
+
continue;
|
|
415
|
+
requirements.push(`header ${name}${input.isSecret ? ' (secret)' : ''}`);
|
|
416
|
+
const explicit = provided[name];
|
|
417
|
+
if (explicit != null)
|
|
418
|
+
values[name] = explicit;
|
|
419
|
+
else if (input.value && !/\{[^}]+\}/.test(input.value))
|
|
420
|
+
values[name] = input.value;
|
|
421
|
+
else if (input.default != null)
|
|
422
|
+
values[name] = input.default;
|
|
423
|
+
else if (input.isRequired || input.value)
|
|
424
|
+
missing.push(`header:${name}`);
|
|
425
|
+
}
|
|
426
|
+
for (const [name, value] of Object.entries(provided))
|
|
427
|
+
values[name] = value;
|
|
428
|
+
return { values, missing, requirements };
|
|
429
|
+
}
|
|
430
|
+
function resolveEnv(inputs, provided) {
|
|
431
|
+
const values = {};
|
|
432
|
+
const missing = [];
|
|
433
|
+
const requirements = [];
|
|
434
|
+
for (const input of inputs) {
|
|
435
|
+
const name = input.name;
|
|
436
|
+
if (!name)
|
|
437
|
+
continue;
|
|
438
|
+
requirements.push(`env ${name}${input.isSecret ? ' (secret)' : ''}`);
|
|
439
|
+
const explicit = provided[name];
|
|
440
|
+
if (explicit != null)
|
|
441
|
+
values[name] = explicit;
|
|
442
|
+
else if (input.value && !/\{[^}]+\}/.test(input.value))
|
|
443
|
+
values[name] = input.value;
|
|
444
|
+
else if (input.default != null)
|
|
445
|
+
values[name] = input.default;
|
|
446
|
+
else if (input.isRequired)
|
|
447
|
+
missing.push(`env:${name}`);
|
|
448
|
+
}
|
|
449
|
+
for (const [name, value] of Object.entries(provided))
|
|
450
|
+
values[name] = value;
|
|
451
|
+
return { values, missing, requirements };
|
|
452
|
+
}
|
|
453
|
+
function choosePackage(packages) {
|
|
454
|
+
return (packages.find((pkg) => pkg.transport?.type === 'stdio' && (pkg.runtimeHint === 'npx' || pkg.registryType === 'npm')) ??
|
|
455
|
+
packages.find((pkg) => pkg.transport?.type === 'stdio' && pkg.runtimeHint === 'uvx') ??
|
|
456
|
+
packages.find((pkg) => pkg.transport?.type === 'stdio' && pkg.runtimeHint === 'docker') ??
|
|
457
|
+
packages.find((pkg) => pkg.transport?.type === 'stdio'));
|
|
458
|
+
}
|
|
459
|
+
function packageCommand(pkg) {
|
|
460
|
+
const identifier = pkg.identifier;
|
|
461
|
+
if (!identifier)
|
|
462
|
+
return undefined;
|
|
463
|
+
const runtime = pkg.runtimeHint ?? (pkg.registryType === 'npm' ? 'npx' : pkg.registryType === 'pypi' ? 'uvx' : undefined);
|
|
464
|
+
const pkgId = packageIdentifierWithVersion(identifier, pkg.version);
|
|
465
|
+
const runtimeArgs = materializeArgs(pkg.runtimeArguments ?? []);
|
|
466
|
+
const packageArgs = materializeArgs(pkg.packageArguments ?? []);
|
|
467
|
+
if (runtime === 'npx') {
|
|
468
|
+
const args = runtimeArgs.length ? runtimeArgs : ['-y'];
|
|
469
|
+
return { command: 'npx', args: [...args, pkgId, ...packageArgs] };
|
|
470
|
+
}
|
|
471
|
+
if (runtime === 'uvx')
|
|
472
|
+
return { command: 'uvx', args: [...runtimeArgs, pkgId, ...packageArgs] };
|
|
473
|
+
if (runtime === 'docker')
|
|
474
|
+
return { command: 'docker', args: ['run', '-i', '--rm', ...runtimeArgs, pkgId, ...packageArgs] };
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
function packageIdentifierWithVersion(identifier, version) {
|
|
478
|
+
if (!version)
|
|
479
|
+
return identifier;
|
|
480
|
+
const scopedPackageNameEnd = identifier.startsWith('@') ? identifier.indexOf('/', 1) + 1 : 0;
|
|
481
|
+
return identifier.indexOf('@', scopedPackageNameEnd) === -1 ? `${identifier}@${version}` : identifier;
|
|
482
|
+
}
|
|
483
|
+
function materializeArgs(args) {
|
|
484
|
+
const out = [];
|
|
485
|
+
for (const arg of args) {
|
|
486
|
+
const value = arg.value ?? arg.default;
|
|
487
|
+
if (!value || /\{[^}]+\}/.test(value))
|
|
488
|
+
continue;
|
|
489
|
+
if (arg.name)
|
|
490
|
+
out.push(arg.name);
|
|
491
|
+
out.push(value);
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
package/dist/mcp-risk.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const RISK_PRIORITY = {
|
|
2
|
+
'read-only': 0,
|
|
3
|
+
'network-write': 1,
|
|
4
|
+
'file-write': 2,
|
|
5
|
+
'database-write': 3,
|
|
6
|
+
'infra/admin': 4,
|
|
7
|
+
};
|
|
8
|
+
const WRITE_TOOL = /\b(write|create|update|delete|insert|drop|push|post|send|execute|deploy|apply|modify|edit|remove|destroy|mutate|run_|upload|patch|merge|commit|publish|trigger|invoke|call|set_)\b/i;
|
|
9
|
+
const READ_ONLY_TEXT = /\b(read[-_ ]?only|readonly|list|get|search|fetch|query|inspect|view|describe|lookup|recall)\b/i;
|
|
10
|
+
const FILE_WRITE_TEXT = /\b(file|filesystem|fs[-_ ]?server|write_file|edit_file|directory)\b/i;
|
|
11
|
+
const DB_WRITE_TEXT = /\b(postgres|postgresql|mysql|sqlite|mongodb|redis|database|sql|db[-_ ]?write)\b/i;
|
|
12
|
+
const NETWORK_WRITE_TEXT = /\b(github|gitlab|slack|discord|linear|jira|notion|fetch|search|browser|playwright|http|web|api|issue|pull|release|message|chat|email|gmail|drive|obsidian|tavily|brave)\b/i;
|
|
13
|
+
const INFRA_TEXT = /\b(docker|kubernetes|k8s|helm|terraform|aws|gcp|azure|infra|container|cluster|pod|deployment|kubectl)\b/i;
|
|
14
|
+
function maxRisk(...labels) {
|
|
15
|
+
return labels.reduce((best, label) => (RISK_PRIORITY[label] > RISK_PRIORITY[best] ? label : best), 'read-only');
|
|
16
|
+
}
|
|
17
|
+
function riskFromText(text) {
|
|
18
|
+
const haystack = text.toLowerCase();
|
|
19
|
+
if (INFRA_TEXT.test(haystack))
|
|
20
|
+
return 'infra/admin';
|
|
21
|
+
if (DB_WRITE_TEXT.test(haystack))
|
|
22
|
+
return READ_ONLY_TEXT.test(haystack) ? 'read-only' : 'database-write';
|
|
23
|
+
if (FILE_WRITE_TEXT.test(haystack))
|
|
24
|
+
return 'file-write';
|
|
25
|
+
if (NETWORK_WRITE_TEXT.test(haystack))
|
|
26
|
+
return READ_ONLY_TEXT.test(haystack) ? 'read-only' : 'network-write';
|
|
27
|
+
if (READ_ONLY_TEXT.test(haystack))
|
|
28
|
+
return 'read-only';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function riskFromTools(tools) {
|
|
32
|
+
const labels = [];
|
|
33
|
+
for (const tool of tools) {
|
|
34
|
+
const text = `${tool.name} ${tool.description ?? ''}`;
|
|
35
|
+
const base = riskFromText(text);
|
|
36
|
+
if (base)
|
|
37
|
+
labels.push(base);
|
|
38
|
+
if (WRITE_TOOL.test(text) && base !== 'read-only') {
|
|
39
|
+
if (DB_WRITE_TEXT.test(text))
|
|
40
|
+
labels.push('database-write');
|
|
41
|
+
else if (FILE_WRITE_TEXT.test(text))
|
|
42
|
+
labels.push('file-write');
|
|
43
|
+
else if (INFRA_TEXT.test(text))
|
|
44
|
+
labels.push('infra/admin');
|
|
45
|
+
else
|
|
46
|
+
labels.push('network-write');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return labels.length ? maxRisk(...labels) : undefined;
|
|
50
|
+
}
|
|
51
|
+
export function inferRegistryServerRisk(server) {
|
|
52
|
+
const parts = [
|
|
53
|
+
server.name,
|
|
54
|
+
server.title,
|
|
55
|
+
server.description,
|
|
56
|
+
...server.packages.map((pkg) => `${pkg.registryType ?? ''} ${pkg.identifier ?? ''} ${pkg.runtimeHint ?? ''}`),
|
|
57
|
+
...server.remotes.map((remote) => `${remote.type ?? ''} ${remote.url ?? ''}`),
|
|
58
|
+
].filter((part) => Boolean(part));
|
|
59
|
+
const labels = parts.map((part) => riskFromText(part)).filter((label) => !!label);
|
|
60
|
+
return labels.length ? maxRisk(...labels) : 'read-only';
|
|
61
|
+
}
|
|
62
|
+
export function inferConfiguredServerRisk(name, cfg, tools = []) {
|
|
63
|
+
const commandLine = [cfg.command, ...(cfg.args ?? []), cfg.url].filter(Boolean).join(' ');
|
|
64
|
+
const labels = [riskFromText(name), riskFromText(commandLine), riskFromTools(tools)].filter((label) => !!label);
|
|
65
|
+
if (cfg.url)
|
|
66
|
+
labels.push('network-write');
|
|
67
|
+
return labels.length ? maxRisk(...labels) : 'read-only';
|
|
68
|
+
}
|
|
69
|
+
export function formatMcpRiskLabel(label) {
|
|
70
|
+
return label;
|
|
71
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -130,7 +130,7 @@ async function callTool(name, args) {
|
|
|
130
130
|
const r = await reindex();
|
|
131
131
|
resetSearchCaches();
|
|
132
132
|
return (`indexed: +${r.added} ~${r.updated} -${r.removed} (skipped ${r.skipped}) · ` +
|
|
133
|
-
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills} · vault=${r.vaultPath ?? '(none)'}`);
|
|
133
|
+
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills} vectors=${r.vectors} · vault=${r.vaultPath ?? '(none)'}`);
|
|
134
134
|
}
|
|
135
135
|
case 'sanook_stats': {
|
|
136
136
|
const { index } = await loadIndex();
|