Skip to content

Commit 282de3f

Browse files
feat: add npm publish workflow (#29)
* feat: add npm publish workflow Co-Authored-By: Han Xiao <[email protected]> * feat: add CLI interface Co-Authored-By: Han Xiao <[email protected]> * fix: add moduleResolution and resolveJsonModule to tsconfig Co-Authored-By: Han Xiao <[email protected]> * feat: add OPENAI_API_KEY to workflow files Co-Authored-By: Han Xiao <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Han Xiao <[email protected]>
1 parent aee5598 commit 282de3f

File tree

7 files changed

+164
-3
lines changed

7 files changed

+164
-3
lines changed

.github/workflows/npm-publish.yml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: NPM Publish
2+
3+
on:
4+
release:
5+
types: [created]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Use Node.js
14+
uses: actions/setup-node@v4
15+
with:
16+
node-version: '20.x'
17+
registry-url: 'https://registry.npmjs.org'
18+
cache: 'npm'
19+
20+
- name: Install dependencies
21+
run: npm ci
22+
23+
- name: Run lint and tests
24+
env:
25+
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
26+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
27+
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
28+
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }}
29+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
30+
run: |
31+
npm run lint
32+
npm test
33+
34+
- name: Build TypeScript
35+
run: npm run build
36+
37+
- name: Publish to npm
38+
env:
39+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
40+
run: npm publish --access public

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ jobs:
3131
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
3232
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
3333
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }}
34+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
3435
run: npm test

package-lock.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
{
22
"name": "node-deepresearch",
33
"version": "1.0.0",
4-
"main": "index.js",
4+
"main": "dist/index.js",
5+
"types": "dist/index.d.ts",
6+
"files": [
7+
"dist",
8+
"README.md",
9+
"LICENSE"
10+
],
511
"scripts": {
12+
"prepare": "npm run build",
613
"build": "tsc",
714
"dev": "npx ts-node src/agent.ts",
815
"search": "npx ts-node src/test-duck.ts",
@@ -25,6 +32,7 @@
2532
"@types/node-fetch": "^2.6.12",
2633
"ai": "^4.1.21",
2734
"axios": "^1.7.9",
35+
"commander": "^13.1.0",
2836
"cors": "^2.8.5",
2937
"duck-duck-scrape": "^2.2.7",
3038
"express": "^4.21.2",
@@ -33,6 +41,7 @@
3341
"zod": "^3.22.4"
3442
},
3543
"devDependencies": {
44+
"@types/commander": "^2.12.0",
3645
"@types/jest": "^29.5.14",
3746
"@types/node": "^22.10.10",
3847
"@typescript-eslint/eslint-plugin": "^7.0.1",

src/__tests__/cli.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
4+
const execAsync = promisify(exec);
5+
6+
// Mock environment variables
7+
process.env.GEMINI_API_KEY = 'test-key';
8+
process.env.JINA_API_KEY = 'test-key';
9+
10+
jest.mock('../agent', () => ({
11+
getResponse: jest.fn().mockResolvedValue({
12+
result: {
13+
action: 'answer',
14+
answer: 'Test answer',
15+
references: []
16+
}
17+
})
18+
}));
19+
20+
describe('CLI', () => {
21+
test('shows version', async () => {
22+
const { stdout } = await execAsync('ts-node src/cli.ts --version');
23+
expect(stdout.trim()).toMatch(/\d+\.\d+\.\d+/);
24+
});
25+
26+
test('shows help', async () => {
27+
const { stdout } = await execAsync('ts-node src/cli.ts --help');
28+
expect(stdout).toContain('deepresearch');
29+
expect(stdout).toContain('AI-powered research assistant');
30+
});
31+
32+
test('handles invalid token budget', async () => {
33+
try {
34+
await execAsync('ts-node src/cli.ts -t invalid "test query"');
35+
fail('Should have thrown');
36+
} catch (error) {
37+
expect((error as { stderr: string }).stderr).toContain('Invalid token budget: must be a number');
38+
}
39+
});
40+
});

src/cli.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env node
2+
import { Command } from 'commander';
3+
import { getResponse } from './agent';
4+
import { version } from '../package.json';
5+
6+
const program = new Command();
7+
8+
program
9+
.name('deepresearch')
10+
.description('AI-powered research assistant that keeps searching until it finds the answer')
11+
.version(version)
12+
.argument('<query>', 'The research query to investigate')
13+
.option('-t, --token-budget <number>', 'Maximum token budget', (val) => {
14+
const num = parseInt(val);
15+
if (isNaN(num)) throw new Error('Invalid token budget: must be a number');
16+
return num;
17+
}, 1000000)
18+
.option('-m, --max-attempts <number>', 'Maximum bad attempts before giving up', (val) => {
19+
const num = parseInt(val);
20+
if (isNaN(num)) throw new Error('Invalid max attempts: must be a number');
21+
return num;
22+
}, 3)
23+
.option('-v, --verbose', 'Show detailed progress')
24+
.action(async (query: string, options: any) => {
25+
try {
26+
const { result } = await getResponse(
27+
query,
28+
parseInt(options.tokenBudget),
29+
parseInt(options.maxAttempts)
30+
);
31+
32+
if (result.action === 'answer') {
33+
console.log('\nAnswer:', result.answer);
34+
if (result.references?.length) {
35+
console.log('\nReferences:');
36+
result.references.forEach(ref => {
37+
console.log(`- ${ref.url}`);
38+
console.log(` "${ref.exactQuote}"`);
39+
});
40+
}
41+
}
42+
} catch (error) {
43+
console.error('Error:', error instanceof Error ? error.message : String(error));
44+
process.exit(1);
45+
}
46+
});
47+
48+
program.parse();

tsconfig.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"esModuleInterop": true,
88
"skipLibCheck": true,
99
"forceConsistentCasingInFileNames": true,
10-
"strict": true
10+
"strict": true,
11+
"resolveJsonModule": true,
12+
"moduleResolution": "node"
1113
}
12-
}
14+
}

0 commit comments

Comments
 (0)