This commit is contained in:
Han Xiao 2025-02-06 17:36:19 +08:00
commit 1677bc5298
7 changed files with 164 additions and 3 deletions

40
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: NPM Publish
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run lint and tests
env:
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
npm run lint
npm test
- name: Build TypeScript
run: npm run build
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public

View File

@ -31,4 +31,5 @@ jobs:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: npm test

21
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@types/node-fetch": "^2.6.12",
"ai": "^4.1.21",
"axios": "^1.7.9",
"commander": "^13.1.0",
"cors": "^2.8.5",
"duck-duck-scrape": "^2.2.7",
"express": "^4.21.2",
@ -24,6 +25,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@types/commander": "^2.12.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^7.0.1",
@ -1529,6 +1531,16 @@
"@types/node": "*"
}
},
"node_modules/@types/commander": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz",
"integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@ -2595,6 +2607,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",

View File

@ -1,8 +1,15 @@
{
"name": "node-deepresearch",
"version": "1.0.0",
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"prepare": "npm run build",
"build": "tsc",
"dev": "npx ts-node src/agent.ts",
"search": "npx ts-node src/test-duck.ts",
@ -25,6 +32,7 @@
"@types/node-fetch": "^2.6.12",
"ai": "^4.1.21",
"axios": "^1.7.9",
"commander": "^13.1.0",
"cors": "^2.8.5",
"duck-duck-scrape": "^2.2.7",
"express": "^4.21.2",
@ -33,6 +41,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@types/commander": "^2.12.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^7.0.1",

40
src/__tests__/cli.test.ts Normal file
View File

@ -0,0 +1,40 @@
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
// Mock environment variables
process.env.GEMINI_API_KEY = 'test-key';
process.env.JINA_API_KEY = 'test-key';
jest.mock('../agent', () => ({
getResponse: jest.fn().mockResolvedValue({
result: {
action: 'answer',
answer: 'Test answer',
references: []
}
})
}));
describe('CLI', () => {
test('shows version', async () => {
const { stdout } = await execAsync('ts-node src/cli.ts --version');
expect(stdout.trim()).toMatch(/\d+\.\d+\.\d+/);
});
test('shows help', async () => {
const { stdout } = await execAsync('ts-node src/cli.ts --help');
expect(stdout).toContain('deepresearch');
expect(stdout).toContain('AI-powered research assistant');
});
test('handles invalid token budget', async () => {
try {
await execAsync('ts-node src/cli.ts -t invalid "test query"');
fail('Should have thrown');
} catch (error) {
expect((error as { stderr: string }).stderr).toContain('Invalid token budget: must be a number');
}
});
});

48
src/cli.ts Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { getResponse } from './agent';
import { version } from '../package.json';
const program = new Command();
program
.name('deepresearch')
.description('AI-powered research assistant that keeps searching until it finds the answer')
.version(version)
.argument('<query>', 'The research query to investigate')
.option('-t, --token-budget <number>', 'Maximum token budget', (val) => {
const num = parseInt(val);
if (isNaN(num)) throw new Error('Invalid token budget: must be a number');
return num;
}, 1000000)
.option('-m, --max-attempts <number>', 'Maximum bad attempts before giving up', (val) => {
const num = parseInt(val);
if (isNaN(num)) throw new Error('Invalid max attempts: must be a number');
return num;
}, 3)
.option('-v, --verbose', 'Show detailed progress')
.action(async (query: string, options: any) => {
try {
const { result } = await getResponse(
query,
parseInt(options.tokenBudget),
parseInt(options.maxAttempts)
);
if (result.action === 'answer') {
console.log('\nAnswer:', result.answer);
if (result.references?.length) {
console.log('\nReferences:');
result.references.forEach(ref => {
console.log(`- ${ref.url}`);
console.log(` "${ref.exactQuote}"`);
});
}
}
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
program.parse();

View File

@ -7,6 +7,8 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"strict": true
"strict": true,
"resolveJsonModule": true,
"moduleResolution": "node"
}
}
}