sanook-cli 0.5.0 → 0.5.2
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 +83 -5
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3045 -210
- package/dist/brain-context.js +223 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +371 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +12 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +152 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/commands.js +172 -13
- package/dist/compaction.js +96 -11
- package/dist/config.js +118 -28
- package/dist/context-compression.js +191 -0
- package/dist/cost.js +49 -15
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +37 -8
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +357 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/email.js +472 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +18 -0
- 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 +343 -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/insights-args.js +35 -0
- package/dist/insights.js +86 -0
- package/dist/loop.js +123 -24
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-registry.js +350 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +44 -6
- package/dist/memory.js +100 -33
- package/dist/orchestrate.js +49 -19
- package/dist/personality.js +58 -0
- package/dist/providers/codex.js +86 -38
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +38 -49
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +75 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session.js +93 -7
- package/dist/skill-install.js +29 -12
- package/dist/support-dump.js +175 -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 +5 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +923 -9
- package/dist/tools/read.js +16 -4
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +217 -13
- package/dist/tools/task.js +18 -7
- package/dist/tools/timeout.js +21 -3
- package/dist/trust.js +11 -1
- package/dist/ui/app.js +57 -11
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +107 -10
- package/dist/update.js +24 -11
- package/dist/worktree.js +175 -4
- package/package.json +4 -4
- 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 +3 -1
- package/second-brain/Projects/sanook-cli/_Index.md +26 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -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-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/_Index.md +6 -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 +22 -3
- 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 +4 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -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/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { appHomePath, persistenceEnabled } from '../brand.js';
|
|
5
|
+
import { runAgent } from '../loop.js';
|
|
6
|
+
import { redactKey } from '../providers/keys.js';
|
|
7
|
+
import { canonicalSpec, parseSpec, PROVIDERS } from '../providers/registry.js';
|
|
8
|
+
import { autoCompact, estimateTokens } from '../compaction.js';
|
|
9
|
+
import { patchGlobalConfig } from '../config.js';
|
|
10
|
+
import { parseInsightsDays } from '../insights-args.js';
|
|
11
|
+
import { normalizePersonalityName, personalityListText } from '../personality.js';
|
|
12
|
+
import { patchGatewayConfig, readGatewayConfig } from './config.js';
|
|
13
|
+
const SESSION_DIR = appHomePath('gateway', 'sessions');
|
|
14
|
+
function safePlatformSegment(platform) {
|
|
15
|
+
const safe = platform.trim().replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
16
|
+
return safe || 'gateway';
|
|
17
|
+
}
|
|
18
|
+
export function gatewaySessionId(platform, target) {
|
|
19
|
+
const digest = createHash('sha256').update(`${platform}:${target}`).digest('hex').slice(0, 24);
|
|
20
|
+
return `${safePlatformSegment(platform)}-${digest}`;
|
|
21
|
+
}
|
|
22
|
+
function sessionPath(id) {
|
|
23
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(id) || id.includes('..')) {
|
|
24
|
+
throw new Error(`gateway session id ไม่ถูกต้อง: ${id}`);
|
|
25
|
+
}
|
|
26
|
+
return join(SESSION_DIR, `${id}.json`);
|
|
27
|
+
}
|
|
28
|
+
function redactUnknown(value) {
|
|
29
|
+
if (typeof value === 'string')
|
|
30
|
+
return redactKey(value);
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return value.map(redactUnknown);
|
|
33
|
+
if (value && typeof value === 'object') {
|
|
34
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactUnknown(v)]));
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function shouldSuppressDelivery(text) {
|
|
39
|
+
const normalized = text.trim().toUpperCase().replace(/[\s_-]+/g, ' ');
|
|
40
|
+
return normalized === '[SILENT]' || normalized === 'SILENT' || normalized === 'NO REPLY';
|
|
41
|
+
}
|
|
42
|
+
export async function loadGatewaySession(platform, target) {
|
|
43
|
+
try {
|
|
44
|
+
const id = gatewaySessionId(platform, target);
|
|
45
|
+
return JSON.parse(await readFile(sessionPath(id), 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function listGatewaySessions() {
|
|
52
|
+
try {
|
|
53
|
+
const files = (await readdir(SESSION_DIR)).filter((f) => f.endsWith('.json'));
|
|
54
|
+
const sessions = await Promise.all(files.map(async (file) => {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(await readFile(join(SESSION_DIR, file), 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
return sessions.filter((s) => s !== null).sort((a, b) => b.updated.localeCompare(a.updated));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function saveGatewaySession(session) {
|
|
69
|
+
if (!persistenceEnabled())
|
|
70
|
+
return;
|
|
71
|
+
await mkdir(SESSION_DIR, { recursive: true });
|
|
72
|
+
const safeSession = {
|
|
73
|
+
...session,
|
|
74
|
+
messages: redactUnknown(session.messages),
|
|
75
|
+
};
|
|
76
|
+
await writeFile(sessionPath(session.id), `${JSON.stringify(safeSession, null, 2)}\n`, { mode: 0o600 });
|
|
77
|
+
await chmod(sessionPath(session.id), 0o600).catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
export async function removeGatewaySession(platform, target) {
|
|
80
|
+
try {
|
|
81
|
+
await rm(sessionPath(gatewaySessionId(platform, target)));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function gatewayCommandHelp() {
|
|
89
|
+
return [
|
|
90
|
+
'Messaging commands:',
|
|
91
|
+
'/new หรือ /reset — เริ่มบทสนทนาใหม่',
|
|
92
|
+
'/model [spec] — ดู/เปลี่ยน model ของ chat นี้',
|
|
93
|
+
'/personality [name] — ดู/ตั้ง personality overlay',
|
|
94
|
+
'/retry — รัน user turn ล่าสุดอีกครั้ง',
|
|
95
|
+
'/undo — ลบ exchange ล่าสุดจาก history',
|
|
96
|
+
'/compress — compact history ของ chat นี้',
|
|
97
|
+
'/usage — ดู usage โดยประมาณของ chat นี้',
|
|
98
|
+
'/insights [days] — ดู usage/session insights',
|
|
99
|
+
'/stop — หยุด turn ที่กำลังรัน (ถ้ามี)',
|
|
100
|
+
'/status — ดู session ปัจจุบัน',
|
|
101
|
+
'/sethome — ตั้ง chat นี้เป็น home target สำหรับ delivery/cron',
|
|
102
|
+
'/help — ดูคำสั่งที่รองรับ',
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
function messageText(content) {
|
|
106
|
+
if (typeof content === 'string')
|
|
107
|
+
return content;
|
|
108
|
+
if (Array.isArray(content)) {
|
|
109
|
+
return content
|
|
110
|
+
.map((part) => {
|
|
111
|
+
if (typeof part === 'string')
|
|
112
|
+
return part;
|
|
113
|
+
if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') {
|
|
114
|
+
return part.text;
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join('\n');
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
function lastUserIndex(messages) {
|
|
124
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
125
|
+
if (messages[i].role === 'user')
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
function trimLastExchange(messages) {
|
|
131
|
+
const userIdx = lastUserIndex(messages);
|
|
132
|
+
return userIdx === -1 ? messages : messages.slice(0, userIdx);
|
|
133
|
+
}
|
|
134
|
+
async function saveGatewayState(opts, existing, model, messages) {
|
|
135
|
+
const now = new Date().toISOString();
|
|
136
|
+
const session = {
|
|
137
|
+
id: gatewaySessionId(opts.platform, opts.target),
|
|
138
|
+
platform: opts.platform,
|
|
139
|
+
target: opts.target,
|
|
140
|
+
created: existing?.created ?? now,
|
|
141
|
+
updated: now,
|
|
142
|
+
model,
|
|
143
|
+
messages,
|
|
144
|
+
};
|
|
145
|
+
await saveGatewaySession(session);
|
|
146
|
+
return session;
|
|
147
|
+
}
|
|
148
|
+
async function runAndSaveGatewayTurn(opts, existing, prompt, history, model) {
|
|
149
|
+
const { text, messages } = await runAgent({
|
|
150
|
+
model,
|
|
151
|
+
prompt,
|
|
152
|
+
history,
|
|
153
|
+
maxSteps: opts.maxSteps ?? 20,
|
|
154
|
+
budgetUsd: opts.budgetUsd,
|
|
155
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
156
|
+
});
|
|
157
|
+
await saveGatewayState(opts, existing, model, messages);
|
|
158
|
+
return { text, messages, suppressDelivery: shouldSuppressDelivery(text) };
|
|
159
|
+
}
|
|
160
|
+
function addFirst(items, item) {
|
|
161
|
+
return [item, ...(items ?? []).filter((x) => x !== item)];
|
|
162
|
+
}
|
|
163
|
+
async function setHomeTarget(platform, target) {
|
|
164
|
+
const cfg = await readGatewayConfig();
|
|
165
|
+
switch (platform) {
|
|
166
|
+
case 'telegram': {
|
|
167
|
+
const chatId = Number(target);
|
|
168
|
+
if (!Number.isInteger(chatId))
|
|
169
|
+
return 'Telegram /sethome ต้องมาจาก numeric chat id';
|
|
170
|
+
await patchGatewayConfig({ telegram: { allowedChatIds: addFirst(cfg.telegram?.allowedChatIds, chatId) } });
|
|
171
|
+
return `ตั้ง Telegram home/allowed chat เป็น ${chatId} แล้ว`;
|
|
172
|
+
}
|
|
173
|
+
case 'discord':
|
|
174
|
+
await patchGatewayConfig({ discord: { defaultChannelId: target, allowedChannelIds: addFirst(cfg.discord?.allowedChannelIds, target) } });
|
|
175
|
+
return `ตั้ง Discord home channel เป็น ${target} แล้ว`;
|
|
176
|
+
case 'slack':
|
|
177
|
+
await patchGatewayConfig({ slack: { defaultChannelId: target, allowedChannelIds: addFirst(cfg.slack?.allowedChannelIds, target) } });
|
|
178
|
+
return `ตั้ง Slack home channel เป็น ${target} แล้ว`;
|
|
179
|
+
case 'mattermost':
|
|
180
|
+
await patchGatewayConfig({ mattermost: { homeChannel: target, allowedChannels: addFirst(cfg.mattermost?.allowedChannels, target) } });
|
|
181
|
+
return `ตั้ง Mattermost home channel เป็น ${target} แล้ว`;
|
|
182
|
+
case 'homeassistant':
|
|
183
|
+
await patchGatewayConfig({ homeassistant: { homeChannel: target } });
|
|
184
|
+
return `ตั้ง Home Assistant home notification เป็น ${target} แล้ว`;
|
|
185
|
+
case 'email':
|
|
186
|
+
await patchGatewayConfig({ email: { homeAddress: target, allowedUsers: addFirst(cfg.email?.allowedUsers, target.toLowerCase()) } });
|
|
187
|
+
return `ตั้ง Email home address เป็น ${target} แล้ว`;
|
|
188
|
+
case 'line':
|
|
189
|
+
await patchGatewayConfig({ line: { homeChannel: target, allowedUsers: addFirst(cfg.line?.allowedUsers, target) } });
|
|
190
|
+
return `ตั้ง LINE home channel เป็น ${target} แล้ว`;
|
|
191
|
+
case 'sms':
|
|
192
|
+
await patchGatewayConfig({ sms: { homeChannel: target, allowedUsers: addFirst(cfg.sms?.allowedUsers, target) } });
|
|
193
|
+
return `ตั้ง SMS home channel เป็น ${target} แล้ว`;
|
|
194
|
+
case 'ntfy':
|
|
195
|
+
await patchGatewayConfig({ ntfy: { homeChannel: target, allowedUsers: addFirst(cfg.ntfy?.allowedUsers, target) } });
|
|
196
|
+
return `ตั้ง ntfy home topic เป็น ${target} แล้ว`;
|
|
197
|
+
case 'signal':
|
|
198
|
+
await patchGatewayConfig({ signal: { homeChannel: target, allowedUsers: addFirst(cfg.signal?.allowedUsers, target) } });
|
|
199
|
+
return `ตั้ง Signal home channel เป็น ${target} แล้ว`;
|
|
200
|
+
case 'whatsapp':
|
|
201
|
+
await patchGatewayConfig({ whatsapp: { homeChannel: target, allowedUsers: addFirst(cfg.whatsapp?.allowedUsers, target) } });
|
|
202
|
+
return `ตั้ง WhatsApp home channel เป็น ${target} แล้ว`;
|
|
203
|
+
case 'matrix':
|
|
204
|
+
await patchGatewayConfig({ matrix: { homeRoom: target, allowedRooms: addFirst(cfg.matrix?.allowedRooms, target) } });
|
|
205
|
+
return `ตั้ง Matrix home room เป็น ${target} แล้ว`;
|
|
206
|
+
case 'googlechat':
|
|
207
|
+
await patchGatewayConfig({ googleChat: { homeChannel: target, allowedSpaces: addFirst(cfg.googleChat?.allowedSpaces, target) } });
|
|
208
|
+
return `ตั้ง Google Chat home channel เป็น ${target} แล้ว`;
|
|
209
|
+
case 'bluebubbles':
|
|
210
|
+
await patchGatewayConfig({ bluebubbles: { homeChannel: target, allowedUsers: addFirst(cfg.bluebubbles?.allowedUsers, target) } });
|
|
211
|
+
return `ตั้ง BlueBubbles home channel เป็น ${target} แล้ว`;
|
|
212
|
+
case 'teams':
|
|
213
|
+
await patchGatewayConfig({ teams: { homeChannel: target, chatId: target } });
|
|
214
|
+
return `ตั้ง Teams home channel เป็น ${target} แล้ว`;
|
|
215
|
+
default:
|
|
216
|
+
return `platform ${platform} ยังไม่รองรับ /sethome`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function handleGatewayCommand(opts) {
|
|
220
|
+
const input = opts.userText?.trim();
|
|
221
|
+
if (!input?.startsWith('/'))
|
|
222
|
+
return null;
|
|
223
|
+
const [command, ...args] = input.slice(1).trim().split(/\s+/);
|
|
224
|
+
const normalized = command?.toLowerCase();
|
|
225
|
+
if (!normalized)
|
|
226
|
+
return null;
|
|
227
|
+
if (normalized === 'new' || normalized === 'reset') {
|
|
228
|
+
await removeGatewaySession(opts.platform, opts.target);
|
|
229
|
+
return { text: 'เริ่มบทสนทนาใหม่แล้ว', messages: [], suppressDelivery: false };
|
|
230
|
+
}
|
|
231
|
+
if (normalized === 'status') {
|
|
232
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
233
|
+
const turns = existing?.messages.length ?? 0;
|
|
234
|
+
const text = existing
|
|
235
|
+
? [
|
|
236
|
+
`Session: ${existing.id}`,
|
|
237
|
+
`Platform: ${existing.platform}`,
|
|
238
|
+
`Target: ${existing.target}`,
|
|
239
|
+
`Model: ${existing.model}`,
|
|
240
|
+
`Messages: ${turns}`,
|
|
241
|
+
`Updated: ${existing.updated}`,
|
|
242
|
+
].join('\n')
|
|
243
|
+
: `ยังไม่มี session สำหรับ ${opts.platform}:${opts.target}`;
|
|
244
|
+
return { text, messages: existing?.messages ?? [], suppressDelivery: false };
|
|
245
|
+
}
|
|
246
|
+
if (normalized === 'help') {
|
|
247
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
248
|
+
return { text: gatewayCommandHelp(), messages: existing?.messages ?? [], suppressDelivery: false };
|
|
249
|
+
}
|
|
250
|
+
if (normalized === 'sethome') {
|
|
251
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
252
|
+
return { text: await setHomeTarget(opts.platform, opts.target), messages: existing?.messages ?? [], suppressDelivery: false };
|
|
253
|
+
}
|
|
254
|
+
if (normalized === 'stop') {
|
|
255
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
256
|
+
return { text: 'ไม่มี turn ที่กำลังทำงานให้หยุดใน command นี้', messages: existing?.messages ?? [], suppressDelivery: false };
|
|
257
|
+
}
|
|
258
|
+
if (normalized === 'model') {
|
|
259
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
260
|
+
const currentModel = existing?.model ?? opts.model;
|
|
261
|
+
const spec = args[0];
|
|
262
|
+
if (!spec)
|
|
263
|
+
return { text: `model ปัจจุบัน: ${currentModel}`, messages: existing?.messages ?? [], suppressDelivery: false };
|
|
264
|
+
const canonical = canonicalSpec(spec);
|
|
265
|
+
const parsed = parseSpec(canonical);
|
|
266
|
+
if (!PROVIDERS[parsed.provider] || !parsed.model) {
|
|
267
|
+
return { text: `model spec ไม่รองรับ: ${spec}`, messages: existing?.messages ?? [], suppressDelivery: false };
|
|
268
|
+
}
|
|
269
|
+
const session = await saveGatewayState(opts, existing, canonical, existing?.messages ?? []);
|
|
270
|
+
return { text: `เปลี่ยน model ของ chat นี้ → ${canonical}`, messages: session.messages, suppressDelivery: false };
|
|
271
|
+
}
|
|
272
|
+
if (normalized === 'personality') {
|
|
273
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
274
|
+
const raw = args.join(' ').trim();
|
|
275
|
+
if (!raw)
|
|
276
|
+
return { text: personalityListText(), messages: existing?.messages ?? [], suppressDelivery: false };
|
|
277
|
+
const name = normalizePersonalityName(raw);
|
|
278
|
+
if (!name)
|
|
279
|
+
return { text: `ไม่รู้จัก personality: ${raw}\n\n${personalityListText()}`, messages: existing?.messages ?? [], suppressDelivery: false };
|
|
280
|
+
await patchGlobalConfig({ personality: name === 'none' ? undefined : name });
|
|
281
|
+
return {
|
|
282
|
+
text: name === 'none' ? 'ปิด personality overlay แล้ว' : `ตั้ง personality → ${name}`,
|
|
283
|
+
messages: existing?.messages ?? [],
|
|
284
|
+
suppressDelivery: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (normalized === 'usage') {
|
|
288
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
289
|
+
const messages = existing?.messages ?? [];
|
|
290
|
+
return {
|
|
291
|
+
text: [
|
|
292
|
+
`messages: ${messages.length}`,
|
|
293
|
+
`approx tokens: ~${estimateTokens(messages)}`,
|
|
294
|
+
`model: ${existing?.model ?? opts.model}`,
|
|
295
|
+
].join('\n'),
|
|
296
|
+
messages,
|
|
297
|
+
suppressDelivery: false,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (normalized === 'insights') {
|
|
301
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
302
|
+
const days = parseInsightsDays(args);
|
|
303
|
+
if (days === null)
|
|
304
|
+
return { text: 'ใช้: /insights [days]', messages: existing?.messages ?? [], suppressDelivery: false };
|
|
305
|
+
const { renderInsights } = await import('../insights.js');
|
|
306
|
+
return { text: await renderInsights({ days, cwd: null, includeGateway: true }), messages: existing?.messages ?? [], suppressDelivery: false };
|
|
307
|
+
}
|
|
308
|
+
if (normalized === 'compress') {
|
|
309
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
310
|
+
if (!existing?.messages.length)
|
|
311
|
+
return { text: 'ยังไม่มี history ให้ compact', messages: [], suppressDelivery: false };
|
|
312
|
+
const before = estimateTokens(existing.messages);
|
|
313
|
+
const messages = autoCompact(existing.messages, 40_000, 20);
|
|
314
|
+
await saveGatewayState(opts, existing, existing.model, messages);
|
|
315
|
+
return { text: `compact แล้ว: ~${before} → ~${estimateTokens(messages)} tokens`, messages, suppressDelivery: false };
|
|
316
|
+
}
|
|
317
|
+
if (normalized === 'undo') {
|
|
318
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
319
|
+
if (!existing?.messages.length)
|
|
320
|
+
return { text: 'ยังไม่มี turn ให้ undo', messages: [], suppressDelivery: false };
|
|
321
|
+
const messages = trimLastExchange(existing.messages);
|
|
322
|
+
await saveGatewayState(opts, existing, existing.model, messages);
|
|
323
|
+
return { text: 'undo exchange ล่าสุดแล้ว', messages, suppressDelivery: false };
|
|
324
|
+
}
|
|
325
|
+
if (normalized === 'retry') {
|
|
326
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
327
|
+
const idx = existing ? lastUserIndex(existing.messages) : -1;
|
|
328
|
+
if (!existing || idx === -1)
|
|
329
|
+
return { text: 'ยังไม่มี user turn ให้ retry', messages: existing?.messages ?? [], suppressDelivery: false };
|
|
330
|
+
const prompt = messageText(existing.messages[idx].content).trim();
|
|
331
|
+
if (!prompt)
|
|
332
|
+
return { text: 'user turn ล่าสุดว่าง retry ไม่ได้', messages: existing.messages, suppressDelivery: false };
|
|
333
|
+
return runAndSaveGatewayTurn(opts, existing, prompt, existing.messages.slice(0, idx), existing.model);
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
export async function runGatewayAgent(opts) {
|
|
338
|
+
const command = await handleGatewayCommand(opts);
|
|
339
|
+
if (command)
|
|
340
|
+
return command;
|
|
341
|
+
const existing = await loadGatewaySession(opts.platform, opts.target);
|
|
342
|
+
return runAndSaveGatewayTurn(opts, existing, opts.prompt, existing?.messages ?? [], existing?.model ?? opts.model);
|
|
343
|
+
}
|