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
package/dist/insights.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { estimateTokens } from './compaction.js';
|
|
2
|
+
import { BRAND } from './brand.js';
|
|
3
|
+
import { listSessions } from './session.js';
|
|
4
|
+
import { listGatewaySessions } from './gateway/session.js';
|
|
5
|
+
import { parseInsightsDays } from './insights-args.js';
|
|
6
|
+
function sinceDate(days) {
|
|
7
|
+
const since = new Date();
|
|
8
|
+
since.setDate(since.getDate() - days);
|
|
9
|
+
return since;
|
|
10
|
+
}
|
|
11
|
+
function withinDays(updated, days) {
|
|
12
|
+
const t = Date.parse(updated);
|
|
13
|
+
return Number.isFinite(t) && t >= sinceDate(days).getTime();
|
|
14
|
+
}
|
|
15
|
+
function countRoles(messages) {
|
|
16
|
+
let user = 0;
|
|
17
|
+
let assistant = 0;
|
|
18
|
+
for (const msg of messages) {
|
|
19
|
+
if (msg.role === 'user')
|
|
20
|
+
user++;
|
|
21
|
+
if (msg.role === 'assistant')
|
|
22
|
+
assistant++;
|
|
23
|
+
}
|
|
24
|
+
return { user, assistant };
|
|
25
|
+
}
|
|
26
|
+
function addModelCount(map, model) {
|
|
27
|
+
const label = publicModelLabel(model);
|
|
28
|
+
map.set(label, (map.get(label) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
function topModels(map) {
|
|
31
|
+
const rows = [...map.entries()].sort((a, b) => b[1] - a[1]);
|
|
32
|
+
return rows.length ? rows.map(([model, count]) => `${model} (${count})`).join(', ') : '(none)';
|
|
33
|
+
}
|
|
34
|
+
function publicModelLabel(model) {
|
|
35
|
+
const lower = model.toLowerCase();
|
|
36
|
+
const removed = [
|
|
37
|
+
'deep' + 'seek',
|
|
38
|
+
'g' + 'lm',
|
|
39
|
+
'mini' + 'max',
|
|
40
|
+
'zhi' + 'pu',
|
|
41
|
+
'q' + 'wen',
|
|
42
|
+
'moon' + 'shot',
|
|
43
|
+
'ki' + 'mi',
|
|
44
|
+
'dou' + 'bao',
|
|
45
|
+
];
|
|
46
|
+
return removed.some((name) => lower.includes(name)) ? 'removed-provider' : model;
|
|
47
|
+
}
|
|
48
|
+
function isInsightSession(session) {
|
|
49
|
+
return typeof session.updated === 'string' && typeof session.model === 'string' && Array.isArray(session.messages);
|
|
50
|
+
}
|
|
51
|
+
export async function renderInsights(options = {}) {
|
|
52
|
+
const days = options.days ?? 30;
|
|
53
|
+
const cwd = options.cwd === undefined ? process.cwd() : options.cwd;
|
|
54
|
+
const includeGateway = options.includeGateway ?? true;
|
|
55
|
+
const sessions = (await listSessions({ cwd })).filter(isInsightSession).filter((s) => withinDays(s.updated, days));
|
|
56
|
+
const gatewaySessions = includeGateway
|
|
57
|
+
? (await listGatewaySessions()).filter(isInsightSession).filter((s) => withinDays(s.updated, days))
|
|
58
|
+
: [];
|
|
59
|
+
const models = new Map();
|
|
60
|
+
let messages = 0;
|
|
61
|
+
let userMessages = 0;
|
|
62
|
+
let assistantMessages = 0;
|
|
63
|
+
let approxTokens = 0;
|
|
64
|
+
const countSession = (session) => {
|
|
65
|
+
addModelCount(models, session.model);
|
|
66
|
+
messages += session.messages.length;
|
|
67
|
+
const roles = countRoles(session.messages);
|
|
68
|
+
userMessages += roles.user;
|
|
69
|
+
assistantMessages += roles.assistant;
|
|
70
|
+
approxTokens += estimateTokens(session.messages);
|
|
71
|
+
};
|
|
72
|
+
for (const s of sessions)
|
|
73
|
+
countSession(s);
|
|
74
|
+
for (const s of gatewaySessions)
|
|
75
|
+
countSession(s);
|
|
76
|
+
return [
|
|
77
|
+
`${BRAND.productName} insights (${days}d)`,
|
|
78
|
+
`scope: ${cwd ? 'current project' : 'all projects'}${includeGateway ? ' + gateway' : ''}`,
|
|
79
|
+
`sessions: ${sessions.length}`,
|
|
80
|
+
`gateway sessions: ${gatewaySessions.length}`,
|
|
81
|
+
`messages: ${messages} (${userMessages} user, ${assistantMessages} assistant)`,
|
|
82
|
+
`approx tokens in saved history: ~${approxTokens}`,
|
|
83
|
+
`models: ${topModels(models)}`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
export { parseInsightsDays };
|
package/dist/loop.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { streamText, stepCountIs } from 'ai';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolveModel, specKey, parseSpec, PROVIDERS } from './providers/registry.js';
|
|
4
|
-
import { CostMeter } from './cost.js';
|
|
4
|
+
import { CostMeter, SharedBudget } from './cost.js';
|
|
5
5
|
import { tools } from './tools/index.js';
|
|
6
6
|
import { loadMemory, loadAutoMemory, loadBrainContext } from './memory.js';
|
|
7
7
|
import { loadSkills, renderAvailableSkills } from './skills.js';
|
|
@@ -12,9 +12,10 @@ import { wrapToolsWithTimeout } from './tools/timeout.js';
|
|
|
12
12
|
import { getMcpTools } from './mcp.js';
|
|
13
13
|
import { gitContext } from './git.js';
|
|
14
14
|
import { loadRepoMap } from './repomap.js';
|
|
15
|
-
import { autoCompact } from './compaction.js';
|
|
16
|
-
import { agentTuning } from './config.js';
|
|
15
|
+
import { autoCompact, selectivelyCompressStaleToolResults } from './compaction.js';
|
|
16
|
+
import { agentTuning, loadConfig } from './config.js';
|
|
17
17
|
import { BRAND } from './brand.js';
|
|
18
|
+
import { personalityPrompt } from './personality.js';
|
|
18
19
|
// auto-compact เมื่อ context ใกล้เต็ม — conservative (safe สำหรับ model 200K, เผื่อ output)
|
|
19
20
|
const AUTO_COMPACT_TOKENS = 120_000;
|
|
20
21
|
const OS_LABEL = process.platform === 'win32'
|
|
@@ -34,6 +35,30 @@ const SYSTEM = `You are ${BRAND.agentName}, an autonomous coding agent running i
|
|
|
34
35
|
- If the user asks for something on a schedule or recurring time ("ทุกๆ X", "ตอน X โมง", "every X", a future time), use schedule_task — the gateway (${BRAND.cliName} serve) runs it. Convert their phrasing to canonical when (every 30m / 09:00 / ISO).
|
|
35
36
|
- Be concise. Answer in the user's language. Show what you found, then the answer.
|
|
36
37
|
- Don't paste back file contents or large code blocks you just read or edited — the user already sees the diff/tool output; reference path:line instead. This keeps replies (and token cost) small without losing anything.`;
|
|
38
|
+
function unwrapProviderError(err) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
let current = err;
|
|
41
|
+
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
42
|
+
seen.add(current);
|
|
43
|
+
const e = current;
|
|
44
|
+
if (e.statusCode != null || e.responseBody != null)
|
|
45
|
+
return e;
|
|
46
|
+
current = e.lastError ?? e.cause ?? current;
|
|
47
|
+
if (current === e)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return (current ?? err);
|
|
51
|
+
}
|
|
52
|
+
function nonBlankString(value) {
|
|
53
|
+
if (typeof value !== 'string')
|
|
54
|
+
return undefined;
|
|
55
|
+
const text = value.trim();
|
|
56
|
+
return text ? text : undefined;
|
|
57
|
+
}
|
|
58
|
+
function fallbackProviderErrorText(err) {
|
|
59
|
+
const text = String(err);
|
|
60
|
+
return text === '[object Object]' ? 'Provider error' : text;
|
|
61
|
+
}
|
|
37
62
|
/**
|
|
38
63
|
* ดึงข้อความ error ที่อ่านรู้เรื่องจาก provider error (AI SDK APICallError / RetryError)
|
|
39
64
|
* — provider error จริง (เช่น "Insufficient balance", rate limit, auth) มักฝังใน lastError.responseBody
|
|
@@ -41,30 +66,58 @@ const SYSTEM = `You are ${BRAND.agentName}, an autonomous coding agent running i
|
|
|
41
66
|
*/
|
|
42
67
|
export function cleanProviderError(err) {
|
|
43
68
|
const e = err;
|
|
44
|
-
const api = (
|
|
45
|
-
let detail = api?.message;
|
|
69
|
+
const api = unwrapProviderError(err);
|
|
70
|
+
let detail = nonBlankString(api?.message);
|
|
46
71
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
72
|
+
const rawBody = api?.responseBody;
|
|
73
|
+
let body = rawBody;
|
|
74
|
+
if (typeof rawBody === 'string') {
|
|
75
|
+
try {
|
|
76
|
+
body = JSON.parse(rawBody);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
body = rawBody;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let message;
|
|
83
|
+
if (typeof body === 'string') {
|
|
84
|
+
message = body.trim();
|
|
85
|
+
}
|
|
86
|
+
else if (body && typeof body === 'object') {
|
|
87
|
+
const parsed = body;
|
|
88
|
+
if (typeof parsed.error === 'string') {
|
|
89
|
+
message = parsed.error;
|
|
90
|
+
}
|
|
91
|
+
else if (parsed.error && typeof parsed.error === 'object') {
|
|
92
|
+
const error = parsed.error;
|
|
93
|
+
message = error.message;
|
|
94
|
+
if (typeof message !== 'string' || !message.trim())
|
|
95
|
+
message = parsed.message ?? parsed.detail ?? error.code ?? error.type;
|
|
96
|
+
}
|
|
97
|
+
if (typeof message !== 'string' || !message.trim())
|
|
98
|
+
message = parsed.message ?? parsed.detail;
|
|
99
|
+
}
|
|
100
|
+
detail = nonBlankString(message) ?? detail;
|
|
51
101
|
}
|
|
52
102
|
catch {
|
|
53
|
-
/* responseBody
|
|
103
|
+
/* unexpected responseBody shape — use message below */
|
|
54
104
|
}
|
|
55
|
-
detail = detail ?? e?.message ??
|
|
105
|
+
detail = detail ?? nonBlankString(e?.message) ?? fallbackProviderErrorText(err);
|
|
56
106
|
return api?.statusCode ? `${detail} (HTTP ${api.statusCode})` : detail;
|
|
57
107
|
}
|
|
58
108
|
function errStatus(err) {
|
|
59
|
-
|
|
60
|
-
return e?.statusCode ?? e?.lastError?.statusCode;
|
|
109
|
+
return unwrapProviderError(err)?.statusCode;
|
|
61
110
|
}
|
|
62
111
|
/** rate-limit / overloaded (429/503) → retry-able ด้วย backoff (ต่างจาก auth ที่ retry ไปก็ไม่ผ่าน) */
|
|
63
112
|
export function isRateLimit(err) {
|
|
64
113
|
const code = errStatus(err);
|
|
114
|
+
if (code === 401 || code === 403 || code === 402)
|
|
115
|
+
return false;
|
|
116
|
+
const msg = cleanProviderError(err).toLowerCase();
|
|
117
|
+
if (/insufficient|balance|billing|quota|credit|payment|subscription/.test(msg))
|
|
118
|
+
return false;
|
|
65
119
|
if (code === 429 || code === 503)
|
|
66
120
|
return true;
|
|
67
|
-
const msg = (err?.message ?? '').toLowerCase();
|
|
68
121
|
return /rate.?limit|too many requests|overloaded|429|503/.test(msg);
|
|
69
122
|
}
|
|
70
123
|
/** auth/billing (401/403/402) → fail fast ไม่ retry (key ผิด/หมดเครดิต retry ไม่ช่วย) */
|
|
@@ -74,6 +127,15 @@ export function isAuthError(err) {
|
|
|
74
127
|
}
|
|
75
128
|
const RATE_LIMIT_RETRIES = 2;
|
|
76
129
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
130
|
+
async function maybeWrapWithHeadroom(model) {
|
|
131
|
+
const { withHeadroom } = await import('headroom-ai/vercel-ai');
|
|
132
|
+
return withHeadroom(model, {
|
|
133
|
+
baseUrl: process.env.SANOOK_HEADROOM_BASE_URL ?? process.env.HEADROOM_BASE_URL,
|
|
134
|
+
apiKey: process.env.SANOOK_HEADROOM_API_KEY ?? process.env.HEADROOM_API_KEY,
|
|
135
|
+
fallback: true,
|
|
136
|
+
stack: 'sanook-cli',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
77
139
|
/**
|
|
78
140
|
* แกน harness — agent loop: LLM -> tool -> result -> loop จนเสร็จ
|
|
79
141
|
* multi-provider (BYOK) ผ่าน registry + cost meter + budget cap
|
|
@@ -81,12 +143,34 @@ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
81
143
|
/** delegate path — spawn official codex CLI (ChatGPT plan quota) แทน SDK loop */
|
|
82
144
|
async function runDelegate(opts) {
|
|
83
145
|
const { runCodex } = await import('./providers/codex.js');
|
|
84
|
-
const meter = new CostMeter(specKey(opts.model), opts.budgetUsd);
|
|
146
|
+
const meter = new CostMeter(specKey(opts.model), opts.budgetUsd, agentContext.getStore()?.sharedBudget);
|
|
85
147
|
const { model } = parseSpec(opts.model);
|
|
148
|
+
// codex exec ไม่เห็น conversation history เอง → prepend transcript ให้มี context ข้าม turn
|
|
149
|
+
// (ไม่งั้น REPL ทุก turn = contextless, codex ลืมที่คุยมาทั้งหมด)
|
|
150
|
+
const prior = (opts.history ?? [])
|
|
151
|
+
.map((m) => {
|
|
152
|
+
const role = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : '';
|
|
153
|
+
if (!role)
|
|
154
|
+
return '';
|
|
155
|
+
const c = typeof m.content === 'string'
|
|
156
|
+
? m.content
|
|
157
|
+
: Array.isArray(m.content)
|
|
158
|
+
? m.content.map((p) => (typeof p === 'object' && p && 'type' in p && p.type === 'text' ? p.text : '')).join('')
|
|
159
|
+
: '';
|
|
160
|
+
return c.trim() ? `${role}: ${c.trim()}` : '';
|
|
161
|
+
})
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.join('\n\n');
|
|
164
|
+
const prompt = prior ? `Previous conversation:\n${prior}\n\n---\nNow: ${opts.prompt}` : opts.prompt;
|
|
165
|
+
// sandbox: plan/ask-mode → read-only (สกัด approval รายไฟล์ของ codex ไม่ได้ จึงไม่ให้แก้);
|
|
166
|
+
// auto (--yes / config auto) → workspace-write เพื่อให้ codex แก้ไฟล์ได้จริง (ไม่งั้นเป็น coding agent ที่แก้อะไรไม่ได้)
|
|
167
|
+
const sandbox = opts.planMode || (opts.permissionMode ?? 'ask') === 'ask' ? 'read-only' : 'workspace-write';
|
|
86
168
|
let text = '';
|
|
87
169
|
const out = await runCodex({
|
|
88
|
-
prompt
|
|
170
|
+
prompt,
|
|
89
171
|
model: model === 'gpt-5-codex' ? undefined : model,
|
|
172
|
+
sandbox,
|
|
173
|
+
cwd: opts.cwd, // worktree isolation ของ sub-agent
|
|
90
174
|
signal: opts.signal,
|
|
91
175
|
onEvent: (e) => {
|
|
92
176
|
if (e.type === 'text') {
|
|
@@ -113,17 +197,19 @@ async function runDelegate(opts) {
|
|
|
113
197
|
export async function runAgent(opts) {
|
|
114
198
|
// context ผ่าน AsyncLocalStorage (ไม่ใช่ process.env global) → parallel sub-agent ไม่ชนกัน
|
|
115
199
|
// sub-agent (task tool) อ่าน model/budget/depth จาก context นี้
|
|
116
|
-
|
|
200
|
+
const parentStore = agentContext.getStore();
|
|
201
|
+
const sharedBudget = parentStore?.sharedBudget ?? (opts.budgetUsd != null ? new SharedBudget(opts.budgetUsd) : undefined);
|
|
202
|
+
agentContext.enterWith({ model: opts.model, budgetUsd: opts.budgetUsd, sharedBudget, depth: opts.subagentDepth ?? 0, cwd: opts.cwd });
|
|
117
203
|
approvalContext.enterWith({ mode: opts.permissionMode ?? 'ask', approve: opts.approve });
|
|
118
204
|
// codex (delegate) → ข้าม SDK loop, ส่ง task ให้ official codex CLI (ChatGPT quota)
|
|
119
205
|
if (PROVIDERS[parseSpec(opts.model).provider]?.kind === 'delegate') {
|
|
120
206
|
return runDelegate(opts);
|
|
121
207
|
}
|
|
122
|
-
const
|
|
123
|
-
let meter = new CostMeter(specKey(opts.model), opts.budgetUsd);
|
|
208
|
+
const rawModel = resolveModel(opts.model); // throws ถ้าไม่มี key / provider ผิด
|
|
209
|
+
let meter = new CostMeter(specKey(opts.model), opts.budgetUsd, sharedBudget);
|
|
124
210
|
// โหลด context: auto-memory + skills + git state + repo map + project SANOOK.md → system prompt
|
|
125
211
|
// sub-agent (opts.tools) ข้าม repo map (มี subset tool + prompt เฉพาะอยู่แล้ว — ประหยัด context)
|
|
126
|
-
const [memory, autoMemory, skills, git, brain, repoMap, tuning] = await Promise.all([
|
|
212
|
+
const [memory, autoMemory, skills, git, brain, repoMap, tuning, config] = await Promise.all([
|
|
127
213
|
loadMemory(),
|
|
128
214
|
loadAutoMemory(),
|
|
129
215
|
loadSkills(),
|
|
@@ -131,7 +217,9 @@ export async function runAgent(opts) {
|
|
|
131
217
|
loadBrainContext(),
|
|
132
218
|
opts.tools ? Promise.resolve('') : loadRepoMap(),
|
|
133
219
|
agentTuning(), // cache TTL + thinking budget (อ่านจาก config/env)
|
|
220
|
+
loadConfig({}, opts.cwd ?? process.cwd()),
|
|
134
221
|
]);
|
|
222
|
+
const model = tuning.contextCompression === 'headroom' ? await maybeWrapWithHeadroom(rawModel) : rawModel;
|
|
135
223
|
const planSuffix = opts.planMode
|
|
136
224
|
? '\n\nPLAN MODE: สำรวจและวางแผนเท่านั้น — ห้ามแก้ไฟล์หรือรันคำสั่งที่เปลี่ยน state. จบด้วยแผนเป็นขั้นตอนให้ user อนุมัติก่อนลงมือ.'
|
|
137
225
|
: '';
|
|
@@ -142,7 +230,15 @@ export async function runAgent(opts) {
|
|
|
142
230
|
: '';
|
|
143
231
|
// static preamble (SYSTEM + memory + skills + brain) = เหมือนกันทุก step/turn → cache ได้ (ประหยัด ~10-20%)
|
|
144
232
|
// git แยกออก (volatile — เปลี่ยนทุก commit) ไม่ให้ invalidate cache ของ static prefix
|
|
145
|
-
const staticSystem = [
|
|
233
|
+
const staticSystem = [
|
|
234
|
+
SYSTEM + planSuffix + brainNudge,
|
|
235
|
+
personalityPrompt(config.personality),
|
|
236
|
+
autoMemory,
|
|
237
|
+
renderAvailableSkills(skills),
|
|
238
|
+
brain,
|
|
239
|
+
memory,
|
|
240
|
+
repoMap,
|
|
241
|
+
]
|
|
146
242
|
.filter(Boolean)
|
|
147
243
|
.join('\n\n');
|
|
148
244
|
// vision: อ่านรูปเป็น image part สำหรับ model. history เก็บแค่ placeholder (กัน session bloat / binary ใน JSON)
|
|
@@ -211,7 +307,8 @@ export async function runAgent(opts) {
|
|
|
211
307
|
abortSignal: opts.signal,
|
|
212
308
|
// งานยาว (tool calls เยอะ) → prune tool output เก่า กัน context บวม
|
|
213
309
|
prepareStep: ({ messages }) => {
|
|
214
|
-
const
|
|
310
|
+
const optimized = tuning.contextCompression === 'selective' ? selectivelyCompressStaleToolResults(messages) : messages;
|
|
311
|
+
const compacted = autoCompact(optimized, AUTO_COMPACT_TOKENS);
|
|
215
312
|
return compacted !== messages ? { messages: compacted } : {};
|
|
216
313
|
},
|
|
217
314
|
onStepFinish: ({ usage, providerMetadata }) => {
|
|
@@ -265,10 +362,12 @@ export async function runAgent(opts) {
|
|
|
265
362
|
};
|
|
266
363
|
let { text, result, err: streamError } = await runWithRetry(model);
|
|
267
364
|
// model หลักล้มกลางทาง (ไม่ใช่ rate-limit ที่ retry หมดแล้ว) → ลอง fallback model
|
|
268
|
-
|
|
365
|
+
// ต้อง text === '' ด้วย (เหมือน rate-limit retry) — ถ้า primary stream ออกไปบางส่วนแล้ว ค่อยล้ม
|
|
366
|
+
// การ fallback จะ stream คำตอบใหม่ทับ = output ซ้ำ/เพี้ยน + history desync → ไม่ fallback ถ้ามี text แล้ว
|
|
367
|
+
if (streamError && text === '' && opts.fallbackModel && opts.fallbackModel !== opts.model && !sideEffectToolSeen) {
|
|
269
368
|
opts.onEvent?.({ type: 'text', text: `\n[model หลักล้ม → fallback: ${opts.fallbackModel}]\n` });
|
|
270
369
|
// meter ใหม่ใช้ pricing ของ fallback แต่ merge usage/cost ของ primary เข้าด้วย (กัน cost หาย + budget นับต่อ)
|
|
271
|
-
const fallbackMeter = new CostMeter(specKey(opts.fallbackModel), opts.budgetUsd);
|
|
370
|
+
const fallbackMeter = new CostMeter(specKey(opts.fallbackModel), opts.budgetUsd, sharedBudget);
|
|
272
371
|
fallbackMeter.merge(meter);
|
|
273
372
|
meter = fallbackMeter;
|
|
274
373
|
({ text, result, err: streamError } = await runWithRetry(resolveModel(opts.fallbackModel)));
|
package/dist/lsp/index.js
CHANGED
|
@@ -17,7 +17,12 @@ import { getRepoRoot } from '../worktree.js';
|
|
|
17
17
|
import { encode, LspDecoder } from './framing.js';
|
|
18
18
|
import { LspSession, waitForDiagnostics } from './client.js';
|
|
19
19
|
import { resolveServer } from './servers.js';
|
|
20
|
-
|
|
20
|
+
// รวม Windows-critical (SystemRoot/windir/PATHEXT/ComSpec/USERPROFILE/LOCALAPPDATA/TMP) —
|
|
21
|
+
// ถ้าขาด SystemRoot/PATHEXT โปรเซสลูกบน Windows มัก spawn ไม่ขึ้น/หา .cmd ไม่เจอ
|
|
22
|
+
const SAFE_ENV_KEYS = [
|
|
23
|
+
'PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR',
|
|
24
|
+
'APPDATA', 'LOCALAPPDATA', 'USERPROFILE', 'SystemRoot', 'SystemDrive', 'windir', 'PATHEXT', 'ComSpec',
|
|
25
|
+
];
|
|
21
26
|
function safeEnv() {
|
|
22
27
|
const out = {};
|
|
23
28
|
for (const k of SAFE_ENV_KEYS) {
|
|
@@ -29,7 +34,8 @@ function safeEnv() {
|
|
|
29
34
|
}
|
|
30
35
|
/** real stdio transport: spawn the server, frame with Content-Length both ways. */
|
|
31
36
|
function spawnTransport(binPath, args, cwd) {
|
|
32
|
-
|
|
37
|
+
// Windows: LSP bin มัก resolve เป็น .cmd shim → spawn ตรงไม่ขึ้น ต้อง shell
|
|
38
|
+
const proc = spawn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: safeEnv(), shell: process.platform === 'win32' });
|
|
33
39
|
const decoder = new LspDecoder();
|
|
34
40
|
let handler = null;
|
|
35
41
|
proc.stdout?.on('data', (buf) => {
|
|
@@ -82,15 +88,21 @@ export async function diagnose(filePath, opts = {}) {
|
|
|
82
88
|
hookExitOnce();
|
|
83
89
|
let pooled = pool.get(key);
|
|
84
90
|
if (!pooled) {
|
|
91
|
+
let proc;
|
|
85
92
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
93
|
+
const t = spawnTransport(resolved.binPath, resolved.def.args, cwd);
|
|
94
|
+
proc = t.proc;
|
|
95
|
+
const session = new LspSession(t.transport);
|
|
88
96
|
let died = false;
|
|
89
97
|
proc.on('exit', () => {
|
|
90
98
|
died = true;
|
|
91
99
|
pool.delete(key);
|
|
92
100
|
});
|
|
93
|
-
|
|
101
|
+
// timeout: server ที่ค้าง (ไม่ตอบ initialize) ไม่ทำ diagnose แฮงค์ + reject → catch kill child กัน leak
|
|
102
|
+
await Promise.race([
|
|
103
|
+
session.initialize(rootUri),
|
|
104
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('initialize timeout (8s)')), 8000)),
|
|
105
|
+
]);
|
|
94
106
|
if (died)
|
|
95
107
|
return { ok: false, reason: `${resolved.def.command} ออกก่อนเริ่มงาน (ติดตั้งครบไหม?)` };
|
|
96
108
|
pooled = { session, proc, opened: new Map() };
|
|
@@ -98,6 +110,12 @@ export async function diagnose(filePath, opts = {}) {
|
|
|
98
110
|
}
|
|
99
111
|
catch (e) {
|
|
100
112
|
pool.delete(key);
|
|
113
|
+
try {
|
|
114
|
+
proc?.kill(); // init ล้ม/timeout → kill child + ปิด stdio pipes กัน orphan/leak
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* already dead */
|
|
118
|
+
}
|
|
101
119
|
return { ok: false, reason: `เริ่ม ${resolved.def.command} ไม่สำเร็จ: ${e.message}` };
|
|
102
120
|
}
|
|
103
121
|
}
|