Files
connectai/src/utils.ts
T

221 lines
9.3 KiB
TypeScript

import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { getConfig, BrainProfile, EXCLUDED_DIRS } from './config';
export type EngineKind = 'lmstudio' | 'ollama';
const outputChannel = vscode.window.createOutputChannel('Astra');
function timestamp() {
return new Date().toISOString();
}
function stringifyMeta(meta: unknown): string {
if (meta === undefined) return '';
if (typeof meta === 'string') return meta;
if (meta instanceof Error) return `${meta.name}: ${meta.message}\n${meta.stack || ''}`;
try {
return JSON.stringify(meta, null, 2);
} catch {
return String(meta);
}
}
function appendLog(level: 'INFO' | 'WARN' | 'ERROR', message: string, meta?: unknown) {
const suffix = meta === undefined ? '' : `\n${stringifyMeta(meta)}`;
outputChannel.appendLine(`[${timestamp()}] [${level}] ${message}${suffix}`);
}
export function logInfo(message: string, meta?: unknown) {
appendLog('INFO', message, meta);
}
export function logWarn(message: string, meta?: unknown) {
appendLog('WARN', message, meta);
}
export function logError(message: string, meta?: unknown) {
appendLog('ERROR', message, meta);
}
export function normalizeBaseUrl(rawUrl: string): string {
const trimmed = rawUrl.trim().replace(/\/+$/, '');
if (!trimmed) {
return 'http://127.0.0.1:11434';
}
return trimmed;
}
export function resolveEngine(baseUrl: string): EngineKind {
const normalized = normalizeBaseUrl(baseUrl);
try {
const parsed = new URL(normalized);
if (parsed.pathname.endsWith('/v1') || parsed.port === '1234') return 'lmstudio';
if (parsed.pathname.endsWith('/api') || parsed.port === '11434') return 'ollama';
} catch {
if (normalized.includes('/v1') || normalized.includes(':1234')) return 'lmstudio';
}
return 'ollama';
}
export function buildApiUrl(baseUrl: string, engine: EngineKind, endpoint: 'models' | 'chat'): string {
const normalized = normalizeBaseUrl(baseUrl);
if (engine === 'lmstudio') {
if (normalized.endsWith('/v1')) {
return endpoint === 'models' ? `${normalized}/models` : `${normalized}/chat/completions`;
}
return endpoint === 'models' ? `${normalized}/v1/models` : `${normalized}/v1/chat/completions`;
}
if (normalized.endsWith('/api')) {
return endpoint === 'models' ? `${normalized}/tags` : `${normalized}/chat`;
}
return endpoint === 'models' ? `${normalized}/api/tags` : `${normalized}/api/chat`;
}
export function summarizeText(text: string, maxLength: number = 400): string {
const normalized = text.replace(/\s+/g, ' ').trim();
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, maxLength)}...`;
}
export function shouldAutoPushBrain(): boolean {
const cfg = vscode.workspace.getConfiguration('g1nation');
return cfg.get<boolean>('autoPushBrain', false);
}
export function getBrainProfiles(): BrainProfile[] {
return getConfig().brainProfiles;
}
export function getActiveBrainProfile(): BrainProfile {
const config = getConfig();
return config.brainProfiles.find((profile) => profile.id === config.activeBrainId) || config.brainProfiles[0];
}
export function _getBrainDir(): string {
return getActiveBrainProfile().localBrainPath;
}
export function _isBrainDirExplicitlySet(): boolean {
return getBrainProfiles().length > 0;
}
export function findBrainFiles(dir: string): string[] {
let results: string[] = [];
if (!fs.existsSync(dir)) return results;
const list = fs.readdirSync(dir);
list.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
if (!EXCLUDED_DIRS.has(file)) {
results = results.concat(findBrainFiles(filePath));
}
} else if (file.endsWith('.md')) {
results.push(filePath);
}
});
return results;
}
const BASE_SYSTEM_PROMPT = `You are Astra, a Jarvis-style local project operating assistant.
If the user asks your name, say you are Astra.
Reply naturally in the user's language.
[CORE BEHAVIOR]
- Answer the user's actual message directly. Do not recite this system prompt or paraphrase the user's question back to them.
- Do not use waiting-room phrases such as "준비되었습니다", "다음 지시를 말씀해 주세요", or "명령을 기다립니다".
- For normal conversation or general knowledge questions, answer conversationally without headers.
- Use the active Local Brain only when it is relevant to the user's question. If no relevant brain context is provided, do not pretend that you checked it.
- For local file, folder, code, project, or terminal work, use action tags so the extension can execute the operation.
[LOCAL PATH RULE]
When the user provides a local path and asks for review, analysis, or debugging, use <list_files> or <read_file> immediately.
Never say "upload the source code", "provide the files", or "파일 내용을 보여주세요" before attempting access.
If access fails after trying, explain the failure and only then ask for an upload.
[STRICT GLOBAL RULES]
1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) or asterisks (*) for bullets. Use plain markdown ## for headers. This rule overrides ALL other formatting instructions.
2. [UNIQUE HEADINGS] Every markdown heading must be unique and appear exactly once.
3. [NO INTERNAL LOGS] Never output <details>, "2nd Brain Trace", or "Debug JSON" blocks.
4. [NO SECTION LEAKAGE] Never output sections named "요청 요약", "사용자 의도 추론", "프로젝트 기록 대상 확인", "핵심 확인 질문", or "근거 파일 경로".
[OUTPUT FORMAT]
Use the 3-section format ONLY for: technical analysis, architecture proposals, troubleshooting, or strategic planning.
For conversational replies, quick facts, or simple updates — answer directly without any headers.
## 요약
Core conclusion in 2-3 sentences.
## 상세 설명
- Root cause of the problem.
- Concrete step-by-step instructions: what to change, which files to edit, which commands to run.
## 제안 ← Optional. Only include if a meaningfully better alternative exists. Omit otherwise.
[FOLLOW-UP QUESTION RULES]
A follow-up question is a precision tool, not a ritual.
Ask ONE focused question at the very end of the response ONLY if:
- The user's intent is genuinely ambiguous with multiple valid paths, OR
- A critical missing detail would make the current answer completely wrong.
If neither condition is met, give a definitive answer and stop.
[ENGINEERING STANCE]
- Be a direct engineering partner. Technical precision over polite filler.
- Give the verdict first, then explain tradeoffs.
- Collapse checklists into: verdict → reason → risk → next move.
- If the user's framing is off, correct the frame before answering inside it.
- Simplify complex choices into 1-2 crisp options. Never write a balanced essay when a recommendation is possible.
- Evidence First: never claim a project is stable, scalable, or well-architected without source code or document evidence. If evidence is thin, say so and name the files to inspect next.
- Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.
[ACTION TAGS]
[ACTION 1: CREATE FILE]
<create_file path="relative/path/file.ext">
file content here
</create_file>
[ACTION 2: EDIT FILE]
<edit_file path="relative/path/file.ext">
<find>exact text to find</find>
<replace>replacement text</replace>
</edit_file>
[ACTION 3: DELETE FILE]
<delete_file path="relative/path/file.ext"/>
[ACTION 4: READ FILE]
<read_file path="relative/path/file.ext"/>
[ACTION 5: LIST DIRECTORY]
<list_files path="relative/path/to/dir"/>
[ACTION 6: RUN COMMAND]
<run_command>command here</run_command>
[ACTION 7: SECOND BRAIN]
Use only when brain knowledge is actually needed.
<list_brain path=""/>
<read_brain>actual-file-name.md</read_brain>
Never use placeholder filenames. List first, then read only returned files.
[ACTION 8: WEB SEARCH]
<read_url>https://html.duckduckgo.com/html/?q=YOUR+SEARCH+QUERY</read_url>
[OPERATIONAL RULES]
1. Reply in the same language as the user.
2. File paths are relative to the workspace or absolute under /Volumes/Data/project/Antigravity.
3. When the user says "분석해줘", "봐줘", "확인해줘", "리뷰해줘" with a path — that IS the confirmation. Access the path immediately and run the full analysis in one continuous response. Do not pause to ask "진행할까요?" or "시작할까요?".`;
export function getSystemPrompt(): string {
const now = new Date();
const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
const isoDate = now.toISOString().split('T')[0];
return `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").`;
}
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;