Add status command, strip raw blobs from search, harden login flow

- Add `alpha status` command to check auth state
- Search results drop raw text blobs by default (opt-in via includeRaw)
- Clean and normalize search result fields
- Export parsePaperSearchResults for external consumers
- Login callback server handles EADDRINUSE, styled HTML pages
- Better browser open error handling on all platforms
- Search prints mode description before results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal 2026-03-25 11:08:54 -07:00
parent f6469556f0
commit 081d32d50c
8 changed files with 134 additions and 29 deletions

View file

@ -10,6 +10,7 @@ Research agents hallucinate paper details and forget what they learn in a sessio
```bash
npm install -g @companion-ai/alpha-hub
alpha login # sign in with alphaXiv
alpha status # show whether alphaXiv auth is present
alpha search "attention mechanism" # search papers
alpha get 1706.03762 # fetch paper report
```
@ -52,6 +53,7 @@ alpha ask 1706.03762 "What datasets were used for evaluation?"
| `alpha annotate <id> --clear` | Remove a note |
| `alpha annotate --list` | List all notes |
| `alpha login` | Sign in with alphaXiv |
| `alpha status` | Show alphaXiv authentication status |
| `alpha logout` | Sign out |
All commands accept `--json` for machine-readable output.

View file

@ -12,6 +12,7 @@ npm install -g @companion-ai/alpha-hub
```bash
alpha login
alpha status
alpha search "attention mechanism"
alpha get 1706.03762
alpha ask 1706.03762 "What datasets were used for evaluation?"

View file

@ -1,5 +1,5 @@
import chalk from 'chalk';
import { login, isLoggedIn, logout } from '../lib/auth.js';
import { getUserName, login, isLoggedIn, logout } from '../lib/auth.js';
export function registerLoginCommand(program) {
program
@ -29,3 +29,19 @@ export function registerLogoutCommand(program) {
console.log(chalk.green('Logged out'));
});
}
export function registerStatusCommand(program) {
program
.command('status')
.description('Show alphaXiv authentication status')
.action(() => {
if (!isLoggedIn()) {
process.stderr.write(chalk.dim('Not logged in to alphaXiv.\n'));
process.exitCode = 1;
return;
}
const name = getUserName();
console.log(chalk.green(name ? `Logged in to alphaXiv as ${name}` : 'Logged in to alphaXiv'));
});
}

View file

@ -1,12 +1,27 @@
import chalk from 'chalk';
import { searchByEmbedding, searchByKeyword, agenticSearch, disconnect } from '../lib/alphaxiv.js';
import { output, error } from '../lib/output.js';
import { output, error, info } from '../lib/output.js';
function formatResults(data) {
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
console.log(text);
}
function describeMode(mode) {
switch (mode) {
case 'keyword':
return 'keyword full-text';
case 'agentic':
return 'agentic';
case 'both':
return 'semantic + keyword';
case 'all':
return 'semantic + keyword + agentic';
default:
return 'semantic';
}
}
export function registerSearchCommand(program) {
program
.command('search <query>')
@ -15,6 +30,9 @@ export function registerSearchCommand(program) {
.action(async (query, cmdOpts) => {
const opts = { ...program.opts(), ...cmdOpts };
try {
if (!opts.json) {
info(chalk.dim(`Searching alphaXiv (${describeMode(opts.mode)})...`));
}
let results;
if (opts.mode === 'keyword') {
results = await searchByKeyword(query);

View file

@ -8,7 +8,7 @@ import { registerGetCommand } from './commands/get.js';
import { registerAskCommand } from './commands/ask.js';
import { registerAnnotateCommand } from './commands/annotate.js';
import { registerCodeCommand } from './commands/code.js';
import { registerLoginCommand, registerLogoutCommand } from './commands/login.js';
import { registerLoginCommand, registerLogoutCommand, registerStatusCommand } from './commands/login.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@ -34,6 +34,7 @@ ${chalk.bold.underline('Usage')}
${chalk.bold.underline('Commands')}
${chalk.bold('login')} Log in to alphaXiv (opens browser)
${chalk.bold('status')} Show alphaXiv authentication status
${chalk.bold('logout')} Log out
${chalk.bold('search')} <query> Search papers (semantic, keyword, both, agentic, or all)
${chalk.bold('get')} <url|arxiv-id> Paper content + local annotation
@ -63,6 +64,7 @@ program
});
registerLoginCommand(program);
registerStatusCommand(program);
registerLogoutCommand(program);
registerSearchCommand(program);
registerGetCommand(program);

View file

@ -81,10 +81,56 @@ function generatePKCE() {
}
function openBrowser(url) {
const plat = platform();
if (plat === 'darwin') execSync(`open "${url}"`);
else if (plat === 'linux') execSync(`xdg-open "${url}"`);
else if (plat === 'win32') execSync(`start "${url}"`);
try {
const plat = platform();
if (plat === 'darwin') execSync(`open "${url}"`);
else if (plat === 'linux') execSync(`xdg-open "${url}"`);
else if (plat === 'win32') execSync(`start "" "${url}"`);
} catch {}
}
const SUCCESS_HTML = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>alphaXiv</title>
<style>
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #e5e5e5; }
.card { text-align: center; padding: 2rem; }
h2 { color: #10b981; margin-bottom: 0.5rem; }
p { color: #737373; }
</style>
</head>
<body><div class="card"><h2>Logged in to alphaXiv</h2><p>You can close this tab</p></div>
<script>setTimeout(function(){window.close()},2000)</script>
</body></html>`;
const ERROR_HTML = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>alphaXiv</title>
<style>
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #e5e5e5; }
.card { text-align: center; padding: 2rem; }
h2 { color: #ef4444; margin-bottom: 0.5rem; }
p { color: #737373; }
</style>
</head>
<body><div class="card"><h2>Login failed</h2><p>You can close this tab and try again</p></div></body></html>`;
function startCallbackServer() {
return new Promise((resolve, reject) => {
const server = createServer();
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Close the process using it and try again.`));
} else {
reject(err);
}
});
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
resolve(server);
});
});
}
function waitForCallback(server) {
@ -108,7 +154,7 @@ function waitForCallback(server) {
if (error) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>');
res.end(ERROR_HTML);
clearTimeout(timeout);
server.close();
reject(new Error(`OAuth error: ${error}`));
@ -117,7 +163,7 @@ function waitForCallback(server) {
if (code) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>');
res.end(SUCCESS_HTML);
clearTimeout(timeout);
server.close();
resolve(code);
@ -194,8 +240,7 @@ export async function login() {
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', state);
const server = createServer();
server.listen(CALLBACK_PORT);
const server = await startCallbackServer();
process.stderr.write('Opening browser for alphaXiv login...\n');
openBrowser(authUrl.toString());

View file

@ -13,9 +13,18 @@ export declare function searchByEmbedding(query: string): Promise<unknown>;
export declare function searchByKeyword(query: string): Promise<unknown>;
export declare function agenticSearch(query: string): Promise<unknown>;
export declare function parsePaperSearchResults(
text: unknown,
options?: { includeRaw?: boolean },
): {
results: unknown[];
raw?: unknown;
};
export declare function searchPapers(
query: string,
mode?: "semantic" | "keyword" | "both" | "agentic" | "all" | string,
options?: { includeRaw?: boolean },
): Promise<unknown>;
export declare function getPaper(

View file

@ -45,9 +45,21 @@ function parsePublishedAt(fragment) {
return match ? match[1].trim() : null;
}
function parsePaperListText(text) {
function cleanSearchField(value) {
if (typeof value !== 'string') return null;
const normalized = value
.replace(/\r\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/\s*\n\s*/g, ' ')
.replace(/[ \t]+/g, ' ')
.trim();
return normalized || null;
}
export function parsePaperSearchResults(text, options = {}) {
const includeRaw = options.includeRaw === true;
if (typeof text !== 'string') {
return { raw: text, results: [] };
return { results: [] };
}
const blocks = text
@ -69,28 +81,28 @@ function parsePaperListText(text) {
return {
rank: index + 1,
title: headerMatch ? headerMatch[1].trim() : header,
title: cleanSearchField(headerMatch ? headerMatch[1] : header),
visits: headerMatch ? parseMetricNumber(headerMatch[2], 'Visits') : null,
likes: headerMatch ? parseMetricNumber(headerMatch[2], 'Likes') : null,
publishedAt: headerMatch ? parsePublishedAt(headerMatch[2]) : null,
organizations: fieldValue('- Organizations:'),
authors: fieldValue('- Authors:'),
abstract: fieldValue('- Abstract:'),
arxivId,
organizations: cleanSearchField(fieldValue('- Organizations:')),
authors: cleanSearchField(fieldValue('- Authors:')),
abstract: cleanSearchField(fieldValue('- Abstract:')),
arxivId: cleanSearchField(arxivId),
arxivUrl: arxivId ? `https://arxiv.org/abs/${arxivId}` : null,
alphaXivUrl: arxivId ? `https://www.alphaxiv.org/overview/${arxivId}` : null,
raw: block,
...(includeRaw ? { raw: block } : {}),
};
});
return { raw: text, results };
return includeRaw ? { raw: text, results } : { results };
}
function normalizeSearchPayload(query, mode, payload) {
function normalizeSearchPayload(query, mode, payload, options = {}) {
if (mode === 'all' || mode === 'both') {
const normalized = {};
for (const [key, value] of Object.entries(payload)) {
normalized[key] = parsePaperListText(value);
normalized[key] = parsePaperSearchResults(value, options);
}
return {
query,
@ -99,7 +111,7 @@ function normalizeSearchPayload(query, mode, payload) {
};
}
const parsed = parsePaperListText(payload);
const parsed = parsePaperSearchResults(payload, options);
return {
query,
mode,
@ -107,18 +119,18 @@ function normalizeSearchPayload(query, mode, payload) {
};
}
export async function searchPapers(query, mode = 'semantic') {
if (mode === 'keyword') return normalizeSearchPayload(query, mode, await searchByKeyword(query));
if (mode === 'agentic') return normalizeSearchPayload(query, mode, await agenticSearch(query));
export async function searchPapers(query, mode = 'semantic', options = {}) {
if (mode === 'keyword') return normalizeSearchPayload(query, mode, await searchByKeyword(query), options);
if (mode === 'agentic') return normalizeSearchPayload(query, mode, await agenticSearch(query), options);
if (mode === 'both') {
const [semantic, keyword] = await Promise.all([
searchByEmbedding(query),
searchByKeyword(query),
]);
return normalizeSearchPayload(query, mode, { semantic, keyword });
return normalizeSearchPayload(query, mode, { semantic, keyword }, options);
}
if (mode === 'all') return normalizeSearchPayload(query, mode, await searchAll(query));
return normalizeSearchPayload(query, mode, await searchByEmbedding(query));
if (mode === 'all') return normalizeSearchPayload(query, mode, await searchAll(query), options);
return normalizeSearchPayload(query, mode, await searchByEmbedding(query), options);
}
export async function getPaper(identifier, options = {}) {