Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions apps/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,12 @@ async function addGitResourceWizard(
const result = await Result.tryPromise(async () => {
const finalUrl = await promptInput(rl, 'URL', normalizedUrl);
const name = await promptInput(rl, 'Name', urlParts.repo);
const branch = await promptInput(rl, 'Branch', 'main');
const branchInput = await promptInput(
rl,
'Branch (optional, auto-detect default if empty)',
''
);
const branch = branchInput.trim();
const wantSearchPaths = await promptConfirm(
rl,
'Do you want to add search paths (subdirectories to focus on)?'
Expand All @@ -300,7 +305,7 @@ async function addGitResourceWizard(
console.log(' Type: git');
console.log(` Name: ${name}`);
console.log(` URL: ${finalUrl}`);
console.log(` Branch: ${branch}`);
console.log(` Branch: ${branch || '(auto-detect remote default branch)'}`);
if (searchPaths.length > 0) console.log(` Search: ${searchPaths.join(', ')}`);
if (notes) console.log(` Notes: ${notes}`);
console.log(` Config: ${options.global ? 'global' : 'project'}`);
Expand All @@ -325,7 +330,7 @@ async function addGitResourceWizard(
type: 'git',
name,
url: finalUrl,
branch,
...(branch ? { branch } : {}),
...(searchPaths.length === 1 && { searchPath: searchPaths[0] }),
...(searchPaths.length > 1 && { searchPaths }),
...(notes && { specialNotes: notes })
Expand All @@ -337,6 +342,9 @@ async function addGitResourceWizard(
if (resource.type === 'git' && resource.url !== finalUrl) {
console.log(` URL normalized: ${resource.url}`);
}
if (resource.type === 'git' && !branch) {
console.log(` Auto-detected branch: ${resource.branch}`);
}
console.log('\nYou can now use this resource:');
console.log(` btca ask -r ${name} -q "your question"`);
});
Expand Down Expand Up @@ -498,7 +506,7 @@ export const addCommand = new Command('add')
.argument('[reference]', 'Repository URL, local path, or npm package reference')
.option('-g, --global', 'Add to global config instead of project config')
.option('-n, --name <name>', 'Resource name')
.option('-b, --branch <branch>', 'Git branch (default: main)')
.option('-b, --branch <branch>', 'Git branch (auto-detected when omitted)')
.option('-s, --search-path <path...>', 'Search paths within repo (can specify multiple)')
.option('--notes <notes>', 'Special notes for the agent')
.option('-t, --type <type>', 'Resource type: git, local, or npm (auto-detected if not specified)')
Expand Down Expand Up @@ -585,7 +593,7 @@ export const addCommand = new Command('add')
type: 'git',
name: options.name,
url: normalizedUrl,
branch: options.branch ?? 'main',
...(options.branch ? { branch: options.branch } : {}),
...(searchPaths.length === 1 && { searchPath: searchPaths[0] }),
...(searchPaths.length > 1 && { searchPaths }),
...(options.notes && { specialNotes: options.notes })
Expand All @@ -597,6 +605,9 @@ export const addCommand = new Command('add')
if (resource.type === 'git' && resource.url !== normalizedUrl) {
console.log(` URL normalized: ${resource.url}`);
}
if (resource.type === 'git' && !options.branch) {
console.log(` Auto-detected branch: ${resource.branch}`);
}
return;
}

Expand Down
13 changes: 12 additions & 1 deletion apps/cli/src/commands/ask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ function getErrorDisplayDetails(error: unknown): { message: string; hint?: strin
const stripped = message.slice('Unhandled exception:'.length).trim();
return stripped.length > 0 ? stripped : message;
};
const fallbackWrapperMessage = (message: string) => {
if (
message === 'match err handler threw' ||
message === 'match ok handler threw' ||
message.endsWith('handler threw') ||
message.endsWith('callback threw')
) {
return 'Internal error while processing a result. Check server logs for details.';
}
return normalizeMessage(message);
};

const chain: unknown[] = [];
const visited = new Set<unknown>();
Expand Down Expand Up @@ -69,7 +80,7 @@ function getErrorDisplayDetails(error: unknown): { message: string; hint?: strin
for (const entry of chain) {
const entryMessage = readStringField(entry, 'message');
if (entryMessage) {
message = normalizeMessage(entryMessage);
message = fallbackWrapperMessage(entryMessage);
break;
}
}
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ describe('errors', () => {
cyclic.cause = cyclic;

expect(getErrorTag(cyclic)).toBe('Panic');
expect(getErrorMessage(cyclic)).toBe('match err handler threw');
expect(getErrorMessage(cyclic)).toBe(
'Internal error while processing a result. Check logs for details.'
);
expect(getErrorHint(cyclic)).toBeUndefined();
});
});
14 changes: 13 additions & 1 deletion apps/server/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ const normalizeMessage = (message: string) => {
return stripped.length > 0 ? stripped : message;
};

const fallbackWrapperMessage = (message: string) => {
if (
message === 'match err handler threw' ||
message === 'match ok handler threw' ||
/handler threw$/u.test(message) ||
/callback threw$/u.test(message)
) {
return 'Internal error while processing a result. Check logs for details.';
}
return normalizeMessage(message);
};

const isWrapperEntry = (entry: unknown) => {
const tag = readStringField(entry, '_tag');
const message = readStringField(entry, 'message');
Expand Down Expand Up @@ -101,7 +113,7 @@ export const getErrorMessage = (error: unknown): string => {

for (const entry of chain) {
const message = readStringField(entry, 'message');
if (message) return normalizeMessage(message);
if (message) return fallbackWrapperMessage(message);
}

return String(error);
Expand Down
48 changes: 46 additions & 2 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,48 @@ const normalizeQuestionResourceReference = (reference: string): string => {
return reference;
};

const withGitAuthArgs = (args: string[]) => {
const token = process.env.BTCA_GIT_TOKEN?.trim();
if (!token) return args;
return [
'-c',
'credential.helper=!f() { test "$1" = get && echo "username=x-access-token" && echo "password=$BTCA_GIT_TOKEN"; }; f',
...args
];
};

const detectDefaultBranchForRepository = async (repoUrl: string): Promise<string | undefined> => {
const proc = Bun.spawn(['git', ...withGitAuthArgs(['ls-remote', '--symref', repoUrl, 'HEAD'])], {
stdout: 'pipe',
stderr: 'pipe',
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0'
}
});

const [stdoutText, stderrText, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited
]);
if (exitCode !== 0) {
Metrics.info('resource.git.default_branch.detect_failed', {
url: repoUrl,
error: stderrText.trim().slice(0, 300)
});
return undefined;
}

const line = stdoutText
.split('\n')
.find((entry) => entry.trim().startsWith('ref:') && entry.includes('\tHEAD'));
if (!line) return undefined;

const match = line.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/);
return match?.[1];
};

const QuestionRequestSchema = z.object({
question: z
.string()
Expand Down Expand Up @@ -135,7 +177,7 @@ const AddGitResourceRequestSchema = z.object({
type: z.literal('git'),
name: GitResourceSchema.shape.name,
url: GitResourceSchema.shape.url,
branch: GitResourceSchema.shape.branch.optional().default('main'),
branch: GitResourceSchema.shape.branch.optional(),
searchPath: GitResourceSchema.shape.searchPath,
searchPaths: GitResourceSchema.shape.searchPaths,
specialNotes: GitResourceSchema.shape.specialNotes
Expand Down Expand Up @@ -448,11 +490,13 @@ const createApp = (deps: {
if (decoded.type === 'git') {
// Normalize GitHub URLs (e.g., /blob/main/file.txt → base repo URL)
const normalizedUrl = normalizeGitHubUrl(decoded.url);
const branch =
decoded.branch ?? (await detectDefaultBranchForRepository(normalizedUrl)) ?? 'main';
const resource = {
type: 'git' as const,
name: decoded.name,
url: normalizedUrl,
branch: decoded.branch ?? 'main',
branch,
...(decoded.searchPath && { searchPath: decoded.searchPath }),
...(decoded.searchPaths && { searchPaths: decoded.searchPaths }),
...(decoded.specialNotes && { specialNotes: decoded.specialNotes })
Expand Down
Loading