mirror of
https://github.com/getcompanion-ai/alpha-hub.git
synced 2026-04-15 03:00:43 +00:00
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:
parent
f6469556f0
commit
081d32d50c
8 changed files with 134 additions and 29 deletions
|
|
@ -10,6 +10,7 @@ Research agents hallucinate paper details and forget what they learn in a sessio
|
||||||
```bash
|
```bash
|
||||||
npm install -g @companion-ai/alpha-hub
|
npm install -g @companion-ai/alpha-hub
|
||||||
alpha login # sign in with alphaXiv
|
alpha login # sign in with alphaXiv
|
||||||
|
alpha status # show whether alphaXiv auth is present
|
||||||
alpha search "attention mechanism" # search papers
|
alpha search "attention mechanism" # search papers
|
||||||
alpha get 1706.03762 # fetch paper report
|
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 <id> --clear` | Remove a note |
|
||||||
| `alpha annotate --list` | List all notes |
|
| `alpha annotate --list` | List all notes |
|
||||||
| `alpha login` | Sign in with alphaXiv |
|
| `alpha login` | Sign in with alphaXiv |
|
||||||
|
| `alpha status` | Show alphaXiv authentication status |
|
||||||
| `alpha logout` | Sign out |
|
| `alpha logout` | Sign out |
|
||||||
|
|
||||||
All commands accept `--json` for machine-readable output.
|
All commands accept `--json` for machine-readable output.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ npm install -g @companion-ai/alpha-hub
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
alpha login
|
alpha login
|
||||||
|
alpha status
|
||||||
alpha search "attention mechanism"
|
alpha search "attention mechanism"
|
||||||
alpha get 1706.03762
|
alpha get 1706.03762
|
||||||
alpha ask 1706.03762 "What datasets were used for evaluation?"
|
alpha ask 1706.03762 "What datasets were used for evaluation?"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import chalk from 'chalk';
|
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) {
|
export function registerLoginCommand(program) {
|
||||||
program
|
program
|
||||||
|
|
@ -29,3 +29,19 @@ export function registerLogoutCommand(program) {
|
||||||
console.log(chalk.green('Logged out'));
|
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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { searchByEmbedding, searchByKeyword, agenticSearch, disconnect } from '../lib/alphaxiv.js';
|
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) {
|
function formatResults(data) {
|
||||||
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||||
console.log(text);
|
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) {
|
export function registerSearchCommand(program) {
|
||||||
program
|
program
|
||||||
.command('search <query>')
|
.command('search <query>')
|
||||||
|
|
@ -15,6 +30,9 @@ export function registerSearchCommand(program) {
|
||||||
.action(async (query, cmdOpts) => {
|
.action(async (query, cmdOpts) => {
|
||||||
const opts = { ...program.opts(), ...cmdOpts };
|
const opts = { ...program.opts(), ...cmdOpts };
|
||||||
try {
|
try {
|
||||||
|
if (!opts.json) {
|
||||||
|
info(chalk.dim(`Searching alphaXiv (${describeMode(opts.mode)})...`));
|
||||||
|
}
|
||||||
let results;
|
let results;
|
||||||
if (opts.mode === 'keyword') {
|
if (opts.mode === 'keyword') {
|
||||||
results = await searchByKeyword(query);
|
results = await searchByKeyword(query);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { registerGetCommand } from './commands/get.js';
|
||||||
import { registerAskCommand } from './commands/ask.js';
|
import { registerAskCommand } from './commands/ask.js';
|
||||||
import { registerAnnotateCommand } from './commands/annotate.js';
|
import { registerAnnotateCommand } from './commands/annotate.js';
|
||||||
import { registerCodeCommand } from './commands/code.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 __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
||||||
|
|
@ -34,6 +34,7 @@ ${chalk.bold.underline('Usage')}
|
||||||
${chalk.bold.underline('Commands')}
|
${chalk.bold.underline('Commands')}
|
||||||
|
|
||||||
${chalk.bold('login')} Log in to alphaXiv (opens browser)
|
${chalk.bold('login')} Log in to alphaXiv (opens browser)
|
||||||
|
${chalk.bold('status')} Show alphaXiv authentication status
|
||||||
${chalk.bold('logout')} Log out
|
${chalk.bold('logout')} Log out
|
||||||
${chalk.bold('search')} <query> Search papers (semantic, keyword, both, agentic, or all)
|
${chalk.bold('search')} <query> Search papers (semantic, keyword, both, agentic, or all)
|
||||||
${chalk.bold('get')} <url|arxiv-id> Paper content + local annotation
|
${chalk.bold('get')} <url|arxiv-id> Paper content + local annotation
|
||||||
|
|
@ -63,6 +64,7 @@ program
|
||||||
});
|
});
|
||||||
|
|
||||||
registerLoginCommand(program);
|
registerLoginCommand(program);
|
||||||
|
registerStatusCommand(program);
|
||||||
registerLogoutCommand(program);
|
registerLogoutCommand(program);
|
||||||
registerSearchCommand(program);
|
registerSearchCommand(program);
|
||||||
registerGetCommand(program);
|
registerGetCommand(program);
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,56 @@ function generatePKCE() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBrowser(url) {
|
function openBrowser(url) {
|
||||||
const plat = platform();
|
try {
|
||||||
if (plat === 'darwin') execSync(`open "${url}"`);
|
const plat = platform();
|
||||||
else if (plat === 'linux') execSync(`xdg-open "${url}"`);
|
if (plat === 'darwin') execSync(`open "${url}"`);
|
||||||
else if (plat === 'win32') execSync(`start "${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) {
|
function waitForCallback(server) {
|
||||||
|
|
@ -108,7 +154,7 @@ function waitForCallback(server) {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
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);
|
clearTimeout(timeout);
|
||||||
server.close();
|
server.close();
|
||||||
reject(new Error(`OAuth error: ${error}`));
|
reject(new Error(`OAuth error: ${error}`));
|
||||||
|
|
@ -117,7 +163,7 @@ function waitForCallback(server) {
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
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);
|
clearTimeout(timeout);
|
||||||
server.close();
|
server.close();
|
||||||
resolve(code);
|
resolve(code);
|
||||||
|
|
@ -194,8 +240,7 @@ export async function login() {
|
||||||
authUrl.searchParams.set('code_challenge_method', 'S256');
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
||||||
authUrl.searchParams.set('state', state);
|
authUrl.searchParams.set('state', state);
|
||||||
|
|
||||||
const server = createServer();
|
const server = await startCallbackServer();
|
||||||
server.listen(CALLBACK_PORT);
|
|
||||||
|
|
||||||
process.stderr.write('Opening browser for alphaXiv login...\n');
|
process.stderr.write('Opening browser for alphaXiv login...\n');
|
||||||
openBrowser(authUrl.toString());
|
openBrowser(authUrl.toString());
|
||||||
|
|
|
||||||
9
cli/src/lib/index.d.ts
vendored
9
cli/src/lib/index.d.ts
vendored
|
|
@ -13,9 +13,18 @@ export declare function searchByEmbedding(query: string): Promise<unknown>;
|
||||||
export declare function searchByKeyword(query: string): Promise<unknown>;
|
export declare function searchByKeyword(query: string): Promise<unknown>;
|
||||||
export declare function agenticSearch(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(
|
export declare function searchPapers(
|
||||||
query: string,
|
query: string,
|
||||||
mode?: "semantic" | "keyword" | "both" | "agentic" | "all" | string,
|
mode?: "semantic" | "keyword" | "both" | "agentic" | "all" | string,
|
||||||
|
options?: { includeRaw?: boolean },
|
||||||
): Promise<unknown>;
|
): Promise<unknown>;
|
||||||
|
|
||||||
export declare function getPaper(
|
export declare function getPaper(
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,21 @@ function parsePublishedAt(fragment) {
|
||||||
return match ? match[1].trim() : null;
|
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') {
|
if (typeof text !== 'string') {
|
||||||
return { raw: text, results: [] };
|
return { results: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = text
|
const blocks = text
|
||||||
|
|
@ -69,28 +81,28 @@ function parsePaperListText(text) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rank: index + 1,
|
rank: index + 1,
|
||||||
title: headerMatch ? headerMatch[1].trim() : header,
|
title: cleanSearchField(headerMatch ? headerMatch[1] : header),
|
||||||
visits: headerMatch ? parseMetricNumber(headerMatch[2], 'Visits') : null,
|
visits: headerMatch ? parseMetricNumber(headerMatch[2], 'Visits') : null,
|
||||||
likes: headerMatch ? parseMetricNumber(headerMatch[2], 'Likes') : null,
|
likes: headerMatch ? parseMetricNumber(headerMatch[2], 'Likes') : null,
|
||||||
publishedAt: headerMatch ? parsePublishedAt(headerMatch[2]) : null,
|
publishedAt: headerMatch ? parsePublishedAt(headerMatch[2]) : null,
|
||||||
organizations: fieldValue('- Organizations:'),
|
organizations: cleanSearchField(fieldValue('- Organizations:')),
|
||||||
authors: fieldValue('- Authors:'),
|
authors: cleanSearchField(fieldValue('- Authors:')),
|
||||||
abstract: fieldValue('- Abstract:'),
|
abstract: cleanSearchField(fieldValue('- Abstract:')),
|
||||||
arxivId,
|
arxivId: cleanSearchField(arxivId),
|
||||||
arxivUrl: arxivId ? `https://arxiv.org/abs/${arxivId}` : null,
|
arxivUrl: arxivId ? `https://arxiv.org/abs/${arxivId}` : null,
|
||||||
alphaXivUrl: arxivId ? `https://www.alphaxiv.org/overview/${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') {
|
if (mode === 'all' || mode === 'both') {
|
||||||
const normalized = {};
|
const normalized = {};
|
||||||
for (const [key, value] of Object.entries(payload)) {
|
for (const [key, value] of Object.entries(payload)) {
|
||||||
normalized[key] = parsePaperListText(value);
|
normalized[key] = parsePaperSearchResults(value, options);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
|
|
@ -99,7 +111,7 @@ function normalizeSearchPayload(query, mode, payload) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parsePaperListText(payload);
|
const parsed = parsePaperSearchResults(payload, options);
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -107,18 +119,18 @@ function normalizeSearchPayload(query, mode, payload) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchPapers(query, mode = 'semantic') {
|
export async function searchPapers(query, mode = 'semantic', options = {}) {
|
||||||
if (mode === 'keyword') return normalizeSearchPayload(query, mode, await searchByKeyword(query));
|
if (mode === 'keyword') return normalizeSearchPayload(query, mode, await searchByKeyword(query), options);
|
||||||
if (mode === 'agentic') return normalizeSearchPayload(query, mode, await agenticSearch(query));
|
if (mode === 'agentic') return normalizeSearchPayload(query, mode, await agenticSearch(query), options);
|
||||||
if (mode === 'both') {
|
if (mode === 'both') {
|
||||||
const [semantic, keyword] = await Promise.all([
|
const [semantic, keyword] = await Promise.all([
|
||||||
searchByEmbedding(query),
|
searchByEmbedding(query),
|
||||||
searchByKeyword(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));
|
if (mode === 'all') return normalizeSearchPayload(query, mode, await searchAll(query), options);
|
||||||
return normalizeSearchPayload(query, mode, await searchByEmbedding(query));
|
return normalizeSearchPayload(query, mode, await searchByEmbedding(query), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPaper(identifier, options = {}) {
|
export async function getPaper(identifier, options = {}) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue