Merge branch 'master' into dev

This commit is contained in:
di-sukharev
2025-08-01 16:02:44 +03:00
44 changed files with 29840 additions and 21584 deletions

View File

@@ -12,17 +12,43 @@ import { GroqEngine } from '../engine/groq';
import { MLXEngine } from '../engine/mlx';
import { DeepseekEngine } from '../engine/deepseek';
import { AimlApiEngine } from '../engine/aimlapi';
import { OpenRouterEngine } from '../engine/openrouter';
export function parseCustomHeaders(headers: any): Record<string, string> {
let parsedHeaders = {};
if (!headers) {
return parsedHeaders;
}
try {
if (typeof headers === 'object' && !Array.isArray(headers)) {
parsedHeaders = headers;
} else {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn(
'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'
);
}
return parsedHeaders;
}
export function getEngine(): AiEngine {
const config = getConfig();
const provider = config.OCO_AI_PROVIDER;
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
const DEFAULT_CONFIG = {
model: config.OCO_MODEL!,
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_API_URL!,
apiKey: config.OCO_API_KEY!
apiKey: config.OCO_API_KEY!,
customHeaders
};
switch (provider) {
@@ -59,6 +85,9 @@ export function getEngine(): AiEngine {
case OCO_AI_PROVIDER_ENUM.AIMLAPI:
return new AimlApiEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.OPENROUTER:
return new OpenRouterEngine(DEFAULT_CONFIG);
default:
return new OpenAiEngine(DEFAULT_CONFIG);
}

View File

@@ -1,7 +1,7 @@
import { execa } from 'execa';
import { readFileSync } from 'fs';
import ignore, { Ignore } from 'ignore';
import { join } from 'path';
import { outro, spinner } from '@clack/prompts';
export const assertGitRepo = async () => {
@@ -16,41 +16,44 @@ export const assertGitRepo = async () => {
// (file) => `:(exclude)${file}`
// );
export const getOpenCommitIgnore = (): Ignore => {
export const getOpenCommitIgnore = async (): Promise<Ignore> => {
const gitDir = await getGitDir();
const ig = ignore();
try {
ig.add(readFileSync('.opencommitignore').toString().split('\n'));
ig.add(
readFileSync(join(gitDir, '.opencommitignore')).toString().split('\n')
);
} catch (e) {}
return ig;
};
export const getCoreHooksPath = async (): Promise<string> => {
const { stdout } = await execa('git', ['config', 'core.hooksPath']);
const gitDir = await getGitDir();
const { stdout } = await execa('git', ['config', 'core.hooksPath'], {
cwd: gitDir
});
return stdout;
};
export const getStagedFiles = async (): Promise<string[]> => {
const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
const gitDir = await getGitDir();
const { stdout: files } = await execa('git', [
'diff',
'--name-only',
'--cached',
'--relative',
gitDir
]);
const { stdout: files } = await execa(
'git',
['diff', '--name-only', '--cached', '--relative'],
{ cwd: gitDir }
);
if (!files) return [];
const filesList = files.split('\n');
const ig = getOpenCommitIgnore();
const ig = await getOpenCommitIgnore();
const allowedFiles = filesList.filter((file) => !ig.ignores(file));
if (!allowedFiles) return [];
@@ -59,12 +62,17 @@ export const getStagedFiles = async (): Promise<string[]> => {
};
export const getChangedFiles = async (): Promise<string[]> => {
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
const { stdout: others } = await execa('git', [
'ls-files',
'--others',
'--exclude-standard'
]);
const gitDir = await getGitDir();
const { stdout: modified } = await execa('git', ['ls-files', '--modified'], {
cwd: gitDir
});
const { stdout: others } = await execa(
'git',
['ls-files', '--others', '--exclude-standard'],
{ cwd: gitDir }
);
const files = [...modified.split('\n'), ...others.split('\n')].filter(
(file) => !!file
@@ -74,16 +82,20 @@ export const getChangedFiles = async (): Promise<string[]> => {
};
export const gitAdd = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const gitAddSpinner = spinner();
gitAddSpinner.start('Adding files to commit');
await execa('git', ['add', ...files]);
await execa('git', ['add', ...files], { cwd: gitDir });
gitAddSpinner.stop('Done');
gitAddSpinner.stop(`Staged ${files.length} files`);
};
export const getDiff = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const lockFiles = files.filter(
(file) =>
file.includes('.lock') ||
@@ -108,12 +120,20 @@ export const getDiff = async ({ files }: { files: string[] }) => {
(file) => !file.includes('.lock') && !file.includes('-lock.')
);
const { stdout: diff } = await execa('git', [
'diff',
'--staged',
'--',
...filesWithoutLocks
]);
const { stdout: diff } = await execa(
'git',
['diff', '--staged', '--', ...filesWithoutLocks],
{ cwd: gitDir }
);
return diff;
};
export const getGitDir = async (): Promise<string> => {
const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
return gitDir;
};

View File

@@ -0,0 +1,57 @@
/**
* Removes content wrapped in specified tags from a string
* @param content The content string to process
* @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>')
* @returns The content with the specified tags and their contents removed, and trimmed
*/
export function removeContentTags<T extends string | null | undefined>(
content: T,
tag: string
): T {
if (!content || typeof content !== 'string') {
return content;
}
// Dynamic implementation for other cases
const openTag = `<${tag}>`;
const closeTag = `</${tag}>`;
// Parse the content and remove tags
let result = '';
let skipUntil: number | null = null;
let depth = 0;
for (let i = 0; i < content.length; i++) {
// Check for opening tag
if (content.substring(i, i + openTag.length) === openTag) {
depth++;
if (depth === 1) {
skipUntil = content.indexOf(closeTag, i + openTag.length);
i = i + openTag.length - 1; // Skip the opening tag
continue;
}
}
// Check for closing tag
else if (
content.substring(i, i + closeTag.length) === closeTag &&
depth > 0
) {
depth--;
if (depth === 0) {
i = i + closeTag.length - 1; // Skip the closing tag
skipUntil = null;
continue;
}
}
// Only add character if not inside a tag
if (skipUntil === null) {
result += content[i];
}
}
// Normalize multiple spaces/tabs into a single space (preserves newlines), then trim.
result = result.replace(/[ \t]+/g, ' ').trim();
return result as unknown as T;
}