๐ cli.ts โข 27836 bytes
#!/usr/bin/env bun
/**
* CmdCode V0.5 - CLI ๅ
ฅๅฃ
*
* ็จๆณ๏ผ
* cmdcode # ไบคไบREPLๆจกๅผ
* cmdcode -p "ๆ็คบ่ฏ" # ๅๆฌกๆง่กๆจกๅผ
* cmdcode -p "ๆ็คบ" --continue # ็ปง็ปญไธๆฌกไผ่ฏ
* cmdcode --sessions # ๅๅบๆๆไผ่ฏ
* cmdcode -h # ๅธฎๅฉ
*/
import { parseArgs } from 'node:util'
import * as readline from 'node:readline'
import { t, initI18n } from './i18n.js'
import { createChatEngine, createChatEngineFromHistory, setGlobalPAVREnabled, isGlobalPAVREnabled } from './chat-factory.js'
import { loadConfig, updateAppConfig } from './config.js'
import { saveSession, loadSession, getLatestSessionId, listSessions } from './session.js'
import { BUILTIN_PROVIDERS, testConnection, type ModelProvider, type CustomModelConfig } from './models.js'
import {
// ๅฏ้ฅๆฑ ็ฎก็
loadChatKeyPool, addChatKey, removeChatKey,
loadEmbeddingKeyPool, addEmbeddingKey, removeEmbeddingKey,
// ็ฎๅๆฅๅฃ
saveKeys
} from './apikeys.js'
import {
register, login, validateToken, loadUserCache, clearUserCache,
restoreWorkspaceSnapshot,
} from './user.js'
import {
createUserModel, loadUserModel, updateUserModel, deleteUserModel,
listUserModels, getUserDefaultModel, setUserDefaultModel,
testUserModelConnection, countUserModels, type UserModelConfig
} from './user-models.js'
import { setUserWorkspace, setUsername } from './tools.js'
import { printHelp } from './commands/help.js'
import { interactiveModelSetup } from './commands/model.js'
import { countHistoryMessages, getDirSize, printBanner, restoreWorkspaceFromSnapshot } from './commands/workspace.js'
import { userAuthFlow } from './commands/auth.js'
import { replLoop } from './commands/repl.js'
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
import { isUsingFallbackKey } from './crypto-util.js'
// ๅ้่ฎฐๅฟ็ณป็ป
import {
initMemorySystem
} from './memory/memoryManager.js'
import { join } from 'node:path'
import { homedir } from 'node:os'
import { getSystemPrompt } from './chat.js'
import { SYSTEM_PROMPT_TEMPLATE } from './system-prompt.js'
const VERSION = '0.5.0'
const API_BASE = 'https://cmdcode.cn/xiaoc/cmdcode_api.php'
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ANSI ๆ ทๅผๅทฅๅ
ท
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
italic: '\x1b[3m',
// ๅๆฏ่ฒ
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
// ไบฎ่ฒ
brightBlack: '\x1b[90m',
brightRed: '\x1b[91m',
brightGreen: '\x1b[92m',
brightYellow: '\x1b[93m',
brightBlue: '\x1b[94m',
brightMagenta: '\x1b[95m',
brightCyan: '\x1b[96m',
brightWhite: '\x1b[97m',
// ่ๆฏ่ฒ
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m',
}
// ๆฃๆต็ป็ซฏๆฏๅฆๆฏๆ้ข่ฒ
const supportsColor = process.stdout.isTTY && (process.env.TERM !== 'dumb')
const color = supportsColor ? C : Object.fromEntries(Object.keys(C).map(k => [k, ''])) as typeof C
/** ๅ็ไธป้ข่ฒ - Claude Code ็ปๅ
ธๆฉ้ป้ฃๆ ผ๏ผๅ supportsColor ๆงๅถ๏ผ */
const BRAND = supportsColor ? '\x1b[38;5;208m' : '' // CmdCodeๅ็่ฒ - ๆ ๅๆฉ้ป
const BRAND_DIM = supportsColor ? '\x1b[38;5;214m' : '' // ไบฎๆฉ่ฒ
const ACCENT = supportsColor ? '\x1b[38;5;220m' : '' // ๅผบ่ฐ่ฒ - ้้ป
const MUTED = supportsColor ? '\x1b[38;5;240m' : '' // ๆฌก่ฆไฟกๆฏ - ๆทฑ็ฐ
const SUCCESS = color.brightGreen // ๆๅ
const ERROR = color.brightRed // ้่ฏฏ
const WARN = color.brightYellow // ่ญฆๅ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆพ็คบๅฝๆฐ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/** ่ฎก็ฎ็ฎๅฝๅคงๅฐ๏ผๅญ่๏ผ */
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ - ไผ่ฏ็บงๅญๅจ๏ผttyd ๆ X ๆพ็คบ็ฏๅข๏ผ */
let sessionClipboard = ''
// ๅฎๅ
จ๏ผ็งป่ณ็จๆทไธๅฑ็ฎๅฝ๏ผ้ฟๅ
ๅค็จๆทๅ
ฑไบซ/tmpๅฏผ่ดๆณ้ฒ
const CLIPBOARD_FILE = join(homedir(), '.cmdcode', 'clipboard')
// ็กฎไฟๅช่ดดๆฟๆไปถๆ้ไธบ600๏ผไป
็จๆทๅฏ่ฏปๅ๏ผ
function ensureClipboardSecure(): void {
try { chmodSync(CLIPBOARD_FILE, 0o600) } catch { /* ignore */ }
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๅฎๅ
จๅฏ้ฅๅบ็ฎก็
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/** ๅฎๅ
จๅฏ็ ่พๅ
ฅ๏ผ้่่พๅ
ฅ๏ผ */
async function askPassword(prompt: string): Promise<string> {
return new Promise((resolve) => {
process.stdout.write(prompt)
process.stdin.setRawMode(true)
let password = ''
process.stdin.on('data', (data: Buffer) => {
const char = data.toString('utf8')
if (char === '\n' || char === '\r' || char === '\u0004') {
process.stdin.setRawMode(false)
console.log('') // ๆข่ก
resolve(password)
} else if (char === '\u0003') {
process.stdin.setRawMode(false)
resolve('')
} else {
password += char
process.stdout.write('*')
}
})
})
}
async function copyToClipboard(text: string): Promise<boolean> {
try {
sessionClipboard = text
const { writeFileSync } = require('node:fs')
writeFileSync(CLIPBOARD_FILE, text, 'utf-8')
return true
} catch {
return false
}
}
async function pasteFromClipboard(): Promise<string> {
try {
// ไผๅ
ไฝฟ็จไผ่ฏๅช่ดดๆฟ
if (sessionClipboard) {
return sessionClipboard
}
// ๅฐ่ฏ่ฏปๅๆไปถ
const { existsSync, readFileSync } = require('node:fs')
if (existsSync(CLIPBOARD_FILE)) {
return readFileSync(CLIPBOARD_FILE, 'utf-8')
}
return ''
} catch {
return ''
}
}
/** REPL ่พๅ
ฅ - ไฝฟ็จๆ ๅ readline๏ผBun ๅ
ผๅฎน๏ผ */
function askREPL(prompt: string): Promise<{ input: string; action: 'submit' | 'exit' }> {
return new Promise((resolve) => {
const stdin = process.stdin
const stdout = process.stdout
const wasRaw = stdin.isRaw
// ่ฟๅ
ฅ raw ๆจกๅผ๏ผๅ
ณ้ญ่ก็ผๅฒ
if (typeof stdin.setRawMode === 'function') {
stdin.setRawMode(true)
}
readline.emitKeypressEvents(stdin)
let buffer = ''
let cursorPos = 0
const render = () => {
// ๆธ
้ค่กๅนถ้็ป
stdout.write('\r\x1b[K')
stdout.write(prompt + ' ' + buffer)
// ็งปๅจๅ
ๆ ๅฐๆญฃ็กฎไฝ็ฝฎ
if (cursorPos < buffer.length) {
stdout.write(`\x1b[${buffer.length - cursorPos}D`)
}
}
render()
// Issue 7: SIGWINCH - terminal resize auto-redraw
const onResize = () => render()
process.stdout.on('resize', onResize)
const cleanup = () => {
if (typeof stdin.setRawMode === 'function') {
stdin.setRawMode(wasRaw ?? false)
}
stdin.removeListener('keypress', onKeypress)
process.stdout.removeListener('resize', onResize)
stdout.write('\n')
}
const onKeypress = async (str: string, key: any) => {
// Ctrl+E (0x05) ้ๅบ
if (key.ctrl && key.name === 'e') {
cleanup()
resolve({ input: '', action: 'exit' })
return
}
// Ctrl+L ๆธ
ๅฑ
if (key.ctrl && key.name === 'l') {
stdout.write('\x1b[2J\x1b[H')
render()
return
}
// Ctrl+C (0x03) ๅคๅถ
if (key.ctrl && key.name === 'c') {
if (buffer.length > 0) {
await copyToClipboard(buffer)
// ๆพ็คบๆ็คบๅ่ชๅจๆธ
้ค๏ผไธ้ฎๆก่พๅ
ฅ่ก๏ผ
stdout.write(`\r\x1b[K${SUCCESS}โ ${t("clip.copied")}${color.reset}`)
setTimeout(() => {
stdout.write('\r\x1b[K')
render()
}, 1000)
}
return
}
// Ctrl+X (0x18) ๅชๅ
if (key.ctrl && key.name === 'x') {
if (buffer.length > 0) {
await copyToClipboard(buffer)
buffer = ''
cursorPos = 0
stdout.write(`\r\x1b[K${SUCCESS}โ ${t("clip.cut")}${color.reset}`)
setTimeout(() => {
stdout.write('\r\x1b[K')
render()
}, 1000)
}
return
}
// Ctrl+V (0x16) ็ฒ่ดด
if (key.ctrl && key.name === 'v') {
const pasted = await pasteFromClipboard()
if (pasted) {
const cleanPasted = pasted.replace(/[\r\n]+/g, ' ')
const before = buffer.slice(0, cursorPos)
const after = buffer.slice(cursorPos)
buffer = before + cleanPasted + after
cursorPos += cleanPasted.length
render()
}
return
}
// Enter ๆไบค
if (key.name === 'return' || key.name === 'enter') {
cleanup()
// ๆชๆญ่ถ
้ฟ่พๅ
ฅ๏ผ้ฒๆญข่ฏฏ็ฒ่ดด้ฟๆ่ถ
ๅบไธไธๆ
const limited = buffer.length > 8000 ? buffer.slice(0, 8000) : buffer
resolve({ input: limited.trim(), action: 'submit' })
return
}
// Backspace / Delete
if (key.name === 'backspace') {
if (cursorPos > 0) {
const before = buffer.slice(0, cursorPos - 1)
const after = buffer.slice(cursorPos)
buffer = before + after
cursorPos--
render()
}
return
}
if (key.name === 'delete') {
if (cursorPos < buffer.length) {
const before = buffer.slice(0, cursorPos)
const after = buffer.slice(cursorPos + 1)
buffer = before + after
render()
}
return
}
// ๅทฆๅณ็ฎญๅคด
if (key.name === 'left') {
if (cursorPos > 0) {
cursorPos--
render()
}
return
}
if (key.name === 'right') {
if (cursorPos < buffer.length) {
cursorPos++
render()
}
return
}
// Home / End
if (key.name === 'home') {
cursorPos = 0
render()
return
}
if (key.name === 'end') {
cursorPos = buffer.length
render()
return
}
// ๆฎ้ๅญ็ฌฆ
if (str && str.length === 1 && !key.ctrl && !key.meta) {
const before = buffer.slice(0, cursorPos)
const after = buffer.slice(cursorPos)
buffer = before + str + after
cursorPos++
render()
}
}
stdin.on('keypress', onKeypress)
})
}
/** readline ๅ่ก่พๅ
ฅๅทฅๅ
ท */
function askQuestion(query: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
rl.question(query, (answer) => {
rl.close()
resolve(answer.trim())
})
})
}
/** ๅฏ็ ่พๅ
ฅ๏ผ้่๏ผ */
function askPassword(query: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
if (process.stdout.isTTY) {
const stdin = process.stdin
const wasRaw = stdin.isRaw
if (wasRaw !== undefined) stdin.setRawMode?.(true)
process.stdout.write(query)
let password = ''
const onData = (char: Buffer) => {
const c = char.toString()
if (c === '\n' || c === '\r') {
if (wasRaw !== undefined) stdin.setRawMode?.(wasRaw)
stdin.removeListener('data', onData)
rl.close()
process.stdout.write('\n')
resolve(password)
} else if (c === '\u007F' || c === '\b') {
if (password.length > 0) {
password = password.slice(0, -1)
process.stdout.write('\b \b')
}
} else if (c === '\u0003') {
// Ctrl+C - ๅๆถๅฏ็ ่พๅ
ฅ๏ผ่ฟๅ็ฉบๅญ็ฌฆไธฒ
if (wasRaw !== undefined) stdin.setRawMode?.(wasRaw)
stdin.removeListener('data', onData)
rl.close()
process.stdout.write('\n')
resolve('')
} else {
password += c
process.stdout.write('*')
}
}
stdin.on('data', onData)
} else {
rl.question(query, (answer) => {
rl.close()
resolve(answer.trim())
})
}
})
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ็จๆท็ปๅฝ/ๆณจๅไบคไบๆต็จ โ ๅทฒๆๅ่ณ commands/auth.ts
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๅทฅไฝๅบๅฟซ็
ง๏ผๆญ็น็ปญไผ ๏ผ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ไธปๆต็จ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function main() {
// ๅๅงๅๅฝ้
ๅ็ณป็ป๏ผๅฟ
้กปๅจไปปไฝ่พๅบไนๅ๏ผ
initI18n()
// P4 #2.10: ็กฎไฟๅช่ดดๆฟๆไปถๆ้ๅฎๅ
จ
ensureClipboardSecure()
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ไฟกๅทๅค็ๅจ - ็กฎไฟ้ๅบๆถไฟๅญๅทฅไฝๅบ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let onExit: (() => Promise<void>) | null = null
let isExiting = false
const handleSignal = async (signal: string) => {
if (isExiting) return
isExiting = true
console.log(`\n ${MUTED}${t('signal.received', { signal })}${color.reset}`)
if (onExit) {
await onExit()
}
process.exit(0)
}
process.on('SIGINT', () => handleSignal('SIGINT'))
process.on('SIGTERM', () => handleSignal('SIGTERM'))
process.on('SIGHUP', () => handleSignal('SIGHUP'))
const { values } = parseArgs({
options: {
prompt: { type: 'string', short: 'p' },
continue: { type: 'boolean', short: 'c' },
sessions: { type: 'boolean', short: 's' },
model: { type: 'string', short: 'm' },
version: { type: 'boolean', short: 'v' },
help: { type: 'boolean', short: 'h' },
pavr: { type: 'boolean', short: 'P', default: true }, // ๅฏ็จ PAVR ๅพช็ฏ
'no-pavr': { type: 'boolean', short: 'N', default: false }, // ็ฆ็จ PAVR ๅพช็ฏ
},
strict: false,
})
// ่ฎพ็ฝฎ PAVR ๆจกๅผ
if (values['no-pavr']) {
setGlobalPAVREnabled(false)
} else if (values.pavr) {
setGlobalPAVREnabled(true)
}
if (values.help) { printHelp(VERSION, color, BRAND, MUTED, ACCENT, SUCCESS, ERROR, WARN); return }
if (values.version) { console.log(`CmdCode V${VERSION}`); return }
// ๐ง ็ฎก้่พๅ
ฅๆฃๆต๏ผstdin ไธๆฏ TTY ๆถ๏ผ่ฏปๅๅ
จ้จๅ
ๅฎนไฝไธบ -p ๆจกๅผ่พๅ
ฅ
if (!process.stdin.isTTY && !values.prompt) {
const pipedInput = await new Promise<string>((resolve) => {
let data = ''
process.stdin.setEncoding('utf-8')
process.stdin.on('data', (chunk: string) => { data += chunk })
process.stdin.on('end', () => resolve(data.trim()))
// ่ถ
ๆถไฟๆค๏ผ5็งๅ
ๆฒก่ฏปๅฎๅฐฑๅ
ๆไบค
setTimeout(() => resolve(data.trim()), 5000)
})
if (pipedInput.length > 0) {
values.prompt = pipedInput
}
}
let config = loadConfig()
if (values.model) config.model = values.model as string
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆ ธๅฟๆน่ฟ1๏ผ็จๆท็ปๅฝ/ๆณจๅ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const userInfo = await userAuthFlow(VERSION, color, BRAND, ACCENT, MUTED, SUCCESS, ERROR, WARN, askQuestion, askPassword)
// ่ฎพ็ฝฎ็จๆทไธๅฑๅทฅไฝๅบๅฐๆฒ็ฎฑ็ณป็ป
setUserWorkspace(userInfo.workspaceDir)
setUsername(userInfo.username)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๅๅงๅๅ้่ฎฐๅฟ็ณป็ป
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
try {
initMemorySystem()
// ๆณจ๏ผๅฏ้ฅๅบ็ถๆๆฃๆฅๅทฒ็งป่ณๅๅ่ฝๆจกๅๅ
้จๆ้ๆง่ก
} catch (e) {
console.log(` ${MUTED}${t('model.memory_init_failed')}${color.reset}`)
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆ ธๅฟๆน่ฟ2๏ผๆญ็น็ปญไผ - ๆขๅคไธๆฌกๅทฅไฝๅบ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let restoredMessages: Message[] | undefined
if (!values.prompt) {
process.stdout.write(` ${MUTED}${t("session.checking_snapshot")}${color.reset}`)
try {
const snapshot = await restoreWorkspaceSnapshot(userInfo)
process.stdout.write('\r' + ' '.repeat(30) + '\r')
if (snapshot) {
const msgs = restoreWorkspaceFromSnapshot(snapshot, userInfo, { WARN, color })
if (msgs && msgs.length > 0) {
restoredMessages = msgs
// ไธๅๆพ็คบๆขๅคไฟกๆฏ๏ผๅจ printBanner ็ปไธๅฑ็คบ
}
} else {
// ๆฒกๆๅฟซ็
ง๏ผ้้ป่ทณ่ฟ
}
} catch {
process.stdout.write('\r' + ' '.repeat(30) + '\r')
console.log(` ${MUTED}${t('model.snapshot_check_failed')}${color.reset}`)
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆ ธๅฟๆน่ฟ3๏ผๆจกๅ้
็ฝฎ๏ผ็จๆท่ชๅฎไน > ็ณป็ป้ป่ฎค๏ผ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๅ
ๆฃๆฅ็จๆทๆฏๅฆๆ่ชๅฎไนๆจกๅ้
็ฝฎ
const userDefaultModel = getUserDefaultModel(userInfo.username)
let modelOnline = false
let modelLatencyMs: number | undefined
if (userDefaultModel) {
// ไฝฟ็จ็จๆท่ชๅฎไนๆจกๅ
config = {
apiKey: userDefaultModel.apiKey,
baseUrl: userDefaultModel.baseUrl,
model: userDefaultModel.model,
timeoutMs: config.timeoutMs,
}
process.stdout.write(` ${ACCENT}${t("model.connecting")} ${userDefaultModel.name} ...${color.reset}`)
const testResult = await testConnection(config.baseUrl, config.apiKey, config.model)
process.stdout.write('\r' + ' '.repeat(40) + '\r')
if (testResult.success) {
modelOnline = true
modelLatencyMs = testResult.latencyMs
} else {
console.log(` ${WARN}${t("model.test_failed")} ${testResult.error}${color.reset}`)
console.log(` ${MUTED}${t('model.manage_hint')}${color.reset}`)
}
} else if (!config.apiKey) {
// ็ณป็ป้ป่ฎค้
็ฝฎไนๆฒกๆ๏ผไบคไบๅผ้
็ฝฎ
const setup = await interactiveModelSetup(userInfo.username, color, BRAND, MUTED, SUCCESS, ERROR, WARN, ACCENT, askQuestion)
if (!setup) {
console.log('')
console.log(` ${ACCENT}${t('model.no_config_exit')}${color.reset}`)
process.exit(0)
}
// ๅฆๆๆฏ็จๆท่ชๅฎไน้
็ฝฎ๏ผๅญๅจๅฐ็จๆท็ฎๅฝ
if (setup.saveToUser) {
createUserModel(userInfo.username, {
name: setup.name || setup.model,
model: setup.model,
baseUrl: setup.baseUrl,
apiKey: setup.apiKey,
note1: setup.note1,
note2: setup.note2,
})
console.log(` ${SUCCESS}${t('model.saved')}${color.reset}`)
} else {
// ไฟๅญไธบ็ณป็ป้ป่ฎค
saveKeys(setup.apiKey, setup.baseUrl)
updateAppConfig({ model: setup.model })
}
config = {
apiKey: setup.apiKey,
baseUrl: setup.baseUrl,
model: setup.model,
timeoutMs: config.timeoutMs,
}
console.log('')
console.log(` ${ACCENT}${t('model.encrypted_saved')}${color.reset}`)
console.log('')
} else {
// ไฝฟ็จ็ณป็ป้ป่ฎค้
็ฝฎ
process.stdout.write(` ${ACCENT}${t("model.connecting")} ${config.model} ...${color.reset}`)
const testResult = await testConnection(config.baseUrl, config.apiKey, config.model)
process.stdout.write('\r' + ' '.repeat(40) + '\r')
if (testResult.success) {
modelOnline = true
modelLatencyMs = testResult.latencyMs
} else {
console.log(` ${WARN}${t("model.test_failed")} ${testResult.error}${color.reset}`)
console.log(` ${MUTED}${t('model.possible_reasons')}${color.reset}`)
console.log(` ${MUTED}${t('model.manage_hint2')}${color.reset}`)
console.log('')
}
}
// ๅๅบไผ่ฏ
if (values.sessions) {
const sessions = listSessions()
if (sessions.length === 0) {
console.log(` ${MUTED}ๆๆ ไผ่ฏ่ฎฐๅฝ${color.reset}`)
} else {
console.log('')
console.log(` ${color.bold}${t('session.list_title')}${color.reset}`)
console.log(` ${MUTED}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${color.reset}`)
for (const s of sessions) {
console.log(` ${BRAND}${s.id}${color.reset}`)
console.log(` ${MUTED}${t('session.messages')}: ${s.messageCount} ยท ${t('session.updated')}: ${s.updatedAt.slice(0, 19)}${color.reset}`)
if (s.preview) console.log(` ${MUTED}${t('session.preview')}: ${s.preview}${color.reset}`)
console.log('')
}
}
return
}
// ๅๆฌกๆง่กๆจกๅผ
if (values.prompt) {
const prompt = values.prompt as string
let messages: Message[] | undefined
if (values.continue) {
const sessionId = getLatestSessionId()
if (sessionId) {
messages = loadSession(sessionId) || undefined
if (messages) {
console.log(` ${MUTED}${t('session.restored')} ${sessionId} (${messages.length} ${t('session.messages_count')})${color.reset}`)
}
} else {
console.log(` ${MUTED}${t('session.no_restore')}${color.reset}`)
}
}
// ๆฏๆ CMD_WORKSPACE ็ฏๅขๅ้่ฆ็ๅทฅไฝ็ฎๅฝ
const cmdWorkspace = process.env.CMD_WORKSPACE
// Issue 10: Warn about potential path conflict
if (values.continue && cmdWorkspace) {
console.log(` ${WARN}โ ๏ธ --continue + CMD_WORKSPACE: workspace changed, session files may be at original path${color.reset}`)
}
// ๐ ไฝฟ็จๅทฅๅๅฝๆฐๅๅปบๅผๆ๏ผๆฏๆ PAVR ๅพช็ฏ๏ผ
const engine = messages
? createChatEngineFromHistory(messages, undefined, config, cmdWorkspace)
: createChatEngine(undefined, config, undefined, cmdWorkspace)
// ๅฆๆ่ฎพ็ฝฎไบ CMD_WORKSPACE๏ผๅๆขๅฐ่ฏฅ็ฎๅฝ
if (cmdWorkspace) {
process.chdir(cmdWorkspace)
setUserWorkspace(cmdWorkspace)
}
try {
const finalText = await engine.chat(prompt)
const sessionId = saveSession(engine.getHistory())
console.log(`\n ${MUTED}${t('session.saved')} ${sessionId}${color.reset}`)
// ใ่ชๅจไฟฎๅคใ่งฃๆๅคงๆจกๅ่พๅบ๏ผๆๅไปฃ็ ๅๅนถไฟๅญไธบๆไปถ๏ผ่ฅๅคงๆจกๅๆช่ฐ็จ file_write๏ผ
const codeBlockRegex = /```(\w+)?\s*\n([\s\S]*?)```/g
let match
let fileSaved = false
while ((match = codeBlockRegex.exec(finalText)) !== null) {
const lang = match[1] || 'txt'
const code = match[2].trim()
if (code.length > 0) {
const ext = lang === 'python' ? 'py' : lang === 'javascript' ? 'js' : lang === 'typescript' ? 'ts' : lang
const fileName = `solution.${ext}`
try {
writeFileSync(fileName, code, 'utf-8')
console.log(` โ
Auto-saved code to ${fileName}`)
fileSaved = true
} catch (e: any) {
console.error(` โ Failed to save ${fileName}: ${e.message}`)
}
}
}
if (!fileSaved) {
console.warn(` โ ๏ธ No code block found in output.`)
}
process.exit(0)
} catch (e: any) {
if (e.status === 429) {
console.error(`\n ${WARN}${t("error.429_single")}${color.reset}`)
console.error(` ${MUTED}${t("error.switch_model_hint")}${color.reset}`)
} else if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT') {
console.error(`\n ${ERROR}${t("error.network_single")} ${e.message}${color.reset}`)
console.error(` ${MUTED}${t("error.check_network_hint")}${color.reset}`)
} else {
console.error(`\n ${ERROR}${t("error.general_single")} ${e.message}${color.reset}`)
console.error(` ${MUTED}${t("error.model_hint_single")}${color.reset}`)
}
process.exit(1)
}
return
}
// ไบคไบREPLๆจกๅผ
// ่ทๅๆจกๅๆพ็คบๅ็งฐ
const modelName = userDefaultModel?.name || config.model
// ่ฎก็ฎๅญๅจ็ฉบ้ดๅๅฏน่ฏๅๅฒ
const usedBytes = getDirSize(userInfo.workspaceDir)
const historyCount = restoredMessages?.length || countHistoryMessages(userInfo.workspaceDir)
printBanner(userInfo, { name: modelName, model: config.model, online: modelOnline, latencyMs: modelLatencyMs }, historyCount, usedBytes, { BRAND, ACCENT, MUTED, SUCCESS, WARN, color })
// ๆพ็คบ PAVR ็ถๆ
console.log(` ${isGlobalPAVREnabled() ? ACCENT : MUTED}๐ PAVR: ${isGlobalPAVREnabled() ? 'ๅฏ็จ' : '็ฆ็จ'}${color.reset}`)
// ่ฎพ็ฝฎๅทฅไฝๅบไธบ็จๆทไธๅฑ็ฎๅฝ
process.chdir(userInfo.workspaceDir)
// ๐ ไฝฟ็จๅทฅๅๅฝๆฐๅๅปบๅผๆ๏ผๆฏๆ PAVR ๅพช็ฏ๏ผ
const engine = restoredMessages
? createChatEngineFromHistory(restoredMessages, undefined, config, userInfo.workspaceDir)
: createChatEngine(undefined, config, undefined, userInfo.workspaceDir)
// ้ป่ฎค็ณป็ปๆ็คบ่ฏ๏ผ็จๆท่ชๅฎไน > ้ป่ฎคๆจกๆฟ๏ผ็ปไธไป secrets.enc ่ฏปๅ๏ผ
const customPrompt = getSystemPrompt()
const DEFAULT_SYSTEM_PROMPT = customPrompt || SYSTEM_PROMPT_TEMPLATE + `
Current user workspace: ${process.cwd()}`
// โ REPL ไธปๅพช็ฏๅทฒๆๅ่ณ commands/repl.ts
await replLoop({
userInfo,
engine,
config,
modelOnline,
modelLatencyMs,
userDefaultModel,
defaultSystemPrompt: DEFAULT_SYSTEM_PROMPT,
color,
BRAND,
ACCENT,
MUTED,
SUCCESS,
ERROR,
WARN,
askREPL,
askQuestion,
askPassword,
registerExitHandler: (handler) => { onExit = handler },
})
}
main().catch(e => {
console.error('Fatal:', e.message)
process.exit(1)
})