sanook-cli 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const TEMPLATE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', 'second-brain', 'Templates', 'project-workspace');
|
|
5
|
+
const WORKSPACE_FILES = ['_Index.md', 'overview.md', 'current-state.md', 'context.md', 'repo.md'];
|
|
6
|
+
export function slugifyProject(value) {
|
|
7
|
+
const slug = value
|
|
8
|
+
.normalize('NFKD')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^\p{Letter}\p{Number}]+/gu, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
.slice(0, 80)
|
|
13
|
+
.replace(/-+$/g, '');
|
|
14
|
+
return slug || 'project';
|
|
15
|
+
}
|
|
16
|
+
function renderTemplate(raw, vars) {
|
|
17
|
+
let out = raw;
|
|
18
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
19
|
+
out = out.replaceAll(`{{${key}}}`, value);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
async function readTemplate(name) {
|
|
24
|
+
return readFile(join(TEMPLATE_ROOT, name), 'utf8');
|
|
25
|
+
}
|
|
26
|
+
async function fileExists(path) {
|
|
27
|
+
try {
|
|
28
|
+
await readFile(path);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function maybeAppendProjectsIndex(brainPath, slug, title) {
|
|
36
|
+
const indexPath = join(brainPath, 'Projects', '_Index.md');
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = await readFile(indexPath, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const link = `[[Projects/${slug}/_Index]]`;
|
|
45
|
+
if (content.includes(link))
|
|
46
|
+
return false;
|
|
47
|
+
const line = `- ${link} — ${title}`;
|
|
48
|
+
const marker = 'up:: [[Home]]';
|
|
49
|
+
const next = content.includes(marker) ? content.replace(marker, `${line}\n\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
|
|
50
|
+
await writeFile(indexPath, next, 'utf8');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export async function scaffoldProjectWorkspace(options) {
|
|
54
|
+
const brainPath = resolve(options.brainPath);
|
|
55
|
+
const title = options.title.trim() || 'Project';
|
|
56
|
+
const slug = options.slug?.trim() || slugifyProject(title);
|
|
57
|
+
const relDir = `Projects/${slug}`;
|
|
58
|
+
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
|
59
|
+
const repoPath = options.repoPath?.trim() ?? '';
|
|
60
|
+
const verify = options.verify?.trim() ?? 'npm test && npm run typecheck';
|
|
61
|
+
const defaultBranch = options.defaultBranch?.trim() ?? 'main';
|
|
62
|
+
const created = [];
|
|
63
|
+
const skipped = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const vars = {
|
|
66
|
+
DATE: today,
|
|
67
|
+
TITLE: title,
|
|
68
|
+
SLUG: slug,
|
|
69
|
+
REPO_PATH: repoPath,
|
|
70
|
+
VERIFY: verify,
|
|
71
|
+
DEFAULT_BRANCH: defaultBranch,
|
|
72
|
+
};
|
|
73
|
+
for (const name of WORKSPACE_FILES) {
|
|
74
|
+
const rel = `${relDir}/${name}`;
|
|
75
|
+
const path = join(brainPath, rel);
|
|
76
|
+
if ((await fileExists(path)) && !options.force) {
|
|
77
|
+
skipped.push(rel);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
await mkdir(dirname(path), { recursive: true });
|
|
81
|
+
const raw = await readTemplate(name);
|
|
82
|
+
await writeFile(path, renderTemplate(raw, vars), 'utf8');
|
|
83
|
+
created.push(rel);
|
|
84
|
+
}
|
|
85
|
+
if (!created.length && skipped.length) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
brainPath,
|
|
89
|
+
slug,
|
|
90
|
+
title,
|
|
91
|
+
relDir,
|
|
92
|
+
created,
|
|
93
|
+
skipped,
|
|
94
|
+
indexed: false,
|
|
95
|
+
warnings: ['Project workspace already exists. Re-run with --force to overwrite scaffold files.'],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const indexed = await maybeAppendProjectsIndex(brainPath, slug, title);
|
|
99
|
+
if (!indexed)
|
|
100
|
+
warnings.push('Projects/_Index.md was not updated (missing or link already present).');
|
|
101
|
+
return { ok: true, brainPath, slug, title, relDir, created, skipped, indexed, warnings };
|
|
102
|
+
}
|
|
103
|
+
export function formatScaffoldProjectReport(report) {
|
|
104
|
+
const lines = ['Sanook brain new project (workspace scaffold)'];
|
|
105
|
+
lines.push(`vault: ${report.brainPath}`);
|
|
106
|
+
lines.push(`slug: ${report.slug}`);
|
|
107
|
+
lines.push(`title: ${report.title}`);
|
|
108
|
+
lines.push(`dir: ${report.relDir}/`);
|
|
109
|
+
if (report.created.length) {
|
|
110
|
+
lines.push(`created (${report.created.length}):`);
|
|
111
|
+
for (const rel of report.created)
|
|
112
|
+
lines.push(` ${rel}`);
|
|
113
|
+
}
|
|
114
|
+
if (report.skipped.length) {
|
|
115
|
+
lines.push(`skipped (${report.skipped.length}):`);
|
|
116
|
+
for (const rel of report.skipped)
|
|
117
|
+
lines.push(` ${rel}`);
|
|
118
|
+
}
|
|
119
|
+
if (report.indexed)
|
|
120
|
+
lines.push('index: Projects/_Index.md updated');
|
|
121
|
+
for (const warning of report.warnings)
|
|
122
|
+
lines.push(`warning: ${warning}`);
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { BRAND } from './brand.js';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { gitContext } from './git.js';
|
|
4
|
+
import { SYSTEM } from './loop.js';
|
|
5
|
+
import { loadAutoMemory, loadBrainContext, loadMemory } from './memory.js';
|
|
6
|
+
import { personalityPrompt } from './personality.js';
|
|
7
|
+
import { loadRepoMap } from './repomap.js';
|
|
8
|
+
import { loadSkills, renderAvailableSkills } from './skills.js';
|
|
9
|
+
import { tools as builtInTools } from './tools/index.js';
|
|
10
|
+
const CHARS_PER_TOKEN = 4;
|
|
11
|
+
export function approximateTokens(chars) {
|
|
12
|
+
return chars <= 0 ? 0 : Math.ceil(chars / CHARS_PER_TOKEN);
|
|
13
|
+
}
|
|
14
|
+
function utf8Bytes(text) {
|
|
15
|
+
return Buffer.byteLength(text, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
export function measurePromptSection(id, label, text) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
label,
|
|
21
|
+
chars: text.length,
|
|
22
|
+
bytes: utf8Bytes(text),
|
|
23
|
+
approxTokens: approximateTokens(text.length),
|
|
24
|
+
empty: text.length === 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function joinPromptBlocks(blocks) {
|
|
28
|
+
return blocks.filter(Boolean).join('\n\n');
|
|
29
|
+
}
|
|
30
|
+
function toJsonSafe(value, options = {}, depth = 0, seen = new WeakSet()) {
|
|
31
|
+
const maxDepth = options.maxDepth ?? 6;
|
|
32
|
+
const maxStringLength = options.maxStringLength ?? 2_000;
|
|
33
|
+
if (value == null)
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof value === 'string')
|
|
36
|
+
return value.length > maxStringLength ? `${value.slice(0, maxStringLength)}...[truncated]` : value;
|
|
37
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
38
|
+
return value;
|
|
39
|
+
if (typeof value === 'bigint')
|
|
40
|
+
return value.toString();
|
|
41
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'undefined')
|
|
42
|
+
return undefined;
|
|
43
|
+
if (depth >= maxDepth)
|
|
44
|
+
return '[MaxDepth]';
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return value.map((item) => toJsonSafe(item, options, depth + 1, seen));
|
|
47
|
+
if (typeof value !== 'object')
|
|
48
|
+
return String(value);
|
|
49
|
+
if (seen.has(value))
|
|
50
|
+
return '[Circular]';
|
|
51
|
+
seen.add(value);
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [key, child] of Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) {
|
|
54
|
+
if (key === 'execute' || key === 'experimental_toToolResultContent')
|
|
55
|
+
continue;
|
|
56
|
+
const safe = toJsonSafe(child, options, depth + 1, seen);
|
|
57
|
+
if (safe !== undefined)
|
|
58
|
+
out[key] = safe;
|
|
59
|
+
}
|
|
60
|
+
seen.delete(value);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
export function serializeToolSchemas(tools) {
|
|
64
|
+
const payload = Object.entries(tools)
|
|
65
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
66
|
+
.map(([name, tool]) => {
|
|
67
|
+
const t = tool;
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
description: typeof t.description === 'string' ? t.description : '',
|
|
71
|
+
inputSchema: toJsonSafe(t.inputSchema ?? t.parameters),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
return JSON.stringify(payload, null, 2);
|
|
75
|
+
}
|
|
76
|
+
export async function buildPromptSizeBreakdown(options = {}) {
|
|
77
|
+
const cwd = options.cwd ?? process.cwd();
|
|
78
|
+
const planMode = options.planMode ?? false;
|
|
79
|
+
const [config, memory, autoMemory, skills, git, brain, repoMap,] = await Promise.all([
|
|
80
|
+
(options.loadConfigImpl ?? loadConfig)({}, cwd),
|
|
81
|
+
(options.loadMemoryImpl ?? loadMemory)(cwd),
|
|
82
|
+
(options.loadAutoMemoryImpl ?? loadAutoMemory)(),
|
|
83
|
+
(options.loadSkillsImpl ?? loadSkills)(cwd),
|
|
84
|
+
(options.gitContextImpl ?? gitContext)(cwd),
|
|
85
|
+
(options.loadBrainContextImpl ?? loadBrainContext)(),
|
|
86
|
+
(options.loadRepoMapImpl ?? loadRepoMap)(cwd),
|
|
87
|
+
]);
|
|
88
|
+
const planSuffix = planMode
|
|
89
|
+
? '\n\nPLAN MODE: สำรวจและวางแผนเท่านั้น — ห้ามแก้ไฟล์หรือรันคำสั่งที่เปลี่ยน state. จบด้วยแผนเป็นขั้นตอนให้ user อนุมัติก่อนลงมือ.'
|
|
90
|
+
: '';
|
|
91
|
+
const brainNudge = brain
|
|
92
|
+
? '\n- second-brain vault โหลดอยู่ (ดู <brain_vault>) — อ่าน current-state + โน้ตที่เกี่ยวก่อนงานไม่ trivial · เจอ preference/decision สำคัญ → remember (เข้า vault) · งานเสร็จควร route/บันทึกตาม Vault Structure Map ของ vault'
|
|
93
|
+
: '';
|
|
94
|
+
const baseSystem = SYSTEM + planSuffix + brainNudge;
|
|
95
|
+
const personality = personalityPrompt(config.personality);
|
|
96
|
+
const skillsBlock = renderAvailableSkills(skills);
|
|
97
|
+
const staticSystem = joinPromptBlocks([baseSystem, personality, autoMemory, skillsBlock, brain, memory, repoMap]);
|
|
98
|
+
const systemPromptText = joinPromptBlocks([staticSystem, git]);
|
|
99
|
+
const toolSchemaText = serializeToolSchemas(options.tools ?? builtInTools);
|
|
100
|
+
const sections = [
|
|
101
|
+
measurePromptSection('base-system', 'Base system', baseSystem),
|
|
102
|
+
measurePromptSection('personality', 'Personality overlay', personality),
|
|
103
|
+
measurePromptSection('auto-memory', 'Auto memory', autoMemory),
|
|
104
|
+
measurePromptSection('skills-index', 'Skills index', skillsBlock),
|
|
105
|
+
measurePromptSection('brain-context', 'Second-brain context', brain),
|
|
106
|
+
measurePromptSection('project-memory', 'Project memory', memory),
|
|
107
|
+
measurePromptSection('repo-map', 'Repo map', repoMap),
|
|
108
|
+
measurePromptSection('git-context', 'Git context', git),
|
|
109
|
+
];
|
|
110
|
+
const systemPrompt = measurePromptSection('system-prompt', 'System prompt total', systemPromptText);
|
|
111
|
+
const toolSchemas = measurePromptSection('tool-schemas', 'Built-in tool schemas', toolSchemaText);
|
|
112
|
+
const totalText = `${systemPromptText}\n\n${toolSchemaText}`;
|
|
113
|
+
return {
|
|
114
|
+
cwd,
|
|
115
|
+
model: config.model,
|
|
116
|
+
planMode,
|
|
117
|
+
skillsCount: skills.length,
|
|
118
|
+
builtInToolsCount: Object.keys(options.tools ?? builtInTools).length,
|
|
119
|
+
sections,
|
|
120
|
+
systemPrompt,
|
|
121
|
+
toolSchemas,
|
|
122
|
+
total: measurePromptSection('total-fixed-payload', 'Total fixed payload', totalText),
|
|
123
|
+
notes: [
|
|
124
|
+
'Counts are approximate; model tokenizers vary.',
|
|
125
|
+
'MCP tools are intentionally not spawned here. Use `sanook mcp list --tools` for live MCP catalog details.',
|
|
126
|
+
'The runtime sends git context as a separate system message so the static prompt cache stays useful.',
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function formatNumber(n) {
|
|
131
|
+
return new Intl.NumberFormat('en-US').format(n);
|
|
132
|
+
}
|
|
133
|
+
function formatSection(section) {
|
|
134
|
+
const empty = section.empty ? ' (empty)' : '';
|
|
135
|
+
return `${section.label.padEnd(22)} ${formatNumber(section.chars).padStart(8)} chars ~${formatNumber(section.approxTokens).padStart(6)} tokens ${formatNumber(section.bytes).padStart(8)} bytes${empty}`;
|
|
136
|
+
}
|
|
137
|
+
export function renderPromptSizeBreakdown(report) {
|
|
138
|
+
const lines = [
|
|
139
|
+
`${BRAND.productName} prompt-size`,
|
|
140
|
+
`cwd: ${report.cwd}`,
|
|
141
|
+
`model: ${report.model}${report.planMode ? ' plan-mode: on' : ''}`,
|
|
142
|
+
`skills: ${report.skillsCount} built-in tools: ${report.builtInToolsCount}`,
|
|
143
|
+
'',
|
|
144
|
+
formatSection(report.systemPrompt),
|
|
145
|
+
formatSection(report.toolSchemas),
|
|
146
|
+
formatSection(report.total),
|
|
147
|
+
'',
|
|
148
|
+
'Breakdown:',
|
|
149
|
+
...report.sections.map((section) => ` ${formatSection(section)}`),
|
|
150
|
+
'',
|
|
151
|
+
'Notes:',
|
|
152
|
+
...report.notes.map((note) => ` - ${note}`),
|
|
153
|
+
];
|
|
154
|
+
return `${lines.join('\n')}\n`;
|
|
155
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { codexHome } from './codex.js';
|
|
4
|
+
/** OpenAI Codex OAuth client id (same public client as Codex CLI / Hermes). */
|
|
5
|
+
export const CODEX_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
6
|
+
const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
|
|
7
|
+
const CODEX_OAUTH_TOKEN_URL = `${CODEX_OAUTH_ISSUER}/oauth/token`;
|
|
8
|
+
export const CODEX_DEVICE_VERIFY_URL = `${CODEX_OAUTH_ISSUER}/codex/device`;
|
|
9
|
+
function parseRetryAfterSeconds(headers) {
|
|
10
|
+
const raw = headers?.get('retry-after')?.trim();
|
|
11
|
+
if (!raw)
|
|
12
|
+
return undefined;
|
|
13
|
+
const seconds = Number(raw);
|
|
14
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
15
|
+
return Math.floor(seconds);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
async function postJson(url, body, fetchImpl) {
|
|
19
|
+
return fetchImpl(url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Step 1 — request device code (Hermes / Codex CLI compatible). */
|
|
26
|
+
export async function requestCodexDeviceCode(fetchImpl = fetch) {
|
|
27
|
+
const maxAttempts = 4;
|
|
28
|
+
let resp;
|
|
29
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
30
|
+
resp = await postJson(`${CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/usercode`, { client_id: CODEX_OAUTH_CLIENT_ID }, fetchImpl);
|
|
31
|
+
if (resp.status !== 429)
|
|
32
|
+
break;
|
|
33
|
+
if (attempt < maxAttempts) {
|
|
34
|
+
const retryAfter = parseRetryAfterSeconds(resp.headers) ?? 2 ** attempt;
|
|
35
|
+
await new Promise((r) => setTimeout(r, Math.max(1000, Math.min(retryAfter * 1000, 60_000))));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!resp || resp.status === 429) {
|
|
39
|
+
throw new Error('OpenAI จำกัดการ login ชั่วคราว (429) — รอ 1 นาทีแล้วลองใหม่');
|
|
40
|
+
}
|
|
41
|
+
if (!resp.ok) {
|
|
42
|
+
throw new Error(`ขอ device code ไม่สำเร็จ (HTTP ${resp.status})`);
|
|
43
|
+
}
|
|
44
|
+
const data = (await resp.json());
|
|
45
|
+
const userCode = data.user_code?.trim();
|
|
46
|
+
const deviceAuthId = data.device_auth_id?.trim();
|
|
47
|
+
if (!userCode || !deviceAuthId)
|
|
48
|
+
throw new Error('OpenAI ตอบ device code ไม่ครบ');
|
|
49
|
+
const pollIntervalMs = Math.max(3000, Number(data.interval ?? 5) * 1000);
|
|
50
|
+
return { userCode, deviceAuthId, pollIntervalMs };
|
|
51
|
+
}
|
|
52
|
+
/** Step 2 — poll until the user completes browser login. */
|
|
53
|
+
export async function pollCodexDeviceCode(session, opts = {}) {
|
|
54
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
55
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
56
|
+
const deadline = Date.now() + (opts.maxWaitMs ?? 15 * 60_000);
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
if (opts.signal?.aborted)
|
|
59
|
+
throw new Error('ยกเลิก login แล้ว');
|
|
60
|
+
await sleep(session.pollIntervalMs);
|
|
61
|
+
if (opts.signal?.aborted)
|
|
62
|
+
throw new Error('ยกเลิก login แล้ว');
|
|
63
|
+
const pollResp = await postJson(`${CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/token`, { device_auth_id: session.deviceAuthId, user_code: session.userCode }, fetchImpl);
|
|
64
|
+
if (pollResp.status === 200) {
|
|
65
|
+
const payload = (await pollResp.json());
|
|
66
|
+
const authorization_code = payload.authorization_code?.trim();
|
|
67
|
+
const code_verifier = payload.code_verifier?.trim();
|
|
68
|
+
if (!authorization_code || !code_verifier)
|
|
69
|
+
throw new Error('OpenAI ตอบ authorization code ไม่ครบ');
|
|
70
|
+
return { authorization_code, code_verifier };
|
|
71
|
+
}
|
|
72
|
+
if (pollResp.status === 403 || pollResp.status === 404)
|
|
73
|
+
continue;
|
|
74
|
+
throw new Error(`รอ login ไม่สำเร็จ (HTTP ${pollResp.status})`);
|
|
75
|
+
}
|
|
76
|
+
throw new Error('หมดเวลารอ login (15 นาที) — ลองใหม่');
|
|
77
|
+
}
|
|
78
|
+
/** Step 3 — exchange authorization code for tokens. */
|
|
79
|
+
export async function exchangeCodexDeviceCode(exchange, fetchImpl = fetch) {
|
|
80
|
+
const body = new URLSearchParams({
|
|
81
|
+
grant_type: 'authorization_code',
|
|
82
|
+
code: exchange.authorization_code,
|
|
83
|
+
redirect_uri: `${CODEX_OAUTH_ISSUER}/deviceauth/callback`,
|
|
84
|
+
client_id: CODEX_OAUTH_CLIENT_ID,
|
|
85
|
+
code_verifier: exchange.code_verifier,
|
|
86
|
+
});
|
|
87
|
+
const resp = await fetchImpl(CODEX_OAUTH_TOKEN_URL, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
90
|
+
body,
|
|
91
|
+
});
|
|
92
|
+
if (resp.status === 429)
|
|
93
|
+
throw new Error('OpenAI จำกัดการ login ชั่วคราว (429) — รอแล้วลองใหม่');
|
|
94
|
+
if (!resp.ok)
|
|
95
|
+
throw new Error(`แลก token ไม่สำเร็จ (HTTP ${resp.status})`);
|
|
96
|
+
const tokens = (await resp.json());
|
|
97
|
+
const access_token = tokens.access_token?.trim();
|
|
98
|
+
const refresh_token = tokens.refresh_token?.trim();
|
|
99
|
+
if (!access_token || !refresh_token)
|
|
100
|
+
throw new Error('OpenAI ไม่ส่ง access/refresh token');
|
|
101
|
+
return {
|
|
102
|
+
access_token,
|
|
103
|
+
refresh_token,
|
|
104
|
+
id_token: tokens.id_token?.trim() || undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Persist ChatGPT-plan credentials where the official Codex CLI expects them. */
|
|
108
|
+
export async function saveCodexAuthFile(tokens, home = codexHome()) {
|
|
109
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
110
|
+
const authPath = join(home, 'auth.json');
|
|
111
|
+
const payload = {
|
|
112
|
+
auth_mode: 'chatgpt',
|
|
113
|
+
tokens: {
|
|
114
|
+
access_token: tokens.access_token,
|
|
115
|
+
refresh_token: tokens.refresh_token,
|
|
116
|
+
...(tokens.id_token ? { id_token: tokens.id_token } : {}),
|
|
117
|
+
},
|
|
118
|
+
last_refresh: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
await writeFile(authPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
121
|
+
return authPath;
|
|
122
|
+
}
|
|
123
|
+
/** Full Hermes-style device-code login → ~/.codex/auth.json (Codex CLI can reuse). */
|
|
124
|
+
export async function runCodexDeviceCodeLogin(opts = {}) {
|
|
125
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
126
|
+
const onStatus = opts.onStatus ?? (() => { });
|
|
127
|
+
onStatus('requesting');
|
|
128
|
+
const session = await requestCodexDeviceCode(fetchImpl);
|
|
129
|
+
onStatus(`code:${session.userCode}`);
|
|
130
|
+
onStatus('waiting');
|
|
131
|
+
const exchange = await pollCodexDeviceCode(session, opts);
|
|
132
|
+
onStatus('exchanging');
|
|
133
|
+
const tokens = await exchangeCodexDeviceCode(exchange, fetchImpl);
|
|
134
|
+
onStatus('saving');
|
|
135
|
+
const authPath = await saveCodexAuthFile(tokens);
|
|
136
|
+
onStatus('done');
|
|
137
|
+
return authPath;
|
|
138
|
+
}
|
package/dist/providers/codex.js
CHANGED
|
@@ -2,8 +2,21 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
export function codexHome() {
|
|
6
|
+
return process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
|
|
7
|
+
}
|
|
8
|
+
async function readCodexLoggedIn() {
|
|
9
|
+
try {
|
|
10
|
+
const auth = JSON.parse(await readFile(join(codexHome(), 'auth.json'), 'utf8'));
|
|
11
|
+
return auth?.auth_mode === 'chatgpt' || Boolean(auth?.tokens?.access_token);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
/** เช็กว่า codex CLI ติดตั้ง + login ChatGPT แล้ว */
|
|
6
18
|
export async function detectCodex() {
|
|
19
|
+
const loggedIn = await readCodexLoggedIn();
|
|
7
20
|
const hasBinary = await new Promise((resolve) => {
|
|
8
21
|
const p = spawn('codex', ['--version'], { shell: process.platform === 'win32' });
|
|
9
22
|
// timeout: binary ค้าง (shim รอ stdin / Gatekeeper stall ตอนรันครั้งแรกบน macOS) → ไม่ให้ wizard ตัน
|
|
@@ -21,24 +34,26 @@ export async function detectCodex() {
|
|
|
21
34
|
});
|
|
22
35
|
});
|
|
23
36
|
if (!hasBinary) {
|
|
24
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
installed: false,
|
|
39
|
+
loggedIn,
|
|
40
|
+
reason: loggedIn
|
|
41
|
+
? 'login แล้ว แต่ยังไม่มี codex CLI — ติดตั้ง: npm i -g @openai/codex'
|
|
42
|
+
: 'ไม่พบ codex CLI — ติดตั้ง: npm i -g @openai/codex',
|
|
43
|
+
};
|
|
25
44
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const loggedIn = auth?.auth_mode === 'chatgpt' || Boolean(auth?.tokens?.access_token);
|
|
29
|
-
return { installed: true, loggedIn, reason: loggedIn ? undefined : 'ยังไม่ได้ login — รัน: codex login' };
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return { installed: true, loggedIn: false, reason: 'ยังไม่ได้ login — รัน: codex login' };
|
|
45
|
+
if (loggedIn) {
|
|
46
|
+
return { installed: true, loggedIn: true, reason: undefined };
|
|
33
47
|
}
|
|
48
|
+
return { installed: true, loggedIn: false, reason: 'ยังไม่ได้ login — รัน: codex login' };
|
|
34
49
|
}
|
|
35
50
|
/**
|
|
36
51
|
* รัน `codex exec` แบบ non-interactive — ส่ง prompt ทาง stdin, parse JSONL events
|
|
37
52
|
* tolerant ต่อ malformed JSONL (codex bug #15451: --json ถูก ignore เมื่อมี tools active)
|
|
38
53
|
*/
|
|
39
54
|
export async function runCodex(opts) {
|
|
40
|
-
//
|
|
41
|
-
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--
|
|
55
|
+
// `codex exec` is already non-interactive; sandbox controls whether generated commands may write.
|
|
56
|
+
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--json'];
|
|
42
57
|
if (opts.model)
|
|
43
58
|
args.push('-m', opts.model);
|
|
44
59
|
if (opts.resumeThreadId)
|
|
@@ -49,52 +64,83 @@ export async function runCodex(opts) {
|
|
|
49
64
|
// (codex bug #2733/#3286: ตั้ง OPENAI_API_KEY ค้าง env ทำให้ ChatGPT-plan auth วน loop sign-in)
|
|
50
65
|
const env = { ...process.env };
|
|
51
66
|
delete env.OPENAI_API_KEY;
|
|
52
|
-
const p = spawn('codex', args, { env, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
67
|
+
const p = spawn('codex', args, { env, cwd: opts.cwd, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
53
68
|
let finalText = '';
|
|
54
69
|
let threadId;
|
|
55
70
|
let buf = '';
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
let stderr = '';
|
|
72
|
+
let aborted = false;
|
|
73
|
+
const handleStdoutLine = (line) => {
|
|
74
|
+
const t = line.trim();
|
|
75
|
+
if (!t)
|
|
76
|
+
return;
|
|
77
|
+
if (!t.startsWith('{')) {
|
|
78
|
+
// plain stdout fallback (JSONL ถูก ignore) — เก็บเป็น final text
|
|
79
|
+
finalText += (finalText ? '\n' : '') + t;
|
|
80
|
+
opts.onEvent?.({ type: 'text', text: finalText });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const ev = JSON.parse(t);
|
|
85
|
+
if (ev.type === 'thread.started' && ev.thread_id) {
|
|
86
|
+
threadId = ev.thread_id;
|
|
87
|
+
opts.onEvent?.({ type: 'thread', threadId });
|
|
88
|
+
}
|
|
89
|
+
else if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') {
|
|
90
|
+
finalText = ev.item.text ?? finalText;
|
|
91
|
+
opts.onEvent?.({ type: 'text', text: finalText });
|
|
92
|
+
}
|
|
93
|
+
else if (ev.type === 'turn.completed') {
|
|
94
|
+
opts.onEvent?.({ type: 'usage', usage: ev.usage });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// malformed JSON line — ข้าม
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const abortHandler = () => {
|
|
102
|
+
aborted = true;
|
|
103
|
+
p.kill();
|
|
104
|
+
};
|
|
105
|
+
const cleanupAbortHandler = () => opts.signal?.removeEventListener('abort', abortHandler);
|
|
106
|
+
// codex อาจตายระหว่างรับ prompt → write ลง stdin ที่ปิดแล้ว = EPIPE; ถ้าไม่ดัก = crash ทั้ง CLI
|
|
107
|
+
// (close handler ด้านล่าง reject error ที่อ่านรู้เรื่องแทน)
|
|
108
|
+
p.stdin.on('error', () => { });
|
|
109
|
+
if (opts.signal?.aborted)
|
|
110
|
+
abortHandler();
|
|
111
|
+
else
|
|
112
|
+
opts.signal?.addEventListener('abort', abortHandler, { once: true });
|
|
113
|
+
if (!aborted) {
|
|
114
|
+
p.stdin.write(opts.prompt);
|
|
115
|
+
p.stdin.end();
|
|
116
|
+
}
|
|
59
117
|
p.stdout.on('data', (chunk) => {
|
|
60
118
|
buf += chunk.toString();
|
|
61
119
|
const lines = buf.split('\n');
|
|
62
120
|
buf = lines.pop() ?? '';
|
|
63
121
|
for (const line of lines) {
|
|
64
|
-
|
|
65
|
-
if (!t)
|
|
66
|
-
continue;
|
|
67
|
-
if (!t.startsWith('{')) {
|
|
68
|
-
// plain stdout fallback (JSONL ถูก ignore) — เก็บเป็น final text
|
|
69
|
-
finalText += (finalText ? '\n' : '') + t;
|
|
70
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const ev = JSON.parse(t);
|
|
75
|
-
if (ev.type === 'thread.started' && ev.thread_id) {
|
|
76
|
-
threadId = ev.thread_id;
|
|
77
|
-
opts.onEvent?.({ type: 'thread', threadId });
|
|
78
|
-
}
|
|
79
|
-
else if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') {
|
|
80
|
-
finalText = ev.item.text ?? finalText;
|
|
81
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
82
|
-
}
|
|
83
|
-
else if (ev.type === 'turn.completed') {
|
|
84
|
-
opts.onEvent?.({ type: 'usage', usage: ev.usage });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// malformed JSON line — ข้าม
|
|
89
|
-
}
|
|
122
|
+
handleStdoutLine(line);
|
|
90
123
|
}
|
|
91
124
|
});
|
|
92
|
-
p.on('
|
|
125
|
+
p.stderr.on('data', (chunk) => {
|
|
126
|
+
stderr += chunk.toString();
|
|
127
|
+
if (stderr.length > 4000)
|
|
128
|
+
stderr = stderr.slice(-4000);
|
|
129
|
+
});
|
|
130
|
+
p.on('error', (err) => {
|
|
131
|
+
cleanupAbortHandler();
|
|
132
|
+
reject(new Error(`เรียก codex ไม่ได้: ${err.message}`));
|
|
133
|
+
});
|
|
93
134
|
p.on('close', (code) => {
|
|
94
|
-
|
|
135
|
+
cleanupAbortHandler();
|
|
136
|
+
handleStdoutLine(buf);
|
|
137
|
+
buf = '';
|
|
138
|
+
if (aborted)
|
|
139
|
+
reject(new Error(`codex exec ถูกยกเลิก${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
140
|
+
else if (code === 0)
|
|
95
141
|
resolve({ text: finalText.trim(), threadId });
|
|
96
142
|
else
|
|
97
|
-
reject(new Error(`codex exec จบด้วย exit code ${code}`));
|
|
143
|
+
reject(new Error(`codex exec จบด้วย exit code ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
98
144
|
});
|
|
99
145
|
});
|
|
100
146
|
}
|
package/dist/providers/keys.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** อ่าน API key จาก env (หลัก + fallbacks) — keychain เป็น enhancement ทีหลัง */
|
|
9
9
|
export function resolveKeyFromEnv(envVar, fallbacks = []) {
|
|
10
10
|
for (const name of [envVar, ...fallbacks]) {
|
|
11
|
-
const v = process.env[name];
|
|
11
|
+
const v = process.env[name]?.trim();
|
|
12
12
|
if (v)
|
|
13
13
|
return v;
|
|
14
14
|
}
|
|
@@ -36,3 +36,24 @@ export function assertDirectApiKey(policy, key) {
|
|
|
36
36
|
export function redactKey(s) {
|
|
37
37
|
return s.replace(/\b(AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{6,}|AIza[A-Za-z0-9_-]{10,}|xai-[A-Za-z0-9]{10,}|gsk_[A-Za-z0-9]{10,}|[A-Za-z0-9_-]{24,})\b/g, (m) => (m.length > 8 ? `${m.slice(0, 4)}…${m.slice(-2)}` : '…'));
|
|
38
38
|
}
|
|
39
|
+
export function redactUnknown(value) {
|
|
40
|
+
const visiting = new WeakSet();
|
|
41
|
+
const visit = (current) => {
|
|
42
|
+
if (typeof current === 'string')
|
|
43
|
+
return redactKey(current);
|
|
44
|
+
if (!current || typeof current !== 'object')
|
|
45
|
+
return current;
|
|
46
|
+
if (visiting.has(current))
|
|
47
|
+
return '[Circular]';
|
|
48
|
+
visiting.add(current);
|
|
49
|
+
try {
|
|
50
|
+
if (Array.isArray(current))
|
|
51
|
+
return current.map(visit);
|
|
52
|
+
return Object.fromEntries(Object.entries(current).map(([k, v]) => [redactKey(k), visit(v)]));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
visiting.delete(current);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return visit(value);
|
|
59
|
+
}
|
package/dist/providers/models.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
|
|
|
22
22
|
.map((m) => (m.name ?? '').replace(/^models\//, ''))
|
|
23
23
|
.filter(Boolean);
|
|
24
24
|
}
|
|
25
|
-
const base = process.env[`${cfg.id.toUpperCase()}_BASE_URL`]
|
|
25
|
+
const base = process.env[`${cfg.id.toUpperCase()}_BASE_URL`]?.trim() || cfg.baseURL?.trim();
|
|
26
26
|
if (!base)
|
|
27
27
|
return []; // ไม่มี baseURL = ดึงไม่ได้
|
|
28
28
|
const headers = cfg.id === 'anthropic'
|
|
@@ -49,7 +49,7 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
|
|
|
49
49
|
*/
|
|
50
50
|
export function mergeModelOptions(cfg, remote = []) {
|
|
51
51
|
// group alias ทั้งหมดตาม id (รวม 'default' ด้วย — กัน id ที่มีแต่ alias 'default' เช่น lmstudio:local-model,
|
|
52
|
-
// ollama:
|
|
52
|
+
// ollama:llama3.3 หายไปจนเลือกไม่ได้/Select ว่าง). ตอนทำ label ค่อยซ่อนคำ "default" ถ้ามีชื่ออื่นอยู่แล้ว
|
|
53
53
|
const aliasesById = new Map();
|
|
54
54
|
const order = []; // คง first-seen order ของ id
|
|
55
55
|
for (const [alias, id] of Object.entries(cfg.models)) {
|