askai-free 0.0.0-stage → 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/bin/askai.js +147 -0
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # Ask AI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Use AskAI.free models without leaving the terminal.
4
+
5
+ ```sh
6
+ npm install --global askai-free
7
+ askai config set-key
8
+ askai "Explain the difference between a process and a thread"
9
+ git diff | askai "Review this diff"
10
+ askai models
11
+ askai usage
12
+ ```
13
+
14
+ Create an API key in your <https://askai.free/account>. For CI, set `ASKAI_API_KEY`.
package/bin/askai.js ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const pkg = require('../package.json');
8
+
9
+ const defaultBase = 'https://askai.free';
10
+ const defaultModel = 'chatgpt-5-6-sol';
11
+
12
+ function configFile() {
13
+ const root = process.env.XDG_CONFIG_HOME || (process.platform === 'win32' && process.env.APPDATA) || path.join(os.homedir(), '.config');
14
+ return path.join(root, 'ask-ai', 'config.json');
15
+ }
16
+
17
+ function loadConfig() {
18
+ try { return JSON.parse(fs.readFileSync(configFile(), 'utf8')); }
19
+ catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
20
+ }
21
+
22
+ function saveConfig(config) {
23
+ fs.mkdirSync(path.dirname(configFile()), { recursive: true, mode: 0o700 });
24
+ fs.writeFileSync(configFile(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
25
+ try { fs.chmodSync(configFile(), 0o600); } catch {}
26
+ }
27
+
28
+ function secret(label) {
29
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) throw new Error('Interactive key entry requires a terminal; use ASKAI_API_KEY in scripts.');
30
+ return new Promise((resolve, reject) => {
31
+ let value = '';
32
+ process.stdout.write(label);
33
+ process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8');
34
+ const done = (error) => {
35
+ process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.off('data', input); process.stdout.write('\n');
36
+ error ? reject(error) : resolve(value.trim());
37
+ };
38
+ const input = (key) => {
39
+ if (key === '\u0003') return done(new Error('Cancelled.'));
40
+ if (key === '\r' || key === '\n') return done();
41
+ if (key === '\u007f' || key === '\b') value = value.slice(0, -1);
42
+ else if (key >= ' ') value += key;
43
+ };
44
+ process.stdin.on('data', input);
45
+ });
46
+ }
47
+
48
+ function auth() {
49
+ const config = loadConfig();
50
+ return {
51
+ key: process.env.ASKAI_API_KEY || config.apiKey,
52
+ model: process.env.ASKAI_MODEL || config.model || defaultModel,
53
+ base: (process.env.ASKAI_BASE_URL || config.baseUrl || defaultBase).replace(/\/$/, ''),
54
+ };
55
+ }
56
+
57
+ async function api(endpoint, options = {}) {
58
+ const current = auth();
59
+ if (!current.key) throw new Error('No API key found. Run `askai config set-key` or set ASKAI_API_KEY.');
60
+ const response = await fetch(`${current.base}/v1/${endpoint}`, {
61
+ method: options.method || 'GET',
62
+ headers: { Authorization: `Bearer ${current.key}`, 'Content-Type': 'application/json', 'User-Agent': `ask-ai/${pkg.version}` },
63
+ body: options.body ? JSON.stringify(options.body) : undefined,
64
+ signal: AbortSignal.timeout(120_000),
65
+ });
66
+ const data = await response.json().catch(() => ({}));
67
+ if (!response.ok) throw new Error(data.error?.message || `Ask AI returned HTTP ${response.status}.`);
68
+ return data;
69
+ }
70
+
71
+ async function stdin() {
72
+ if (process.stdin.isTTY) return '';
73
+ const chunks = [];
74
+ for await (const chunk of process.stdin) chunks.push(chunk);
75
+ return Buffer.concat(chunks).toString('utf8').trim();
76
+ }
77
+
78
+ function help() {
79
+ return `Ask AI ${pkg.version}
80
+
81
+ Usage:
82
+ askai "Explain this error"
83
+ command | askai "Review this output"
84
+ askai models [--json]
85
+ askai usage [--json]
86
+ askai config set-key|clear-key|show
87
+
88
+ Options:
89
+ -m, --model MODEL
90
+ -s, --system MESSAGE
91
+ --max-tokens N
92
+ --json`;
93
+ }
94
+
95
+ function parse(args) {
96
+ const options = { json: false, maxTokens: 1000, words: [] };
97
+ for (let i = 0; i < args.length; i += 1) {
98
+ const arg = args[i];
99
+ if (arg === '--json') options.json = true;
100
+ else if (arg === '--model' || arg === '-m') options.model = args[++i];
101
+ else if (arg === '--system' || arg === '-s') options.system = args[++i];
102
+ else if (arg === '--max-tokens') options.maxTokens = Number(args[++i]);
103
+ else options.words.push(arg);
104
+ }
105
+ if (!Number.isInteger(options.maxTokens) || options.maxTokens < 1 || options.maxTokens > 8000) throw new Error('--max-tokens must be an integer from 1 to 8000.');
106
+ return options;
107
+ }
108
+
109
+ async function main() {
110
+ const args = process.argv.slice(2);
111
+ if (!args.length || args.includes('--help') || args.includes('-h')) return process.stdout.write(`${help()}\n`);
112
+ if (args.includes('--version') || args.includes('-v')) return process.stdout.write(`${pkg.version}\n`);
113
+ const command = ['models', 'usage', 'config', 'ask'].includes(args[0]) ? args.shift() : 'ask';
114
+ const options = parse(args);
115
+ if (command === 'config') {
116
+ const action = options.words[0] || 'show';
117
+ const config = loadConfig();
118
+ if (action === 'set-key') {
119
+ const key = await secret('Ask AI API key: ');
120
+ if (!key.startsWith('sk_askai_')) throw new Error('That does not look like an Ask AI API key.');
121
+ config.apiKey = key; saveConfig(config); process.stdout.write(`API key saved in ${configFile()}\n`);
122
+ } else if (action === 'clear-key') {
123
+ delete config.apiKey; saveConfig(config); process.stdout.write('API key removed.\n');
124
+ } else {
125
+ const current = auth();
126
+ process.stdout.write(`Config: ${configFile()}\nAPI key: ${current.key ? `${current.key.slice(0, 9)}…${current.key.slice(-4)}` : 'not set'}\nModel: ${current.model}\n`);
127
+ }
128
+ return;
129
+ }
130
+ if (command === 'models' || command === 'usage') {
131
+ const data = await api(command);
132
+ if (options.json) return process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
133
+ if (command === 'models') return process.stdout.write(`${(data.data || []).map((item) => `${item.id}\t${item.name || ''}`).join('\n')}\n`);
134
+ return process.stdout.write(`Plan: ${data.plan || '-'}\nTokens remaining: ${data.token_credits_remaining ?? '-'}\n`);
135
+ }
136
+ const prompt = [options.words.join(' ').trim(), await stdin()].filter(Boolean).join('\n\n');
137
+ if (!prompt) throw new Error('Add a prompt or pipe text into askai.');
138
+ const current = auth();
139
+ const messages = [...(options.system ? [{ role: 'system', content: options.system }] : []), { role: 'user', content: prompt }];
140
+ const data = await api('chat/completions', { method: 'POST', body: { model: options.model || current.model, messages, max_tokens: options.maxTokens, stream: false } });
141
+ if (options.json) process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
142
+ else process.stdout.write(`${data.choices?.[0]?.message?.content || ''}\n`);
143
+ }
144
+
145
+ if (require.main === module) main().catch((error) => { process.stderr.write(`askai: ${error.message}\n`); process.exitCode = 1; });
146
+
147
+ module.exports = { parse };
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "askai-free",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Use ChatGPT, Claude, and DeepSeek through Ask AI from your terminal.",
5
+ "bin": { "askai": "bin/askai.js", "ask-ai": "bin/askai.js" },
6
+ "files": ["bin", "README.md"],
7
+ "scripts": { "test": "node --check bin/askai.js && node bin/askai.js --version", "prepublishOnly": "npm test" },
8
+ "engines": { "node": ">=20" },
9
+ "keywords": ["ai", "chatgpt", "claude", "deepseek", "terminal", "cli", "ask-ai"],
10
+ "homepage": "https://askai.free/cli",
11
+ "license": "UNLICENSED",
12
+ "author": "AskAI.free",
13
+ "publishConfig": { "access": "public" }
14
+ }