@fettstorch/clai 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
1
  {
2
- "name": "@fettstorch/clai",
3
- "version": "0.1.5",
4
- "main": "dist/index.js",
5
- "bin": {
6
- "clai": "dist/cli.js"
7
- },
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/schnullerpip/clai.git"
11
- },
12
- "scripts": {
13
- "start": "bun run src/cli.ts",
14
- "build": "bun build ./src/index.ts --outdir dist --target node && bun build ./src/cli.ts --outdir dist --target node",
15
- "dev": "bun --watch src/cli.ts"
16
- },
17
- "author": "schnullerpip (https://github.com/schnullerpip)",
18
- "license": "ISC",
19
- "description": "AI-powered webpage summarizer",
20
- "dependencies": {
21
- "@fettstorch/jule": "^0.5.3",
22
- "chalk": "^5.3.0",
23
- "cheerio": "^1.0.0-rc.12",
24
- "commander": "^12.1.0",
25
- "inquirer": "^12.1.0",
26
- "openai": "^4.73.0",
27
- "ora": "^8.1.1",
28
- "googleapis": "^126.0.1"
29
- },
30
- "devDependencies": {
31
- "@types/inquirer": "^9.0.7",
32
- "@types/node": "^20.11.19",
33
- "bun-types": "latest"
34
- },
35
- "publishConfig": {
36
- "access": "public"
37
- }
2
+ "name": "@fettstorch/clai",
3
+ "version": "0.1.7",
4
+ "main": "dist/index.js",
5
+ "bin": {
6
+ "clai": "dist/cli.js"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/schnullerpip/clai.git"
11
+ },
12
+ "scripts": {
13
+ "start": "bun run src/cli.ts",
14
+ "build": "bun build ./src/index.ts --outdir dist --target node && bun build ./src/cli.ts --outdir dist --target node",
15
+ "dev": "bun --watch src/cli.ts"
16
+ },
17
+ "author": "schnullerpip (https://github.com/schnullerpip)",
18
+ "license": "ISC",
19
+ "description": "AI-powered webpage summarizer",
20
+ "dependencies": {
21
+ "@biomejs/biome": "^1.9.4",
22
+ "@fettstorch/jule": "^0.5.3",
23
+ "chalk": "^5.3.0",
24
+ "cheerio": "^1.0.0-rc.12",
25
+ "commander": "^12.1.0",
26
+ "inquirer": "^12.1.0",
27
+ "openai": "^4.73.0",
28
+ "ora": "^8.1.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/inquirer": "^9.0.7",
32
+ "@types/node": "^20.11.19",
33
+ "bun-types": "latest"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
38
  }
package/src/cli.ts CHANGED
@@ -1,155 +1,163 @@
1
1
  #!/usr/bin/env bun
2
- import { Command } from 'commander';
3
- import inquirer from 'inquirer';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { clai } from './index';
7
- import { when } from '@fettstorch/jule';
8
- import pkg from '../package.json' assert { type: 'json' };
2
+ import { when } from '@fettstorch/jule'
3
+ import chalk from 'chalk'
4
+ import { Command } from 'commander'
5
+ import inquirer from 'inquirer'
6
+ import ora from 'ora'
7
+ import pkg from '../package.json' assert { type: 'json' }
8
+ import { clai } from './index'
9
9
 
10
- const program = new Command();
10
+ const program = new Command()
11
11
 
12
12
  async function main() {
13
- try {
14
- program
15
- .name('clai')
16
- .description('AI-powered web scraping tool')
17
- .version(pkg.version)
18
- .argument('[input...]', 'URL or search terms to analyze')
19
- .action(async (inputs: string[]) => {
20
- const openAIKey = process.env.OPENAI_API_KEY;
21
-
22
- if (!openAIKey) {
23
- console.error(chalk.red('❌ OPENAI_API_KEY environment variable is not set'));
24
- process.exit(1);
25
- }
26
-
27
- let input = inputs?.join(' ');
28
-
29
- if (!input) {
30
- const answers = await inquirer.prompt([
31
- {
32
- type: 'input',
33
- name: 'input',
34
- message: 'Enter a URL or search query:',
35
- validate: (input) => input.length > 0
36
- }
37
- ]);
38
- input = answers.input;
39
- }
40
-
41
- await analyzeInput(input, openAIKey);
42
- process.exit(0);
43
- });
44
-
45
- await program.parseAsync();
46
- } catch (error) {
47
- console.error(chalk.red('Fatal error:'), error);
48
- process.exit(1);
49
- }
13
+ try {
14
+ program
15
+ .name('clai')
16
+ .description('AI-powered web scraping tool')
17
+ .version(pkg.version)
18
+ .argument('[input...]', 'URL or search terms to analyze')
19
+ .action(async (inputs: string[]) => {
20
+ const openAIKey = process.env.OPENAI_API_KEY
21
+
22
+ if (!openAIKey) {
23
+ console.error(
24
+ chalk.red('❌ OPENAI_API_KEY environment variable is not set'),
25
+ )
26
+ process.exit(1)
27
+ }
28
+
29
+ let input = inputs?.join(' ')
30
+
31
+ if (!input) {
32
+ const answers = await inquirer.prompt([
33
+ {
34
+ type: 'input',
35
+ name: 'input',
36
+ message: 'Enter a URL or search query:',
37
+ validate: (input) => input.length > 0,
38
+ },
39
+ ])
40
+ input = answers.input
41
+ }
42
+
43
+ await analyzeInput(input, openAIKey)
44
+ process.exit(0)
45
+ })
46
+
47
+ await program.parseAsync()
48
+ } catch (error) {
49
+ console.error(chalk.red('Fatal error:'), error)
50
+ process.exit(1)
51
+ }
50
52
  }
51
53
 
52
54
  async function animateText(text: string, delay = 25) {
53
- let shouldComplete = false;
54
-
55
- // Setup keypress listener
56
- const keypressHandler = (str: string, key: { name: string }) => {
57
- if (key.name === 'return') {
58
- shouldComplete = true;
59
- }
60
- };
61
-
62
- process.stdin.on('keypress', keypressHandler);
63
-
64
- // Enable raw mode to get keypress events
65
- process.stdin.setRawMode(true);
66
- process.stdin.resume();
67
-
68
- let currentIndex = 0;
69
- while (currentIndex < text.length) {
70
- if (shouldComplete) {
71
- // Show remaining text immediately
72
- process.stdout.write(text.slice(currentIndex));
73
- break;
74
- }
75
-
76
- process.stdout.write(text[currentIndex]);
77
- currentIndex++;
78
-
79
- if (!shouldComplete) {
80
- await new Promise(resolve => setTimeout(resolve, delay));
81
- }
82
- }
83
-
84
- // Cleanup
85
- process.stdin.setRawMode(false);
86
- process.stdin.pause();
87
- process.stdin.removeListener('keypress', keypressHandler);
88
-
89
- process.stdout.write('\n');
55
+ let shouldComplete = false
56
+
57
+ // Setup keypress listener
58
+ const keypressHandler = (str: string, key: { name: string }) => {
59
+ if (key.name === 'return') {
60
+ shouldComplete = true
61
+ }
62
+ }
63
+
64
+ process.stdin.on('keypress', keypressHandler)
65
+
66
+ // Enable raw mode to get keypress events
67
+ process.stdin.setRawMode(true)
68
+ process.stdin.resume()
69
+
70
+ let currentIndex = 0
71
+ while (currentIndex < text.length) {
72
+ if (shouldComplete) {
73
+ // Show remaining text immediately
74
+ process.stdout.write(text.slice(currentIndex))
75
+ break
76
+ }
77
+
78
+ process.stdout.write(text[currentIndex])
79
+ currentIndex++
80
+
81
+ if (!shouldComplete) {
82
+ await new Promise((resolve) => setTimeout(resolve, delay))
83
+ }
84
+ }
85
+
86
+ // Cleanup
87
+ process.stdin.setRawMode(false)
88
+ process.stdin.pause()
89
+ process.stdin.removeListener('keypress', keypressHandler)
90
+
91
+ process.stdout.write('\n')
90
92
  }
91
93
 
92
94
  function formatMarkdownForTerminal(text: string): string {
93
- // Handle headings first
94
- const headingHandled = text.replace(/^(#{1,3})\s+(.*?)$/gm, (_, hashes, content) => when(hashes.length)({
95
- 1: () => `\n${chalk.yellow.bold('═══ ')}${chalk.yellow.bold(content)}${chalk.yellow.bold(' ═══')}`,
96
- 2: () => chalk.yellowBright.bold(content),
97
- 3: () => chalk.yellow(content),
98
- else: () => content
99
- }));
100
-
101
- // Handle regular bold text after headings
102
- const boldHandled = headingHandled.replace(/\*\*(.*?)\*\*/g, (_, content) => chalk.bold(content));
103
-
104
- return boldHandled;
95
+ // Handle headings first
96
+ const headingHandled = text.replace(
97
+ /^(#{1,3})\s+(.*?)$/gm,
98
+ (_, hashes, content) =>
99
+ when(hashes.length)({
100
+ 1: () =>
101
+ `\n${chalk.yellow.bold('═══ ')}${chalk.yellow.bold(content)}${chalk.yellow.bold(' ═══')}`,
102
+ 2: () => chalk.yellowBright.bold(content),
103
+ 3: () => chalk.yellow(content),
104
+ else: () => content,
105
+ }),
106
+ )
107
+
108
+ // Handle regular bold text after headings
109
+ const boldHandled = headingHandled.replace(/\*\*(.*?)\*\*/g, (_, content) =>
110
+ chalk.bold(content),
111
+ )
112
+
113
+ return boldHandled
105
114
  }
106
115
 
107
116
  async function analyzeInput(input: string, openAIKey: string) {
108
- const spinner = ora('Thinking...').start();
109
-
110
- try {
111
- const result = await clai(input, openAIKey);
112
- spinner.succeed('AHA!');
113
-
114
- console.log(chalk.green.bold('\n📝 ═══ Summary ═══ :'));
115
- const formattedContent = formatMarkdownForTerminal(result.summary);
116
- await animateText(formattedContent);
117
-
118
- // Prompt user to select a link
119
- const { selectedLink } = await inquirer.prompt([
120
- {
121
- type: 'list',
122
- name: 'selectedLink',
123
- message: '\n\nWhat now?:',
124
- choices: [
125
- { name: chalk.yellow('🔍 New search'), value: 'new' },
126
- ...result.links.map(link => ({
127
- name: `${chalk.bold(link.name)}: ${chalk.cyan(link.url)}`,
128
- value: link.url
129
- })),
130
- { name: 'Exit', value: 'exit' }
131
- ]
132
- }
133
- ]);
134
-
135
- if (selectedLink === 'new') {
136
- const { input: newInput } = await inquirer.prompt([
137
- {
138
- type: 'input',
139
- name: 'input',
140
- message: 'Enter a URL or search query:',
141
- validate: (input) => input.length > 0
142
- }
143
- ]);
144
- await analyzeInput(newInput, openAIKey);
145
- } else if (selectedLink && selectedLink !== 'exit') {
146
- await analyzeInput(selectedLink, openAIKey);
147
- }
148
-
149
- } catch (error) {
150
- spinner?.fail('Analysis failed');
151
- console.error(chalk.red('Error:'), error);
152
- }
117
+ const spinner = ora('Thinking...').start()
118
+
119
+ try {
120
+ const result = await clai(input, openAIKey)
121
+ spinner.succeed('AHA!')
122
+
123
+ console.log(chalk.green.bold('\n📝 ═══ Summary ═══ :'))
124
+ const formattedContent = formatMarkdownForTerminal(result.summary)
125
+ await animateText(formattedContent)
126
+
127
+ // Prompt user to select a link
128
+ const { selectedLink } = await inquirer.prompt([
129
+ {
130
+ type: 'list',
131
+ name: 'selectedLink',
132
+ message: '\n\nWhat now?:',
133
+ choices: [
134
+ { name: chalk.yellow('🔍 New search'), value: 'new' },
135
+ ...result.links.map((link) => ({
136
+ name: `${chalk.bold(link.name)}: ${chalk.cyan(link.url)}`,
137
+ value: link.url,
138
+ })),
139
+ { name: 'Exit', value: 'exit' },
140
+ ],
141
+ },
142
+ ])
143
+
144
+ if (selectedLink === 'new') {
145
+ const { input: newInput } = await inquirer.prompt([
146
+ {
147
+ type: 'input',
148
+ name: 'input',
149
+ message: 'Enter a URL or search query:',
150
+ validate: (input) => input.length > 0,
151
+ },
152
+ ])
153
+ await analyzeInput(newInput, openAIKey)
154
+ } else if (selectedLink && selectedLink !== 'exit') {
155
+ await analyzeInput(selectedLink, openAIKey)
156
+ }
157
+ } catch (error) {
158
+ spinner?.fail('Analysis failed')
159
+ console.error(chalk.red('Error:'), error)
160
+ }
153
161
  }
154
162
 
155
- main();
163
+ main()
package/src/index.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { scrape } from './scraper';
2
- import { summarizeWebPage as summarize } from './summarizer';
1
+ import { scrape } from './scraper'
2
+ import { summarizeWebPage as summarize } from './summarizer'
3
3
 
4
4
  export interface SummaryOutput {
5
- summary: string;
6
- links: ReadonlyArray<{
7
- name: string;
8
- url: string;
9
- }>;
10
- sources: string[];
5
+ summary: string
6
+ links: ReadonlyArray<{
7
+ name: string
8
+ url: string
9
+ }>
10
+ sources: string[]
11
11
  }
12
12
 
13
13
  /**
@@ -15,7 +15,7 @@ export interface SummaryOutput {
15
15
  * @param input - URL or search query to analyze
16
16
  * @param openAIKey - OpenAI API key
17
17
  * @returns Promise with summary, extracted links, and source URLs
18
- *
18
+ *
19
19
  * @example
20
20
  * ```ts
21
21
  * const result = await clai('https://example.com', 'your-openai-key')
@@ -24,22 +24,25 @@ export interface SummaryOutput {
24
24
  * console.log(result.sources) // Source URLs
25
25
  * ```
26
26
  */
27
- export async function clai(input: string, openAIKey: string): Promise<SummaryOutput> {
28
- const scrapedData = await scrape(input);
29
-
30
- // Combine all content with source attribution
31
- const combinedContent = scrapedData
32
- .map(data => `Content from ${data.url}:\n${data.content}`)
33
- .join('\n\n');
34
-
35
- const result = await summarize(combinedContent, openAIKey);
36
-
37
- return {
38
- summary: result.textual.trim(),
39
- links: result.links,
40
- sources: scrapedData.map(data => data.url)
41
- };
27
+ export async function clai(
28
+ input: string,
29
+ openAIKey: string,
30
+ ): Promise<SummaryOutput> {
31
+ const scrapedData = await scrape(input)
32
+
33
+ // Combine all content with source attribution
34
+ const combinedContent = scrapedData
35
+ .map((data) => `Content from ${data.url}:\n${data.content}`)
36
+ .join('\n\n')
37
+
38
+ const result = await summarize(combinedContent, openAIKey)
39
+
40
+ return {
41
+ summary: result.textual.trim(),
42
+ links: result.links,
43
+ sources: scrapedData.map((data) => data.url),
44
+ }
42
45
  }
43
46
 
44
47
  // Default export for easier importing
45
- export default clai;
48
+ export default clai
package/src/openai.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { once } from '@fettstorch/jule';
2
- import OpenAI from 'openai';
1
+ import { once } from "@fettstorch/jule";
2
+ import OpenAI from "openai";
3
3
 
4
4
  const MAX_INPUT_TOKENS = 10000;
5
5
 
@@ -30,16 +30,16 @@ class OpenAIWrapper {
30
30
  } = {}
31
31
  ): Promise<string> {
32
32
  const truncatedPrompt = truncateContent(prompt);
33
- const { model = 'gpt-4o-turbo', temperature = 0.6 } = options;
33
+ const { model = "gpt-4o", temperature = 0.6 } = options;
34
34
 
35
35
  const response = await this.client.chat.completions.create({
36
36
  model,
37
- messages: [{ role: 'user', content: truncatedPrompt }],
37
+ messages: [{ role: "user", content: truncatedPrompt }],
38
38
  temperature,
39
- max_tokens: 2000
39
+ max_tokens: 2000,
40
40
  });
41
41
 
42
- return response.choices[0]?.message?.content ?? '';
42
+ return response.choices[0]?.message?.content ?? "";
43
43
  }
44
44
 
45
45
  async completeStructured<T>(
@@ -52,41 +52,45 @@ class OpenAIWrapper {
52
52
  }
53
53
  ): Promise<T> {
54
54
  const truncatedPrompt = truncateContent(prompt);
55
- const {
56
- model = 'gpt-4o-mini',
55
+ const {
56
+ model = "gpt-4o",
57
57
  temperature = 1.6,
58
- functionName = 'generate_response',
59
- responseSchema
58
+ functionName = "generate_response",
59
+ responseSchema,
60
60
  } = options;
61
61
 
62
62
  const response = await this.client.chat.completions.create({
63
63
  model,
64
- messages: [{ role: 'user', content: truncatedPrompt }],
64
+ messages: [{ role: "user", content: truncatedPrompt }],
65
65
  temperature,
66
66
  max_tokens: 2000,
67
- functions: [{
68
- name: functionName,
69
- parameters: {
70
- type: 'object',
71
- properties: responseSchema,
72
- required: Object.keys(responseSchema)
73
- }
74
- }],
75
- function_call: { name: functionName }
67
+ functions: [
68
+ {
69
+ name: functionName,
70
+ parameters: {
71
+ type: "object",
72
+ properties: responseSchema,
73
+ required: Object.keys(responseSchema),
74
+ },
75
+ },
76
+ ],
77
+ function_call: { name: functionName },
76
78
  });
77
79
 
78
80
  const functionCall = response.choices[0]?.message?.function_call;
79
81
  if (!functionCall?.arguments) {
80
- throw new Error('No function call arguments received');
82
+ throw new Error("No function call arguments received");
81
83
  }
82
84
 
83
85
  return JSON.parse(functionCall.arguments) as T;
84
86
  }
85
87
  }
86
88
 
87
- export const openaiClient: (apiKey?: string) => OpenAIWrapper = once((apiKey?: string) => {
89
+ export const openaiClient: (apiKey?: string) => OpenAIWrapper = once(
90
+ (apiKey?: string) => {
88
91
  if (!apiKey) {
89
- throw new Error('OPENAI_API_KEY is not set')
92
+ throw new Error("OPENAI_API_KEY is not set");
90
93
  }
91
- return new OpenAIWrapper(apiKey)
92
- });
94
+ return new OpenAIWrapper(apiKey);
95
+ }
96
+ );