sanook-cli 0.5.2 → 0.5.7
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/CHANGELOG.md +112 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +637 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- package/dist/brain-link.js +73 -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.js +3 -0
- package/dist/brand.js +4 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +98 -15
- package/dist/config.js +66 -34
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +20 -0
- 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/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +34 -11
- 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 +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +65 -9
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- 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 +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +11 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-brain.js +103 -0
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +874 -35
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +30 -2
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +163 -50
- 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 +32 -6
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +4 -4
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- 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 +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -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 +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -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
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { HOTKEYS } from '../hotkeys.js';
|
|
4
|
+
const MIN_OVERLAY_COLUMNS = 42;
|
|
5
|
+
const MAX_OVERLAY_COLUMNS = 96;
|
|
6
|
+
const MODEL_WINDOW = 10;
|
|
7
|
+
const MCP_WINDOW = 10;
|
|
8
|
+
const SKILL_WINDOW = 10;
|
|
9
|
+
const SESSION_WINDOW = 10;
|
|
10
|
+
const TASK_WINDOW = 10;
|
|
11
|
+
const TOOL_WINDOW = 10;
|
|
12
|
+
const DEFAULT_PAGER_PAGE_SIZE = 12;
|
|
13
|
+
const COMPLETION_WINDOW = 8;
|
|
14
|
+
function OverlayBox({ children, columns }) {
|
|
15
|
+
const width = overlayWidth(columns);
|
|
16
|
+
return (_jsx(Box, { borderStyle: "double", borderColor: "cyan", flexDirection: "column", marginBottom: 1, paddingX: 1, width: width, children: children }));
|
|
17
|
+
}
|
|
18
|
+
function overlayWidth(columns) {
|
|
19
|
+
return Math.max(34, Math.min(Math.max(MIN_OVERLAY_COLUMNS, Math.floor(columns || 80) - 4), MAX_OVERLAY_COLUMNS));
|
|
20
|
+
}
|
|
21
|
+
function clip(text, width) {
|
|
22
|
+
if (width <= 0)
|
|
23
|
+
return '';
|
|
24
|
+
return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text;
|
|
25
|
+
}
|
|
26
|
+
export function completionOverlayLines(items, selected, columns) {
|
|
27
|
+
if (!items.length)
|
|
28
|
+
return [];
|
|
29
|
+
const width = Math.max(28, Math.min(Math.max(34, columns - 6), MAX_OVERLAY_COLUMNS));
|
|
30
|
+
const viewport = Math.min(COMPLETION_WINDOW, items.length);
|
|
31
|
+
const safeSelected = Math.max(0, Math.min(selected, items.length - 1));
|
|
32
|
+
const start = Math.max(0, Math.min(safeSelected - Math.floor(COMPLETION_WINDOW / 2), items.length - viewport));
|
|
33
|
+
const visible = items.slice(start, start + viewport);
|
|
34
|
+
const commandWidth = Math.max(10, Math.min(22, Math.floor(width * 0.34)));
|
|
35
|
+
const metaWidth = Math.max(8, width - commandWidth - 7);
|
|
36
|
+
const lines = visible.map((item, offset) => {
|
|
37
|
+
const index = start + offset;
|
|
38
|
+
const cursor = index === safeSelected ? '>' : ' ';
|
|
39
|
+
return `${cursor} ${clip(item.display, commandWidth).padEnd(commandWidth)} ${clip(item.meta, metaWidth)}`;
|
|
40
|
+
});
|
|
41
|
+
lines.push('↑↓ select · Tab/Enter complete');
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
export function CompletionOverlay({ columns, items, selected }) {
|
|
45
|
+
const width = Math.max(28, Math.min(Math.max(34, columns - 6), MAX_OVERLAY_COLUMNS));
|
|
46
|
+
const innerWidth = Math.max(1, width - 4);
|
|
47
|
+
const lines = completionOverlayLines(items, selected, columns);
|
|
48
|
+
if (!lines.length)
|
|
49
|
+
return null;
|
|
50
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", marginBottom: 1, paddingX: 1, width: width, children: lines.map((line, index) => {
|
|
51
|
+
const isActive = line.startsWith('>');
|
|
52
|
+
return (_jsx(Text, { color: isActive ? 'green' : undefined, dimColor: !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
53
|
+
}) }));
|
|
54
|
+
}
|
|
55
|
+
export function hotkeyOverlayLines(columns) {
|
|
56
|
+
const width = overlayWidth(columns);
|
|
57
|
+
const keyWidth = Math.min(24, HOTKEYS.reduce((max, [key]) => Math.max(max, key.length), 0));
|
|
58
|
+
const bodyWidth = Math.max(10, width - keyWidth - 7);
|
|
59
|
+
return [
|
|
60
|
+
'Sanook hotkeys',
|
|
61
|
+
...HOTKEYS.map(([key, help]) => `${key.padEnd(keyWidth)} ${clip(help, bodyWidth)}`),
|
|
62
|
+
'Esc / Enter / q close',
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function HotkeysOverlay({ columns }) {
|
|
66
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
67
|
+
const lines = hotkeyOverlayLines(columns);
|
|
68
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'cyan' : undefined, dimColor: index > 0, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`))) }));
|
|
69
|
+
}
|
|
70
|
+
function modelWindow(options, selected) {
|
|
71
|
+
const safeSelected = Math.max(0, Math.min(selected, Math.max(0, options.length - 1)));
|
|
72
|
+
const start = Math.max(0, Math.min(safeSelected - Math.floor(MODEL_WINDOW / 2), Math.max(0, options.length - MODEL_WINDOW)));
|
|
73
|
+
return { end: Math.min(options.length, start + MODEL_WINDOW), start };
|
|
74
|
+
}
|
|
75
|
+
function listWindow(count, selected, size) {
|
|
76
|
+
const safeSelected = Math.max(0, Math.min(selected, Math.max(0, count - 1)));
|
|
77
|
+
const start = Math.max(0, Math.min(safeSelected - Math.floor(size / 2), Math.max(0, count - size)));
|
|
78
|
+
return { end: Math.min(count, start + size), start };
|
|
79
|
+
}
|
|
80
|
+
function shortDate(iso) {
|
|
81
|
+
const parsed = Date.parse(iso);
|
|
82
|
+
if (!Number.isFinite(parsed))
|
|
83
|
+
return iso.slice(0, 16);
|
|
84
|
+
return new Date(parsed).toISOString().slice(0, 16).replace('T', ' ');
|
|
85
|
+
}
|
|
86
|
+
export function modelOverlayLines(overlay, columns) {
|
|
87
|
+
const width = overlayWidth(columns);
|
|
88
|
+
const innerWidth = Math.max(1, width - 4);
|
|
89
|
+
if (overlay.phase === 'provider') {
|
|
90
|
+
const window = listWindow(overlay.providers.length, overlay.selected, MODEL_WINDOW);
|
|
91
|
+
const visible = overlay.providers.slice(window.start, window.end);
|
|
92
|
+
const nameWidth = Math.max(12, Math.min(24, Math.floor(innerWidth * 0.45)));
|
|
93
|
+
const lines = ['Sanook model picker — choose provider'];
|
|
94
|
+
if (window.start > 0)
|
|
95
|
+
lines.push(`... ${window.start} above`);
|
|
96
|
+
for (const [offset, provider] of visible.entries()) {
|
|
97
|
+
const index = window.start + offset;
|
|
98
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
99
|
+
lines.push(`${cursor} ${clip(provider.label, nameWidth).padEnd(nameWidth)} ${provider.modelCount} models · ${provider.status}`);
|
|
100
|
+
}
|
|
101
|
+
if (window.end < overlay.providers.length)
|
|
102
|
+
lines.push(`... ${overlay.providers.length - window.end} more`);
|
|
103
|
+
lines.push('Enter drill down · Esc/q close');
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
const window = modelWindow(overlay.options, overlay.selected);
|
|
107
|
+
const visible = overlay.options.slice(window.start, window.end);
|
|
108
|
+
const optionWidth = Math.max(10, Math.min(28, Math.floor(innerWidth * 0.38)));
|
|
109
|
+
const metaWidth = Math.max(10, innerWidth - optionWidth - 8);
|
|
110
|
+
const providerLabel = overlay.providerFilter ?? 'all';
|
|
111
|
+
const lines = [`Sanook model picker — ${providerLabel}`];
|
|
112
|
+
if (window.start > 0)
|
|
113
|
+
lines.push(`... ${window.start} above`);
|
|
114
|
+
for (const [offset, option] of visible.entries()) {
|
|
115
|
+
const index = window.start + offset;
|
|
116
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
117
|
+
const current = option.current ? '*' : ' ';
|
|
118
|
+
lines.push(`${cursor}${current} ${clip(option.label, optionWidth).padEnd(optionWidth)} ${clip(option.meta, metaWidth)}`);
|
|
119
|
+
}
|
|
120
|
+
if (window.end < overlay.options.length)
|
|
121
|
+
lines.push(`... ${overlay.options.length - window.end} more`);
|
|
122
|
+
lines.push('Enter switch · Esc back to providers · q close');
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
125
|
+
function ModelPickerOverlay({ columns, overlay }) {
|
|
126
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
127
|
+
const lines = modelOverlayLines(overlay, columns);
|
|
128
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
129
|
+
const isHeader = index === 0;
|
|
130
|
+
const isActive = line.startsWith('>');
|
|
131
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'green' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
132
|
+
}) }));
|
|
133
|
+
}
|
|
134
|
+
export function mcpOverlayLines(overlay, columns) {
|
|
135
|
+
const width = overlayWidth(columns);
|
|
136
|
+
const innerWidth = Math.max(1, width - 4);
|
|
137
|
+
const selected = overlay.servers[overlay.selected];
|
|
138
|
+
if (!overlay.servers.length) {
|
|
139
|
+
return [
|
|
140
|
+
'Sanook MCP hub',
|
|
141
|
+
'No MCP servers configured',
|
|
142
|
+
'add: sanook mcp search github · sanook mcp install <server>',
|
|
143
|
+
...overlay.notes.slice(0, 2).map((note) => `note: ${clip(note, Math.max(1, innerWidth - 6))}`),
|
|
144
|
+
'Esc/q close',
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
if (overlay.detail && selected) {
|
|
148
|
+
const probe = overlay.probe?.serverName === selected.name ? overlay.probe : undefined;
|
|
149
|
+
const probeLines = mcpProbeLines(probe, innerWidth, overlay.toolSelected ?? 0);
|
|
150
|
+
return [
|
|
151
|
+
'Sanook MCP hub',
|
|
152
|
+
`${selected.name} (${selected.transport})`,
|
|
153
|
+
clip(selected.target, innerWidth),
|
|
154
|
+
`secrets: ${selected.secretSummary}`,
|
|
155
|
+
`doctor: t test selected · sanook mcp test ${selected.name}`,
|
|
156
|
+
...probeLines,
|
|
157
|
+
't test · Enter/Esc back · q close',
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
const nameWidth = Math.max(10, Math.min(22, Math.floor(innerWidth * 0.28)));
|
|
161
|
+
const targetWidth = Math.max(10, innerWidth - nameWidth - 19);
|
|
162
|
+
const window = listWindow(overlay.servers.length, overlay.selected, MCP_WINDOW);
|
|
163
|
+
const visible = overlay.servers.slice(window.start, window.end);
|
|
164
|
+
const lines = ['Sanook MCP hub', `${overlay.servers.length} servers · Enter inspect · t test`];
|
|
165
|
+
if (overlay.notes.length)
|
|
166
|
+
lines.push(`note: ${clip(overlay.notes[0], Math.max(1, innerWidth - 6))}`);
|
|
167
|
+
if (window.start > 0)
|
|
168
|
+
lines.push(`... ${window.start} above`);
|
|
169
|
+
for (const [offset, server] of visible.entries()) {
|
|
170
|
+
const index = window.start + offset;
|
|
171
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
172
|
+
lines.push(`${cursor} ${clip(server.name, nameWidth).padEnd(nameWidth)} ${server.transport.padEnd(5)} ${clip(server.target, targetWidth)}`);
|
|
173
|
+
}
|
|
174
|
+
if (window.end < overlay.servers.length)
|
|
175
|
+
lines.push(`... ${overlay.servers.length - window.end} more`);
|
|
176
|
+
lines.push('↑↓/jk select · Enter inspect · t test · Esc/q close');
|
|
177
|
+
return lines;
|
|
178
|
+
}
|
|
179
|
+
function mcpProbeLines(probe, innerWidth, selectedIndex = 0) {
|
|
180
|
+
if (!probe)
|
|
181
|
+
return [];
|
|
182
|
+
if (probe.status === 'running')
|
|
183
|
+
return ['test: running...'];
|
|
184
|
+
if (probe.status === 'fail') {
|
|
185
|
+
const transport = probe.transport ? ` (${probe.transport})` : '';
|
|
186
|
+
return [`test: FAIL${transport} ${clip(probe.error ?? 'unknown error', Math.max(1, innerWidth - 12))}`];
|
|
187
|
+
}
|
|
188
|
+
const tools = probe.tools ?? [];
|
|
189
|
+
const lines = [`test: PASS (${tools.length} tools)`];
|
|
190
|
+
if (!tools.length)
|
|
191
|
+
return lines;
|
|
192
|
+
const active = Math.max(0, Math.min(selectedIndex, tools.length - 1));
|
|
193
|
+
const window = listWindow(tools.length, active, 6);
|
|
194
|
+
const visible = tools.slice(window.start, window.end);
|
|
195
|
+
lines.push(`catalog: ${tools.length} tools · ↑↓/jk browse`);
|
|
196
|
+
if (window.start > 0)
|
|
197
|
+
lines.push(`... ${window.start} above`);
|
|
198
|
+
for (const [offset, tool] of visible.entries()) {
|
|
199
|
+
const index = window.start + offset;
|
|
200
|
+
const cursor = index === active ? '>' : ' ';
|
|
201
|
+
const description = tool.description ? ` - ${tool.description}` : '';
|
|
202
|
+
lines.push(`${cursor} ${clip(`${tool.name}${description}`, Math.max(1, innerWidth - 2))}`);
|
|
203
|
+
}
|
|
204
|
+
if (window.end < tools.length)
|
|
205
|
+
lines.push(`... ${tools.length - window.end} more tools`);
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
function McpHubOverlay({ columns, overlay }) {
|
|
209
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
210
|
+
const lines = mcpOverlayLines(overlay, columns);
|
|
211
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
212
|
+
const isHeader = index === 0;
|
|
213
|
+
const isActive = line.startsWith('>');
|
|
214
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'green' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
215
|
+
}) }));
|
|
216
|
+
}
|
|
217
|
+
export function pagerOverlayLines(overlay, columns, pageSize = DEFAULT_PAGER_PAGE_SIZE) {
|
|
218
|
+
const width = overlayWidth(columns);
|
|
219
|
+
const innerWidth = Math.max(1, width - 4);
|
|
220
|
+
const size = Math.max(3, pageSize);
|
|
221
|
+
const max = Math.max(0, overlay.lines.length - size);
|
|
222
|
+
const offset = Math.max(0, Math.min(overlay.offset, max));
|
|
223
|
+
const end = Math.min(overlay.lines.length, offset + size);
|
|
224
|
+
const title = overlay.title || 'Sanook pager';
|
|
225
|
+
const visible = overlay.lines.slice(offset, end).map((line) => clip(line || ' ', innerWidth));
|
|
226
|
+
const hint = end < overlay.lines.length
|
|
227
|
+
? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · Esc/q close (${end}/${overlay.lines.length})`
|
|
228
|
+
: `end · ↑↓/jk · b/PgUp back · g top · Esc/q close (${overlay.lines.length} lines)`;
|
|
229
|
+
return [title, ...visible, clip(hint, innerWidth)];
|
|
230
|
+
}
|
|
231
|
+
function PagerOverlay({ columns, overlay, pageSize }) {
|
|
232
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
233
|
+
const lines = pagerOverlayLines(overlay, columns, pageSize);
|
|
234
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'cyan' : undefined, dimColor: index > 0, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`))) }));
|
|
235
|
+
}
|
|
236
|
+
export function skillsOverlayLines(overlay, columns) {
|
|
237
|
+
const width = overlayWidth(columns);
|
|
238
|
+
const innerWidth = Math.max(1, width - 4);
|
|
239
|
+
const selected = overlay.skills[overlay.selected];
|
|
240
|
+
const nameWidth = Math.max(12, Math.min(30, Math.floor(innerWidth * 0.34)));
|
|
241
|
+
const descWidth = Math.max(10, innerWidth - nameWidth - 6);
|
|
242
|
+
if (!overlay.skills.length) {
|
|
243
|
+
return ['Sanook skills hub', 'No skills found', 'Esc/q close'];
|
|
244
|
+
}
|
|
245
|
+
if (overlay.detail && selected) {
|
|
246
|
+
return [
|
|
247
|
+
'Sanook skills hub',
|
|
248
|
+
selected.name,
|
|
249
|
+
selected.description ? clip(selected.description, innerWidth) : '(no description)',
|
|
250
|
+
selected.whenToUse ? `when: ${clip(selected.whenToUse, Math.max(1, innerWidth - 6))}` : '',
|
|
251
|
+
`path: ${clip(selected.path, Math.max(1, innerWidth - 6))}`,
|
|
252
|
+
'Enter/Esc back · q close',
|
|
253
|
+
].filter(Boolean);
|
|
254
|
+
}
|
|
255
|
+
const window = listWindow(overlay.skills.length, overlay.selected, SKILL_WINDOW);
|
|
256
|
+
const visible = overlay.skills.slice(window.start, window.end);
|
|
257
|
+
const lines = ['Sanook skills hub', `${overlay.skills.length} skills · Enter inspect`];
|
|
258
|
+
if (window.start > 0)
|
|
259
|
+
lines.push(`... ${window.start} above`);
|
|
260
|
+
for (const [offset, skill] of visible.entries()) {
|
|
261
|
+
const index = window.start + offset;
|
|
262
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
263
|
+
lines.push(`${cursor} ${clip(skill.name, nameWidth).padEnd(nameWidth)} ${clip(skill.description || '(no description)', descWidth)}`);
|
|
264
|
+
}
|
|
265
|
+
if (window.end < overlay.skills.length)
|
|
266
|
+
lines.push(`... ${overlay.skills.length - window.end} more`);
|
|
267
|
+
lines.push('↑↓/jk select · Enter inspect · Esc/q close');
|
|
268
|
+
return lines;
|
|
269
|
+
}
|
|
270
|
+
function SkillsHubOverlay({ columns, overlay }) {
|
|
271
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
272
|
+
const lines = skillsOverlayLines(overlay, columns);
|
|
273
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
274
|
+
const isHeader = index === 0;
|
|
275
|
+
const isActive = line.startsWith('>');
|
|
276
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'green' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
277
|
+
}) }));
|
|
278
|
+
}
|
|
279
|
+
export function toolsOverlayLines(overlay, columns) {
|
|
280
|
+
const width = overlayWidth(columns);
|
|
281
|
+
const innerWidth = Math.max(1, width - 4);
|
|
282
|
+
const selected = overlay.tools[overlay.selected];
|
|
283
|
+
const groupWidth = Math.max(8, Math.min(16, Math.floor(innerWidth * 0.2)));
|
|
284
|
+
const nameWidth = Math.max(12, Math.min(26, Math.floor(innerWidth * 0.3)));
|
|
285
|
+
const summaryWidth = Math.max(10, innerWidth - groupWidth - nameWidth - 8);
|
|
286
|
+
if (!overlay.tools.length) {
|
|
287
|
+
return ['Sanook tools hub', 'No built-in tools found', 'Esc/q close'];
|
|
288
|
+
}
|
|
289
|
+
if (overlay.detail && selected) {
|
|
290
|
+
return [
|
|
291
|
+
'Sanook tools hub',
|
|
292
|
+
`${selected.group} / ${selected.name}`,
|
|
293
|
+
clip(selected.summary, innerWidth),
|
|
294
|
+
`detail: ${clip(selected.detail, Math.max(1, innerWidth - 8))}`,
|
|
295
|
+
'MCP tools live in /mcp · skills live in /skills',
|
|
296
|
+
'Enter/Esc back · q close',
|
|
297
|
+
].map((line) => clip(line, width));
|
|
298
|
+
}
|
|
299
|
+
const window = listWindow(overlay.tools.length, overlay.selected, TOOL_WINDOW);
|
|
300
|
+
const visible = overlay.tools.slice(window.start, window.end);
|
|
301
|
+
const lines = ['Sanook tools hub', `${overlay.tools.length} built-in lanes · Enter inspect`];
|
|
302
|
+
if (window.start > 0)
|
|
303
|
+
lines.push(`... ${window.start} above`);
|
|
304
|
+
for (const [offset, tool] of visible.entries()) {
|
|
305
|
+
const index = window.start + offset;
|
|
306
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
307
|
+
lines.push(`${cursor} ${clip(tool.group, groupWidth).padEnd(groupWidth)} ${clip(tool.name, nameWidth).padEnd(nameWidth)} ${clip(tool.summary, summaryWidth)}`);
|
|
308
|
+
}
|
|
309
|
+
if (window.end < overlay.tools.length)
|
|
310
|
+
lines.push(`... ${overlay.tools.length - window.end} more`);
|
|
311
|
+
lines.push('↑↓/jk select · Enter inspect · Esc/q close');
|
|
312
|
+
return lines.map((line) => clip(line, width));
|
|
313
|
+
}
|
|
314
|
+
function ToolsHubOverlay({ columns, overlay }) {
|
|
315
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
316
|
+
const lines = toolsOverlayLines(overlay, columns);
|
|
317
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
318
|
+
const isHeader = index === 0;
|
|
319
|
+
const isActive = line.startsWith('>');
|
|
320
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'green' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
321
|
+
}) }));
|
|
322
|
+
}
|
|
323
|
+
function taskElapsed(rec, now = Date.now()) {
|
|
324
|
+
if (rec.state === 'running')
|
|
325
|
+
return `${Math.max(0, Math.floor((now - rec.startedMs) / 1000))}s…`;
|
|
326
|
+
if (rec.endedMs)
|
|
327
|
+
return `${((rec.endedMs - rec.startedMs) / 1000).toFixed(1)}s`;
|
|
328
|
+
return '—';
|
|
329
|
+
}
|
|
330
|
+
export function tasksOverlayLines(overlay, columns, now = Date.now()) {
|
|
331
|
+
const width = overlayWidth(columns);
|
|
332
|
+
const innerWidth = Math.max(1, width - 4);
|
|
333
|
+
const selected = overlay.tasks[overlay.selected];
|
|
334
|
+
if (!overlay.tasks.length) {
|
|
335
|
+
return [
|
|
336
|
+
'Sanook background tasks',
|
|
337
|
+
'No task_spawn jobs in this session',
|
|
338
|
+
'Esc/q close · spawn via task_spawn tool',
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
if (overlay.detail && selected) {
|
|
342
|
+
const lines = [
|
|
343
|
+
'Sanook background tasks',
|
|
344
|
+
`${selected.id} · ${selected.state} · ${taskElapsed(selected, now)}`,
|
|
345
|
+
selected.description,
|
|
346
|
+
'Esc back · q close',
|
|
347
|
+
];
|
|
348
|
+
if (selected.state === 'done' && selected.text) {
|
|
349
|
+
lines.push('', clip(selected.text.trim().slice(0, 800), innerWidth));
|
|
350
|
+
}
|
|
351
|
+
else if (selected.state === 'error' && selected.error) {
|
|
352
|
+
lines.push('', clip(selected.error, innerWidth));
|
|
353
|
+
}
|
|
354
|
+
else if (selected.state === 'running') {
|
|
355
|
+
lines.push('', 'Still running — task_collect(id) when ready');
|
|
356
|
+
}
|
|
357
|
+
return lines;
|
|
358
|
+
}
|
|
359
|
+
const window = listWindow(overlay.tasks.length, overlay.selected, TASK_WINDOW);
|
|
360
|
+
const visible = overlay.tasks.slice(window.start, window.end);
|
|
361
|
+
const running = overlay.tasks.filter((t) => t.state === 'running').length;
|
|
362
|
+
const lines = [
|
|
363
|
+
'Sanook background tasks',
|
|
364
|
+
`${overlay.tasks.length} job(s) · ${running} running · Enter inspect`,
|
|
365
|
+
];
|
|
366
|
+
for (let index = 0; index < visible.length; index++) {
|
|
367
|
+
const rec = visible[index];
|
|
368
|
+
const absolute = window.start + index;
|
|
369
|
+
const cursor = absolute === overlay.selected ? '>' : ' ';
|
|
370
|
+
lines.push(`${cursor} ${rec.id} ${rec.state.padEnd(8)} ${taskElapsed(rec, now)} ${clip(rec.description, Math.max(12, innerWidth - 28))}`);
|
|
371
|
+
}
|
|
372
|
+
if (window.end < overlay.tasks.length)
|
|
373
|
+
lines.push(`... ${overlay.tasks.length - window.end} more`);
|
|
374
|
+
lines.push('Esc/q close');
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
function TasksHubOverlay({ columns, overlay }) {
|
|
378
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
379
|
+
const lines = tasksOverlayLines(overlay, columns);
|
|
380
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
381
|
+
const isHeader = index === 0;
|
|
382
|
+
const isActive = line.startsWith('>');
|
|
383
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'yellow' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
384
|
+
}) }));
|
|
385
|
+
}
|
|
386
|
+
function sessionTitle(session, currentCwd) {
|
|
387
|
+
const base = session.title || firstUserSummary(session) || '(untitled)';
|
|
388
|
+
if (currentCwd && session.cwd !== currentCwd)
|
|
389
|
+
return `≠ ${base}`;
|
|
390
|
+
return base;
|
|
391
|
+
}
|
|
392
|
+
export function firstUserSummary(session) {
|
|
393
|
+
const user = session.messages.find((message) => message.role === 'user');
|
|
394
|
+
const content = user?.content;
|
|
395
|
+
if (typeof content === 'string')
|
|
396
|
+
return content.replace(/\s+/g, ' ').trim();
|
|
397
|
+
if (Array.isArray(content)) {
|
|
398
|
+
const text = content
|
|
399
|
+
.map((part) => (typeof part === 'object' && part && 'text' in part && typeof part.text === 'string' ? part.text : ''))
|
|
400
|
+
.join(' ')
|
|
401
|
+
.replace(/\s+/g, ' ')
|
|
402
|
+
.trim();
|
|
403
|
+
return text;
|
|
404
|
+
}
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
export function sessionsOverlayLines(overlay, columns) {
|
|
408
|
+
const width = overlayWidth(columns);
|
|
409
|
+
const innerWidth = Math.max(1, width - 4);
|
|
410
|
+
if (!overlay.sessions.length) {
|
|
411
|
+
return ['Sanook sessions', overlay.notice ? clip(overlay.notice, innerWidth) : 'No saved sessions yet', 'Esc/q close'];
|
|
412
|
+
}
|
|
413
|
+
const selected = overlay.sessions[overlay.selected];
|
|
414
|
+
if (overlay.renaming !== undefined && selected) {
|
|
415
|
+
const title = selected.title || firstUserSummary(selected) || selected.id;
|
|
416
|
+
return [
|
|
417
|
+
'Sanook sessions',
|
|
418
|
+
`rename: ${title}`,
|
|
419
|
+
`title: ${overlay.renaming || '(empty)'}`,
|
|
420
|
+
clip(overlay.notice ?? 'Enter save · Esc cancel', innerWidth),
|
|
421
|
+
];
|
|
422
|
+
}
|
|
423
|
+
if (overlay.detail && selected) {
|
|
424
|
+
const title = sessionTitle(selected, overlay.currentCwd);
|
|
425
|
+
const user = firstUserSummary(selected) || '(no user prompt found)';
|
|
426
|
+
const deleteHint = overlay.pendingDeleteId === selected.id
|
|
427
|
+
? 'Delete this session? press d again · Esc cancel'
|
|
428
|
+
: 'Enter resume · r rename · d delete · Esc back · q close';
|
|
429
|
+
return [
|
|
430
|
+
'Sanook sessions',
|
|
431
|
+
clip(title, innerWidth),
|
|
432
|
+
`id: ${clip(selected.id, Math.max(1, innerWidth - 4))}`,
|
|
433
|
+
`model: ${clip(selected.model, Math.max(1, innerWidth - 7))}`,
|
|
434
|
+
`updated: ${shortDate(selected.updated)} · messages: ${selected.messages.length}`,
|
|
435
|
+
`cwd: ${clip(selected.cwd, Math.max(1, innerWidth - 5))}`,
|
|
436
|
+
`first: ${clip(user, Math.max(1, innerWidth - 7))}`,
|
|
437
|
+
clip(deleteHint, innerWidth),
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
const idWidth = Math.max(12, Math.min(24, Math.floor(innerWidth * 0.28)));
|
|
441
|
+
const metaWidth = Math.max(12, Math.min(28, Math.floor(innerWidth * 0.34)));
|
|
442
|
+
const titleWidth = Math.max(10, innerWidth - idWidth - metaWidth - 8);
|
|
443
|
+
const window = listWindow(overlay.sessions.length, overlay.selected, SESSION_WINDOW);
|
|
444
|
+
const visible = overlay.sessions.slice(window.start, window.end);
|
|
445
|
+
const lines = ['Sanook sessions', `${overlay.sessions.length} resumable · all projects · Enter resume · i inspect · r rename · d delete`];
|
|
446
|
+
if (overlay.notice)
|
|
447
|
+
lines.push(clip(overlay.notice, innerWidth));
|
|
448
|
+
if (window.start > 0)
|
|
449
|
+
lines.push(`... ${window.start} above`);
|
|
450
|
+
for (const [offset, session] of visible.entries()) {
|
|
451
|
+
const index = window.start + offset;
|
|
452
|
+
const cursor = index === overlay.selected ? '>' : ' ';
|
|
453
|
+
const title = sessionTitle(session, overlay.currentCwd);
|
|
454
|
+
const meta = `${session.model} · ${shortDate(session.updated)}`;
|
|
455
|
+
lines.push(`${cursor} ${clip(session.id, idWidth).padEnd(idWidth)} ${clip(title, titleWidth).padEnd(titleWidth)} ${clip(meta, metaWidth)}`);
|
|
456
|
+
}
|
|
457
|
+
if (window.end < overlay.sessions.length)
|
|
458
|
+
lines.push(`... ${overlay.sessions.length - window.end} more`);
|
|
459
|
+
const active = overlay.sessions[overlay.selected];
|
|
460
|
+
if (active && overlay.pendingDeleteId === active.id)
|
|
461
|
+
lines.push('Delete selected? press d again · Esc cancel');
|
|
462
|
+
else
|
|
463
|
+
lines.push('↑↓/jk select · Enter resume · i inspect · r rename · Esc/q close');
|
|
464
|
+
return lines;
|
|
465
|
+
}
|
|
466
|
+
function SessionsSwitcherOverlay({ columns, overlay }) {
|
|
467
|
+
const innerWidth = Math.max(1, overlayWidth(columns) - 4);
|
|
468
|
+
const lines = sessionsOverlayLines(overlay, columns);
|
|
469
|
+
return (_jsx(OverlayBox, { columns: columns, children: lines.map((line, index) => {
|
|
470
|
+
const isHeader = index === 0;
|
|
471
|
+
const isActive = line.startsWith('>');
|
|
472
|
+
return (_jsx(Text, { color: isHeader ? 'cyan' : isActive ? 'green' : undefined, dimColor: !isHeader && !isActive, inverse: isActive, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`));
|
|
473
|
+
}) }));
|
|
474
|
+
}
|
|
475
|
+
/** Floating TUI overlays inspired by Hermes hubs; model/skills/session hubs plug in here. */
|
|
476
|
+
export function FloatingOverlay({ columns, overlay, pageSize = DEFAULT_PAGER_PAGE_SIZE }) {
|
|
477
|
+
if (!overlay)
|
|
478
|
+
return null;
|
|
479
|
+
if (overlay.kind === 'hotkeys')
|
|
480
|
+
return _jsx(HotkeysOverlay, { columns: columns });
|
|
481
|
+
if (overlay.kind === 'mcp')
|
|
482
|
+
return _jsx(McpHubOverlay, { columns: columns, overlay: overlay });
|
|
483
|
+
if (overlay.kind === 'model')
|
|
484
|
+
return _jsx(ModelPickerOverlay, { columns: columns, overlay: overlay });
|
|
485
|
+
if (overlay.kind === 'pager')
|
|
486
|
+
return _jsx(PagerOverlay, { columns: columns, overlay: overlay, pageSize: pageSize });
|
|
487
|
+
if (overlay.kind === 'skills')
|
|
488
|
+
return _jsx(SkillsHubOverlay, { columns: columns, overlay: overlay });
|
|
489
|
+
if (overlay.kind === 'sessions')
|
|
490
|
+
return _jsx(SessionsSwitcherOverlay, { columns: columns, overlay: overlay });
|
|
491
|
+
if (overlay.kind === 'tasks')
|
|
492
|
+
return _jsx(TasksHubOverlay, { columns: columns, overlay: overlay });
|
|
493
|
+
if (overlay.kind === 'tools')
|
|
494
|
+
return _jsx(ToolsHubOverlay, { columns: columns, overlay: overlay });
|
|
495
|
+
return null;
|
|
496
|
+
}
|
package/dist/ui/queue.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const QUEUE_WINDOW = 3;
|
|
2
|
+
export function compactPreview(text, width) {
|
|
3
|
+
const max = Math.max(8, width);
|
|
4
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
5
|
+
}
|
|
6
|
+
export function getQueueWindow(queueLength, activeIndex = null) {
|
|
7
|
+
const start = activeIndex === null ? 0 : Math.max(0, Math.min(activeIndex - 1, Math.max(0, queueLength - QUEUE_WINDOW)));
|
|
8
|
+
const end = Math.min(queueLength, start + QUEUE_WINDOW);
|
|
9
|
+
return { end, showLead: start > 0, showTail: end < queueLength, start };
|
|
10
|
+
}
|
|
11
|
+
export function clampQueueActiveIndex(activeIndex, queueLength) {
|
|
12
|
+
if (queueLength <= 0)
|
|
13
|
+
return null;
|
|
14
|
+
if (activeIndex === null)
|
|
15
|
+
return 0;
|
|
16
|
+
return Math.max(0, Math.min(activeIndex, queueLength - 1));
|
|
17
|
+
}
|
|
18
|
+
export function queueActiveIndexAfterDelete(activeIndex, previousLength) {
|
|
19
|
+
const active = clampQueueActiveIndex(activeIndex, previousLength);
|
|
20
|
+
if (active === null || previousLength <= 1)
|
|
21
|
+
return null;
|
|
22
|
+
return Math.min(active, previousLength - 2);
|
|
23
|
+
}
|
package/dist/ui/render.js
CHANGED
|
@@ -5,6 +5,17 @@ import { App } from './app.js';
|
|
|
5
5
|
import { SetupWizard } from './setup.js';
|
|
6
6
|
import { BrainWizard } from './brain-wizard.js';
|
|
7
7
|
import { saveKey, saveGlobalConfig, saveBrainPath } from '../config.js';
|
|
8
|
+
import { BRAND } from '../brand.js';
|
|
9
|
+
// Ink needs raw mode; mounting on a non-TTY stdin (piped/redirected/cron/CI) throws
|
|
10
|
+
// 'Raw mode is not supported' deep in react-reconciler and — worse — exits 0, so a
|
|
11
|
+
// script reads success on a fatal crash. Fail fast with a clear message + non-zero exit.
|
|
12
|
+
function requireInteractiveTTY() {
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
process.stderr.write(`${BRAND.cliName}: โหมด interactive (REPL/wizard) ต้องใช้ terminal จริง (TTY).\n` +
|
|
15
|
+
`รันแบบ headless แทน: ${BRAND.cliName} "<task>" หรือ ${BRAND.cliName} -z "<task>"\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
/**
|
|
9
20
|
* Root — โฮสต์ setup wizard → brain wizard → REPL ใน **Ink render เดียว**
|
|
10
21
|
*
|
|
@@ -21,7 +32,12 @@ export function Root({ needsSetup, appProps }) {
|
|
|
21
32
|
void (async () => {
|
|
22
33
|
if (r.key)
|
|
23
34
|
await saveKey(r.envVar, r.key);
|
|
24
|
-
await saveGlobalConfig({
|
|
35
|
+
await saveGlobalConfig({
|
|
36
|
+
model: r.model,
|
|
37
|
+
provider: r.provider,
|
|
38
|
+
locale: r.locale,
|
|
39
|
+
permissionMode: r.permissionMode,
|
|
40
|
+
});
|
|
25
41
|
setModel(r.model);
|
|
26
42
|
setPhase(r.createBrain ? 'brain' : 'app');
|
|
27
43
|
})();
|
|
@@ -32,6 +48,7 @@ export function Root({ needsSetup, appProps }) {
|
|
|
32
48
|
const onComplete = (a) => {
|
|
33
49
|
void (async () => {
|
|
34
50
|
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('../brain.js');
|
|
51
|
+
const { linkBrainToProject } = await import('../brain-link.js');
|
|
35
52
|
const today = new Date().toISOString().slice(0, 10);
|
|
36
53
|
const target = expandHome(a.path);
|
|
37
54
|
try {
|
|
@@ -44,8 +61,12 @@ export function Root({ needsSetup, appProps }) {
|
|
|
44
61
|
});
|
|
45
62
|
await saveBrainPath(target);
|
|
46
63
|
const wired = await wireBrainMcp(target).catch(() => 'skip');
|
|
64
|
+
const linked = await linkBrainToProject({ brainPath: target, cwd: process.cwd(), today }).catch(() => null);
|
|
65
|
+
const linkNote = linked?.projectRelDir
|
|
66
|
+
? ` · project ${linked.projectRelDir} · ${linked.memoryCreated ? 'created' : 'linked'} ${BRAND.memoryFileName}`
|
|
67
|
+
: '';
|
|
47
68
|
setBrainNote(`✅ second-brain — ${target} · สร้าง ${res.created.length} ไฟล์ · ` +
|
|
48
|
-
`${wired === 'added' ? 'wire filesystem MCP เข้า vault แล้ว' : 'MCP เดิมอยู่แล้ว (ไม่ทับ)'} · เปิดใน Obsidian: Open folder as vault`);
|
|
69
|
+
`${wired === 'added' ? 'wire filesystem MCP เข้า vault แล้ว' : 'MCP เดิมอยู่แล้ว (ไม่ทับ)'}${linkNote} · เปิดใน Obsidian: Open folder as vault`);
|
|
49
70
|
}
|
|
50
71
|
catch (e) {
|
|
51
72
|
setBrainNote(`⚠ สร้าง second-brain ไม่สำเร็จ: ${e.message} — ลองใหม่ด้วย ${'`'}sanook brain init${'`'}`);
|
|
@@ -60,19 +81,23 @@ export function Root({ needsSetup, appProps }) {
|
|
|
60
81
|
}
|
|
61
82
|
/** เปิดแอป: wizard (ถ้า first-run) → REPL — Ink render ครั้งเดียว (fix: พิมพ์ในช่องแชทไม่ได้) */
|
|
62
83
|
export function startApp(props) {
|
|
84
|
+
requireInteractiveTTY();
|
|
63
85
|
render(_jsx(Root, { ...props }));
|
|
64
86
|
}
|
|
65
87
|
/** เปิด REPL ตรงๆ (ไม่ผ่าน wizard) — เก็บไว้เผื่อ caller อื่น */
|
|
66
88
|
export function startRepl(appProps) {
|
|
89
|
+
requireInteractiveTTY();
|
|
67
90
|
render(_jsx(App, { ...appProps }));
|
|
68
91
|
}
|
|
69
92
|
/** standalone `sanook brain init` (interactive): ถาม path + ตัวตน → scaffold + wire MCP — single render, จบแล้ว process ออก */
|
|
70
93
|
export function startBrainSetup() {
|
|
94
|
+
requireInteractiveTTY();
|
|
71
95
|
return new Promise((resolve) => {
|
|
72
96
|
let unmount = () => { };
|
|
73
97
|
const onComplete = (a) => {
|
|
74
98
|
void (async () => {
|
|
75
99
|
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('../brain.js');
|
|
100
|
+
const { linkBrainToProject } = await import('../brain-link.js');
|
|
76
101
|
const today = new Date().toISOString().slice(0, 10);
|
|
77
102
|
const target = expandHome(a.path);
|
|
78
103
|
const res = await scaffoldBrain(target, {
|
|
@@ -84,9 +109,12 @@ export function startBrainSetup() {
|
|
|
84
109
|
});
|
|
85
110
|
await saveBrainPath(target);
|
|
86
111
|
const wired = await wireBrainMcp(target).catch(() => 'skip');
|
|
112
|
+
const linked = await linkBrainToProject({ brainPath: target, cwd: process.cwd(), today }).catch(() => null);
|
|
87
113
|
unmount();
|
|
114
|
+
const linkLine = linked?.projectRelDir ? `\n linked repo → ${linked.projectRelDir} · ${BRAND.memoryFileName} in cwd` : '';
|
|
88
115
|
process.stdout.write(`\n✅ second-brain — ${target}\n สร้าง ${res.created.length} · ข้าม ${res.skipped.length} (มีอยู่แล้ว ไม่ทับ)` +
|
|
89
116
|
`\n ${wired === 'added' ? 'wire filesystem MCP เข้า vault แล้ว (agent อ่าน/เขียนได้)' : 'MCP: มี server เดิมอยู่แล้ว (ไม่ทับ)'}` +
|
|
117
|
+
`${linkLine}` +
|
|
90
118
|
`\n เปิดใน Obsidian: Open folder as vault\n`);
|
|
91
119
|
resolve();
|
|
92
120
|
})();
|