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,97 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
export const TOOL_TRAIL_LIMIT = 6;
|
|
3
|
+
function clip(text, width) {
|
|
4
|
+
if (width <= 0)
|
|
5
|
+
return '';
|
|
6
|
+
return text.length > width ? `${text.slice(0, Math.max(0, width - 3))}...` : text;
|
|
7
|
+
}
|
|
8
|
+
function normalizeWhitespace(text) {
|
|
9
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
10
|
+
}
|
|
11
|
+
export function compactToolDetail(detail, width = 64) {
|
|
12
|
+
if (detail === undefined || detail === null)
|
|
13
|
+
return '';
|
|
14
|
+
const text = typeof detail === 'string'
|
|
15
|
+
? detail
|
|
16
|
+
: (() => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(detail);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return inspect(detail, { breakLength: Infinity, depth: 2 });
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
return clip(normalizeWhitespace(text), width);
|
|
25
|
+
}
|
|
26
|
+
function trimItems(items) {
|
|
27
|
+
return items.slice(Math.max(0, items.length - TOOL_TRAIL_LIMIT));
|
|
28
|
+
}
|
|
29
|
+
function latestRunningToolIndex(items, tool) {
|
|
30
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
31
|
+
const item = items[i];
|
|
32
|
+
if (item.status !== 'running')
|
|
33
|
+
continue;
|
|
34
|
+
if (!tool || item.name === tool)
|
|
35
|
+
return i;
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
export function updateToolTrailOnEvent(items, event, nextId) {
|
|
40
|
+
if (event.type === 'tool-call') {
|
|
41
|
+
const name = event.tool?.trim() || 'tool';
|
|
42
|
+
return {
|
|
43
|
+
items: trimItems([...items, { detail: compactToolDetail(event.detail), id: nextId, name, status: 'running' }]),
|
|
44
|
+
nextId: nextId + 1,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (event.type === 'tool-result') {
|
|
48
|
+
const index = latestRunningToolIndex(items, event.tool);
|
|
49
|
+
const detail = compactToolDetail(event.detail);
|
|
50
|
+
if (index === -1) {
|
|
51
|
+
const name = event.tool?.trim() || 'tool';
|
|
52
|
+
return {
|
|
53
|
+
items: trimItems([...items, { detail, id: nextId, name, status: 'done' }]),
|
|
54
|
+
nextId: nextId + 1,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const next = [...items];
|
|
58
|
+
next[index] = { ...next[index], detail: detail || next[index].detail, status: 'done' };
|
|
59
|
+
return { items: trimItems(next), nextId };
|
|
60
|
+
}
|
|
61
|
+
const index = latestRunningToolIndex(items);
|
|
62
|
+
const detail = compactToolDetail(event.detail ?? event.text);
|
|
63
|
+
if (index === -1)
|
|
64
|
+
return { items, nextId };
|
|
65
|
+
const next = [...items];
|
|
66
|
+
next[index] = { ...next[index], detail: detail || next[index].detail, status: 'error' };
|
|
67
|
+
return { items: trimItems(next), nextId };
|
|
68
|
+
}
|
|
69
|
+
function markerForStatus(status) {
|
|
70
|
+
return status === 'running' ? '>' : status === 'done' ? '+' : '!';
|
|
71
|
+
}
|
|
72
|
+
function statusSummary(items) {
|
|
73
|
+
const running = items.filter((item) => item.status === 'running').length;
|
|
74
|
+
const done = items.filter((item) => item.status === 'done').length;
|
|
75
|
+
const error = items.filter((item) => item.status === 'error').length;
|
|
76
|
+
return [`${done} done`, running ? `${running} running` : '', error ? `${error} error` : ''].filter(Boolean).join(' / ');
|
|
77
|
+
}
|
|
78
|
+
export function toolTrailLines(items, columns, mode = 'expanded') {
|
|
79
|
+
if (mode === 'hidden')
|
|
80
|
+
return [];
|
|
81
|
+
if (!items.length)
|
|
82
|
+
return [];
|
|
83
|
+
const width = Math.max(24, Math.min(Math.max(30, columns - 4), 96));
|
|
84
|
+
const nameWidth = Math.max(8, Math.min(24, Math.floor(width * 0.34)));
|
|
85
|
+
const detailWidth = Math.max(0, width - nameWidth - 18);
|
|
86
|
+
const lines = [`Sanook tool trail (${items.length})`, `view: ${mode} | ${statusSummary(items)} | Ctrl+T / /trail`];
|
|
87
|
+
if (mode === 'compact') {
|
|
88
|
+
lines.push(`tools: ${items.map((item) => `${markerForStatus(item.status)}${item.name}`).join(' ')}`);
|
|
89
|
+
return lines.map((line) => clip(line, width));
|
|
90
|
+
}
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
const marker = markerForStatus(item.status);
|
|
93
|
+
const detail = item.detail ? ` ${clip(item.detail, detailWidth)}` : '';
|
|
94
|
+
lines.push(`${marker} ${clip(item.name, nameWidth).padEnd(nameWidth)} ${item.status.padEnd(7)}${detail}`);
|
|
95
|
+
}
|
|
96
|
+
return lines.map((line) => clip(line, width));
|
|
97
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const DEFAULT_TRANSCRIPT_WINDOW = 30;
|
|
2
|
+
/** Window into conversation turns — scrollFromBottom=0 pins to the latest messages. */
|
|
3
|
+
export function getTranscriptWindow(totalLength, windowSize, scrollFromBottom = 0) {
|
|
4
|
+
if (totalLength <= 0) {
|
|
5
|
+
return { end: 0, scrollFromBottom: 0, showNewer: false, showOlder: false, start: 0 };
|
|
6
|
+
}
|
|
7
|
+
const size = Math.max(1, Math.min(windowSize, totalLength));
|
|
8
|
+
const maxScroll = Math.max(0, totalLength - size);
|
|
9
|
+
const scroll = Math.max(0, Math.min(scrollFromBottom, maxScroll));
|
|
10
|
+
const end = totalLength - scroll;
|
|
11
|
+
const start = Math.max(0, end - size);
|
|
12
|
+
return {
|
|
13
|
+
end,
|
|
14
|
+
scrollFromBottom: scroll,
|
|
15
|
+
showNewer: scroll > 0,
|
|
16
|
+
showOlder: start > 0,
|
|
17
|
+
start,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function transcriptWindowSize(rows, min = 8, max = 40) {
|
|
21
|
+
const terminalRows = rows ?? 24;
|
|
22
|
+
return Math.max(min, Math.min(max, terminalRows - 12));
|
|
23
|
+
}
|
|
24
|
+
export function transcriptScrollStep(windowSize) {
|
|
25
|
+
return Math.max(3, Math.floor(windowSize / 2));
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
export function useBusyElapsedSeconds(busy) {
|
|
3
|
+
const [startedAt, setStartedAt] = useState(null);
|
|
4
|
+
const [now, setNow] = useState(() => Date.now());
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!busy) {
|
|
7
|
+
setStartedAt(null);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
setStartedAt(started);
|
|
12
|
+
setNow(started);
|
|
13
|
+
const interval = setInterval(() => setNow(Date.now()), 1_000);
|
|
14
|
+
return () => clearInterval(interval);
|
|
15
|
+
}, [busy]);
|
|
16
|
+
if (startedAt == null)
|
|
17
|
+
return undefined;
|
|
18
|
+
return Math.max(0, Math.floor((now - startedAt) / 1_000));
|
|
19
|
+
}
|
package/dist/ui/useEditor.js
CHANGED
|
@@ -1,15 +1,148 @@
|
|
|
1
1
|
import { useState, useRef } from 'react';
|
|
2
|
+
export const PASTE_COLLAPSE_LINES = 5;
|
|
3
|
+
export const PASTE_COLLAPSE_CHARS = 2000;
|
|
4
|
+
export const PASTE_SNIPPET_RE = /\[\[ paste [^\n]*?\]\]/g;
|
|
5
|
+
const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
6
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
7
|
+
: null;
|
|
8
|
+
const BRACKETED_PASTE_RE = /\x1b?\[20[01]~/g;
|
|
9
|
+
const BRACKETED_PASTE_TEST_RE = /\x1b?\[20[01]~/;
|
|
10
|
+
const COMPACT_NUMBER = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, notation: 'compact' });
|
|
11
|
+
function compactNumber(n) {
|
|
12
|
+
return COMPACT_NUMBER.format(n).replace(/[KMBT]$/, (suffix) => suffix.toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
function oneLinePreview(text) {
|
|
15
|
+
return text.replace(/\s+/g, ' ').trim().replace(/\]\]/g, '] ]');
|
|
16
|
+
}
|
|
17
|
+
export function stripTrailingPasteNewlines(text) {
|
|
18
|
+
return /[^\n]/.test(text) ? text.replace(/\n+$/, '') : text;
|
|
19
|
+
}
|
|
20
|
+
export function normalizePastedInput(input) {
|
|
21
|
+
return stripTrailingPasteNewlines(input.replace(BRACKETED_PASTE_RE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
22
|
+
}
|
|
23
|
+
export function isPasteLikeInput(input) {
|
|
24
|
+
return BRACKETED_PASTE_TEST_RE.test(input) || input.includes('\n');
|
|
25
|
+
}
|
|
26
|
+
export function pasteTokenLabel(text, lineCount) {
|
|
27
|
+
const preview = oneLinePreview(text);
|
|
28
|
+
const count = `${compactNumber(lineCount)} lines`;
|
|
29
|
+
if (!preview)
|
|
30
|
+
return `[[ paste ${count} ]]`;
|
|
31
|
+
const headWidth = 20;
|
|
32
|
+
const tailWidth = 28;
|
|
33
|
+
const body = preview.length <= headWidth + tailWidth + 5
|
|
34
|
+
? preview
|
|
35
|
+
: `${preview.slice(0, headWidth).trimEnd()}.. ${preview.slice(-tailWidth).trimStart()}`;
|
|
36
|
+
return `[[ paste ${count} · ${body} ]]`;
|
|
37
|
+
}
|
|
38
|
+
function insertToken(value, cursor, token) {
|
|
39
|
+
const lead = cursor > 0 && !/\s/.test(value[cursor - 1] ?? '') ? ' ' : '';
|
|
40
|
+
const tail = cursor < value.length && !/\s/.test(value[cursor] ?? '') ? ' ' : '';
|
|
41
|
+
const insert = `${lead}${token}${tail}`;
|
|
42
|
+
return { cursor: cursor + insert.length, value: value.slice(0, cursor) + insert + value.slice(cursor) };
|
|
43
|
+
}
|
|
44
|
+
export function trimPasteSnips(snips) {
|
|
45
|
+
const maxCount = 32;
|
|
46
|
+
const maxChars = 4 * 1024 * 1024;
|
|
47
|
+
const out = [];
|
|
48
|
+
let chars = 0;
|
|
49
|
+
for (let i = snips.length - 1; i >= 0; i -= 1) {
|
|
50
|
+
const snip = snips[i];
|
|
51
|
+
if (out.length >= maxCount || chars + snip.text.length > maxChars)
|
|
52
|
+
break;
|
|
53
|
+
chars += snip.text.length;
|
|
54
|
+
out.unshift(snip);
|
|
55
|
+
}
|
|
56
|
+
return out.length === snips.length ? snips : out;
|
|
57
|
+
}
|
|
58
|
+
export function applyPasteInput(value, cursor, input, pasteSnips) {
|
|
59
|
+
const text = normalizePastedInput(input);
|
|
60
|
+
if (!text)
|
|
61
|
+
return { cursor, pasteSnips, value };
|
|
62
|
+
const lineCount = text.split('\n').length;
|
|
63
|
+
const shouldCollapse = lineCount >= PASTE_COLLAPSE_LINES || text.length >= PASTE_COLLAPSE_CHARS;
|
|
64
|
+
if (!shouldCollapse) {
|
|
65
|
+
return {
|
|
66
|
+
cursor: cursor + text.length,
|
|
67
|
+
pasteSnips,
|
|
68
|
+
value: value.slice(0, cursor) + text + value.slice(cursor),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const label = pasteTokenLabel(text, lineCount);
|
|
72
|
+
const inserted = insertToken(value, cursor, label);
|
|
73
|
+
return {
|
|
74
|
+
...inserted,
|
|
75
|
+
pasteSnips: trimPasteSnips([...pasteSnips, { label, text }]),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function expandPasteSnippets(value, pasteSnips) {
|
|
79
|
+
const byLabel = new Map();
|
|
80
|
+
for (const { label, text } of pasteSnips) {
|
|
81
|
+
const matches = byLabel.get(label);
|
|
82
|
+
if (matches)
|
|
83
|
+
matches.push(text);
|
|
84
|
+
else
|
|
85
|
+
byLabel.set(label, [text]);
|
|
86
|
+
}
|
|
87
|
+
return value.replace(PASTE_SNIPPET_RE, (token) => byLabel.get(token)?.shift() ?? token);
|
|
88
|
+
}
|
|
89
|
+
export function graphemeBoundaries(text) {
|
|
90
|
+
const boundaries = [0];
|
|
91
|
+
if (segmenter) {
|
|
92
|
+
for (const segment of segmenter.segment(text))
|
|
93
|
+
boundaries.push(segment.index + segment.segment.length);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
let index = 0;
|
|
97
|
+
for (const point of Array.from(text)) {
|
|
98
|
+
index += point.length;
|
|
99
|
+
boundaries.push(index);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return boundaries.at(-1) === text.length ? boundaries : [...boundaries, text.length];
|
|
103
|
+
}
|
|
104
|
+
export function clampCursorToGrapheme(text, cursor) {
|
|
105
|
+
const target = Math.max(0, Math.min(cursor, text.length));
|
|
106
|
+
const boundaries = graphemeBoundaries(text);
|
|
107
|
+
let best = 0;
|
|
108
|
+
for (const boundary of boundaries) {
|
|
109
|
+
if (boundary > target)
|
|
110
|
+
break;
|
|
111
|
+
best = boundary;
|
|
112
|
+
}
|
|
113
|
+
return best;
|
|
114
|
+
}
|
|
115
|
+
export function previousGraphemeCursor(text, cursor) {
|
|
116
|
+
const target = clampCursorToGrapheme(text, cursor);
|
|
117
|
+
let previous = 0;
|
|
118
|
+
for (const boundary of graphemeBoundaries(text)) {
|
|
119
|
+
if (boundary >= target)
|
|
120
|
+
return previous;
|
|
121
|
+
previous = boundary;
|
|
122
|
+
}
|
|
123
|
+
return previous;
|
|
124
|
+
}
|
|
125
|
+
export function nextGraphemeCursor(text, cursor) {
|
|
126
|
+
const target = clampCursorToGrapheme(text, cursor);
|
|
127
|
+
for (const boundary of graphemeBoundaries(text)) {
|
|
128
|
+
if (boundary > target)
|
|
129
|
+
return boundary;
|
|
130
|
+
}
|
|
131
|
+
return text.length;
|
|
132
|
+
}
|
|
2
133
|
export function useEditor(history) {
|
|
3
134
|
const [value, setValue] = useState('');
|
|
4
135
|
const [cursor, setCursor] = useState(0);
|
|
136
|
+
const [pasteSnips, setPasteSnips] = useState([]);
|
|
5
137
|
const histIndex = useRef(null); // null = กำลังแก้ draft (ไม่ได้อยู่ในประวัติ)
|
|
6
138
|
const draft = useRef('');
|
|
7
139
|
const set = (v, c = v.length) => {
|
|
8
140
|
setValue(v);
|
|
9
|
-
setCursor(
|
|
141
|
+
setCursor(clampCursorToGrapheme(v, c));
|
|
10
142
|
};
|
|
11
143
|
const reset = () => {
|
|
12
144
|
histIndex.current = null;
|
|
145
|
+
setPasteSnips([]);
|
|
13
146
|
set('');
|
|
14
147
|
};
|
|
15
148
|
const insert = (s) => set(value.slice(0, cursor) + s + value.slice(cursor), cursor + s.length);
|
|
@@ -51,9 +184,9 @@ export function useEditor(history) {
|
|
|
51
184
|
if (key.downArrow)
|
|
52
185
|
return historyNext(), 'handled';
|
|
53
186
|
if (key.leftArrow)
|
|
54
|
-
return setCursor(
|
|
187
|
+
return setCursor(previousGraphemeCursor(value, cursor)), 'handled';
|
|
55
188
|
if (key.rightArrow)
|
|
56
|
-
return setCursor(
|
|
189
|
+
return setCursor(nextGraphemeCursor(value, cursor)), 'handled';
|
|
57
190
|
if (key.ctrl) {
|
|
58
191
|
switch (input) {
|
|
59
192
|
case 'a': return setCursor(0), 'handled';
|
|
@@ -71,13 +204,19 @@ export function useEditor(history) {
|
|
|
71
204
|
if (key.backspace || key.delete) {
|
|
72
205
|
if (cursor === 0)
|
|
73
206
|
return 'handled';
|
|
74
|
-
|
|
207
|
+
const previous = previousGraphemeCursor(value, cursor);
|
|
208
|
+
return set(value.slice(0, previous) + value.slice(cursor), previous), 'handled';
|
|
75
209
|
}
|
|
76
210
|
if (input && !key.meta) {
|
|
77
211
|
histIndex.current = null; // เริ่มพิมพ์ = ออกจากโหมดดูประวัติ
|
|
212
|
+
if (isPasteLikeInput(input)) {
|
|
213
|
+
const pasted = applyPasteInput(value, cursor, input, pasteSnips);
|
|
214
|
+
setPasteSnips(pasted.pasteSnips);
|
|
215
|
+
return set(pasted.value, pasted.cursor), 'handled';
|
|
216
|
+
}
|
|
78
217
|
return insert(input), 'handled';
|
|
79
218
|
}
|
|
80
219
|
return 'none';
|
|
81
220
|
};
|
|
82
|
-
return { value, cursor, setValue: (v) => set(v), reset, handleKey };
|
|
221
|
+
return { value, cursor, pasteSnips, expandValue: (v = value) => expandPasteSnippets(v, pasteSnips), setValue: (v) => set(v), reset, handleKey };
|
|
83
222
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
const execFileP = promisify(execFile);
|
|
5
|
+
const BRANCH_CACHE_MS = 15_000;
|
|
6
|
+
const BRANCH_TIMEOUT_MS = 700;
|
|
7
|
+
const cache = new Map();
|
|
8
|
+
const inflight = new Map();
|
|
9
|
+
export async function resolveGitBranch(cwd) {
|
|
10
|
+
try {
|
|
11
|
+
const { stdout } = await execFileP('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
12
|
+
timeout: BRANCH_TIMEOUT_MS,
|
|
13
|
+
});
|
|
14
|
+
const branch = stdout.trim();
|
|
15
|
+
return branch && branch !== 'HEAD' ? branch : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function loadCachedGitBranch(cwd) {
|
|
22
|
+
const active = inflight.get(cwd);
|
|
23
|
+
if (active)
|
|
24
|
+
return active;
|
|
25
|
+
const load = resolveGitBranch(cwd).finally(() => inflight.delete(cwd));
|
|
26
|
+
inflight.set(cwd, load);
|
|
27
|
+
return load;
|
|
28
|
+
}
|
|
29
|
+
export function useGitBranch(cwd) {
|
|
30
|
+
const [branch, setBranch] = useState(() => cache.get(cwd)?.branch ?? null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
const refresh = async () => {
|
|
34
|
+
const cached = cache.get(cwd);
|
|
35
|
+
if (cached && Date.now() - cached.at < BRANCH_CACHE_MS) {
|
|
36
|
+
if (!cancelled)
|
|
37
|
+
setBranch(cached.branch);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const branch = await loadCachedGitBranch(cwd);
|
|
41
|
+
cache.set(cwd, { at: Date.now(), branch });
|
|
42
|
+
if (!cancelled)
|
|
43
|
+
setBranch(branch);
|
|
44
|
+
};
|
|
45
|
+
void refresh();
|
|
46
|
+
const interval = setInterval(() => void refresh(), BRANCH_CACHE_MS);
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
};
|
|
51
|
+
}, [cwd]);
|
|
52
|
+
return branch;
|
|
53
|
+
}
|
|
54
|
+
export function clearGitBranchCacheForTests() {
|
|
55
|
+
cache.clear();
|
|
56
|
+
inflight.clear();
|
|
57
|
+
}
|
package/dist/update.js
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
3
3
|
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
const SEMVER_NUMERIC_IDENTIFIER = '(?:0|[1-9]\\d*)';
|
|
5
|
+
const SEMVER_NON_NUMERIC_PRERELEASE_IDENTIFIER = '\\d*[A-Za-z-][0-9A-Za-z-]*';
|
|
6
|
+
const SEMVER_PRERELEASE_IDENTIFIER = `(?:${SEMVER_NUMERIC_IDENTIFIER}|${SEMVER_NON_NUMERIC_PRERELEASE_IDENTIFIER})`;
|
|
7
|
+
const NPM_VERSION_PATTERN = new RegExp(`^v?${SEMVER_NUMERIC_IDENTIFIER}\\.${SEMVER_NUMERIC_IDENTIFIER}\\.${SEMVER_NUMERIC_IDENTIFIER}` +
|
|
8
|
+
`(?:-${SEMVER_PRERELEASE_IDENTIFIER}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER})*)?` +
|
|
9
|
+
`(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
|
|
10
|
+
function isAbortError(err) {
|
|
11
|
+
return typeof err === 'object' && err !== null && 'name' in err && err.name === 'AbortError';
|
|
12
|
+
}
|
|
13
|
+
function readLatestDistTag(body) {
|
|
14
|
+
if (typeof body !== 'object' || body === null)
|
|
15
|
+
return undefined;
|
|
16
|
+
const distTags = body['dist-tags'];
|
|
17
|
+
if (typeof distTags !== 'object' || distTags === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
return distTags.latest;
|
|
20
|
+
}
|
|
4
21
|
function packageUrl(registry, packageName) {
|
|
5
|
-
const base = registry.replace(/\/+$/, '') || DEFAULT_REGISTRY;
|
|
22
|
+
const base = registry.trim().replace(/\/+$/, '') || DEFAULT_REGISTRY;
|
|
6
23
|
const encoded = encodeURIComponent(packageName).replace(/^%40/, '@');
|
|
7
24
|
return `${base}/${encoded}`;
|
|
8
25
|
}
|
|
26
|
+
function normalizeNumericIdentifier(part) {
|
|
27
|
+
return /^\d+$/.test(part) ? part.replace(/^0+/, '') || '0' : undefined;
|
|
28
|
+
}
|
|
29
|
+
function compareNumericIdentifiers(a, b) {
|
|
30
|
+
if (a === b)
|
|
31
|
+
return 0;
|
|
32
|
+
if (a.length !== b.length)
|
|
33
|
+
return a.length > b.length ? 1 : -1;
|
|
34
|
+
return a > b ? 1 : -1;
|
|
35
|
+
}
|
|
9
36
|
function splitVersion(version) {
|
|
10
37
|
const [withoutBuild] = version.trim().replace(/^v/, '').split('+');
|
|
11
|
-
const
|
|
38
|
+
const prereleaseIndex = withoutBuild.indexOf('-');
|
|
39
|
+
const corePart = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex);
|
|
40
|
+
const prereleasePart = prereleaseIndex === -1 ? '' : withoutBuild.slice(prereleaseIndex + 1);
|
|
12
41
|
return {
|
|
13
|
-
core: corePart.split('.').map((part) =>
|
|
42
|
+
core: corePart.split('.').map((part) => normalizeNumericIdentifier(part) ?? '0'),
|
|
14
43
|
prerelease: prereleasePart ? prereleasePart.split('.') : [],
|
|
15
44
|
};
|
|
16
45
|
}
|
|
@@ -29,12 +58,13 @@ function comparePrerelease(a, b) {
|
|
|
29
58
|
return -1;
|
|
30
59
|
if (pb === undefined)
|
|
31
60
|
return 1;
|
|
32
|
-
const na =
|
|
33
|
-
const nb =
|
|
34
|
-
if (
|
|
35
|
-
return na
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
const na = normalizeNumericIdentifier(pa);
|
|
62
|
+
const nb = normalizeNumericIdentifier(pb);
|
|
63
|
+
if (na !== undefined && nb !== undefined && na !== nb) {
|
|
64
|
+
return compareNumericIdentifiers(na, nb);
|
|
65
|
+
}
|
|
66
|
+
if ((na !== undefined) !== (nb !== undefined))
|
|
67
|
+
return na !== undefined ? -1 : 1;
|
|
38
68
|
if (pa !== pb)
|
|
39
69
|
return pa > pb ? 1 : -1;
|
|
40
70
|
}
|
|
@@ -45,10 +75,10 @@ export function compareVersions(a, b) {
|
|
|
45
75
|
const vb = splitVersion(b);
|
|
46
76
|
const len = Math.max(va.core.length, vb.core.length, 3);
|
|
47
77
|
for (let i = 0; i < len; i++) {
|
|
48
|
-
const na = va.core[i] ?? 0;
|
|
49
|
-
const nb = vb.core[i] ?? 0;
|
|
78
|
+
const na = va.core[i] ?? '0';
|
|
79
|
+
const nb = vb.core[i] ?? '0';
|
|
50
80
|
if (na !== nb)
|
|
51
|
-
return na
|
|
81
|
+
return compareNumericIdentifiers(na, nb);
|
|
52
82
|
}
|
|
53
83
|
return comparePrerelease(va.prerelease, vb.prerelease);
|
|
54
84
|
}
|
|
@@ -71,7 +101,8 @@ export function shouldCheckForUpdate(cache, nowMs = Date.now(), intervalMs = UPD
|
|
|
71
101
|
export async function fetchLatestVersion(meta, opts = {}) {
|
|
72
102
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
73
103
|
const ctrl = new AbortController();
|
|
74
|
-
const
|
|
104
|
+
const timeoutMs = opts.timeoutMs ?? 8000;
|
|
105
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
75
106
|
try {
|
|
76
107
|
const res = await fetchImpl(packageUrl(opts.registry ?? process.env.npm_config_registry ?? DEFAULT_REGISTRY, meta.name), {
|
|
77
108
|
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
@@ -80,11 +111,19 @@ export async function fetchLatestVersion(meta, opts = {}) {
|
|
|
80
111
|
if (!res.ok) {
|
|
81
112
|
throw new Error(`npm registry ตอบ ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`);
|
|
82
113
|
}
|
|
83
|
-
const body =
|
|
84
|
-
const latest = body
|
|
85
|
-
if (!latest)
|
|
114
|
+
const body = await res.json();
|
|
115
|
+
const latest = readLatestDistTag(body);
|
|
116
|
+
if (typeof latest !== 'string' || !latest.trim())
|
|
86
117
|
throw new Error('npm registry ไม่มี dist-tag "latest"');
|
|
87
|
-
|
|
118
|
+
const trimmedLatest = latest.trim();
|
|
119
|
+
if (!NPM_VERSION_PATTERN.test(trimmedLatest))
|
|
120
|
+
throw new Error('npm registry dist-tag "latest" ไม่ใช่ semver');
|
|
121
|
+
return trimmedLatest;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (ctrl.signal.aborted && isAbortError(err))
|
|
125
|
+
throw new Error(`npm registry timeout after ${timeoutMs}ms`);
|
|
126
|
+
throw err;
|
|
88
127
|
}
|
|
89
128
|
finally {
|
|
90
129
|
clearTimeout(timer);
|