scorpion-cli 0.1.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.
- package/README.md +152 -0
- package/install.ps1 +22 -0
- package/install.sh +19 -0
- package/package.json +44 -0
- package/src/agent.js +226 -0
- package/src/commands.js +193 -0
- package/src/index.js +86 -0
- package/src/ollama.js +126 -0
- package/src/registry.js +112 -0
- package/src/tools/context.js +432 -0
- package/src/tools/deep-research.js +425 -0
- package/src/tools/documents.js +299 -0
- package/src/tools/filesystem.js +402 -0
- package/src/tools/shell.js +134 -0
- package/src/tools/system.js +379 -0
- package/src/tools/web.js +433 -0
- package/src/ui/charts.js +213 -0
- package/src/ui/export.js +141 -0
- package/src/ui/formatter.js +365 -0
- package/src/ui/images.js +153 -0
- package/src/ui/panels.js +150 -0
- package/src/ui/progress.js +131 -0
- package/src/ui/repl.js +477 -0
- package/src/ui/spinner.js +99 -0
- package/src/ui/table.js +145 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scorpion CLI
|
|
5
|
+
* An agentic AI assistant powered by Ollama
|
|
6
|
+
* Inspired by Claude Code & Gemini CLI
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { program } from 'commander';
|
|
10
|
+
import { startREPL } from './ui/repl.js';
|
|
11
|
+
import { query } from './agent.js';
|
|
12
|
+
import { isModelAvailable, DEFAULT_MODEL } from './ollama.js';
|
|
13
|
+
import {
|
|
14
|
+
displayBanner,
|
|
15
|
+
showError,
|
|
16
|
+
showSuccess,
|
|
17
|
+
showInfo,
|
|
18
|
+
colors
|
|
19
|
+
} from './ui/formatter.js';
|
|
20
|
+
|
|
21
|
+
// Package info
|
|
22
|
+
const VERSION = '0.1.0';
|
|
23
|
+
const NAME = 'scorpion';
|
|
24
|
+
|
|
25
|
+
// Setup CLI
|
|
26
|
+
program
|
|
27
|
+
.name(NAME)
|
|
28
|
+
.version(VERSION)
|
|
29
|
+
.description('🦂 Scorpion - Agentic AI CLI powered by Ollama')
|
|
30
|
+
.option('-m, --model <model>', 'Ollama model to use', DEFAULT_MODEL)
|
|
31
|
+
.option('-q, --query <query>', 'Run a single query and exit')
|
|
32
|
+
.option('--think', 'Show AI thinking process')
|
|
33
|
+
.option('--check', 'Check Ollama connection and exit');
|
|
34
|
+
|
|
35
|
+
program.parse();
|
|
36
|
+
|
|
37
|
+
const options = program.opts();
|
|
38
|
+
|
|
39
|
+
// Main entry point
|
|
40
|
+
async function main() {
|
|
41
|
+
// Handle --check flag
|
|
42
|
+
if (options.check) {
|
|
43
|
+
showInfo('Checking Ollama connection...');
|
|
44
|
+
try {
|
|
45
|
+
const available = await isModelAvailable(options.model);
|
|
46
|
+
if (available) {
|
|
47
|
+
showSuccess(`Ollama is running. Model '${options.model}' is available.`);
|
|
48
|
+
} else {
|
|
49
|
+
showError(`Model '${options.model}' not found.`);
|
|
50
|
+
showInfo(`Run: ollama pull ${options.model}`);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
showError('Could not connect to Ollama.');
|
|
54
|
+
showInfo('Make sure Ollama is running: ollama serve');
|
|
55
|
+
}
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle --query flag (single query mode)
|
|
60
|
+
if (options.query) {
|
|
61
|
+
try {
|
|
62
|
+
console.log(colors.dim('\n Processing...\n'));
|
|
63
|
+
|
|
64
|
+
const result = await query(options.query);
|
|
65
|
+
|
|
66
|
+
console.log(colors.white(' ' + result.content.split('\n').join('\n ')));
|
|
67
|
+
console.log();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
showError(error.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Start interactive REPL
|
|
76
|
+
await startREPL({
|
|
77
|
+
model: options.model,
|
|
78
|
+
showThinkingOutput: options.think || false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Run
|
|
83
|
+
main().catch((error) => {
|
|
84
|
+
showError(error.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
package/src/ollama.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama Client Wrapper
|
|
3
|
+
* Provides a centralized interface to the Ollama API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Ollama } from 'ollama';
|
|
7
|
+
|
|
8
|
+
// Default configuration
|
|
9
|
+
const DEFAULT_MODEL = process.env.OLLAMA_MODEL || 'qwen3.5:0.8b';
|
|
10
|
+
const DEFAULT_HOST = 'http://localhost:11434';
|
|
11
|
+
|
|
12
|
+
// Create the Ollama client
|
|
13
|
+
const ollama = new Ollama({ host: process.env.OLLAMA_HOST || DEFAULT_HOST });
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Chat with the model using tool calling
|
|
17
|
+
* @param {Object} options - Chat options
|
|
18
|
+
* @param {string} options.model - Model name
|
|
19
|
+
* @param {Array} options.messages - Conversation messages
|
|
20
|
+
* @param {Array} options.tools - Available tools
|
|
21
|
+
* @param {boolean} options.think - Enable thinking mode
|
|
22
|
+
* @param {boolean} options.stream - Enable streaming
|
|
23
|
+
* @returns {Promise} - Chat response or async iterator for streaming
|
|
24
|
+
*/
|
|
25
|
+
export async function chat(options) {
|
|
26
|
+
const {
|
|
27
|
+
model = DEFAULT_MODEL,
|
|
28
|
+
messages,
|
|
29
|
+
tools = [],
|
|
30
|
+
think = true,
|
|
31
|
+
stream = false,
|
|
32
|
+
format = undefined,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
return ollama.chat({
|
|
36
|
+
model,
|
|
37
|
+
messages,
|
|
38
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
39
|
+
think,
|
|
40
|
+
stream,
|
|
41
|
+
format,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Web search using Ollama's API (requires API key)
|
|
47
|
+
* @param {string} query - Search query
|
|
48
|
+
* @param {number} maxResults - Maximum results to return
|
|
49
|
+
* @returns {Promise<Array>} - Search results
|
|
50
|
+
*/
|
|
51
|
+
export async function webSearch(query, maxResults = 5) {
|
|
52
|
+
try {
|
|
53
|
+
const results = await ollama.webSearch({ query, max_results: maxResults });
|
|
54
|
+
return results;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Fallback: return error message if API key not set
|
|
57
|
+
if (error.message?.includes('unauthorized') || error.message?.includes('API key')) {
|
|
58
|
+
return {
|
|
59
|
+
error: 'Web search requires OLLAMA_API_KEY. Set it in your environment.',
|
|
60
|
+
suggestion: 'Get your API key at https://ollama.com/settings/keys'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch a web page using Ollama's API
|
|
69
|
+
* @param {string} url - URL to fetch
|
|
70
|
+
* @returns {Promise<Object>} - Page content
|
|
71
|
+
*/
|
|
72
|
+
export async function webFetch(url) {
|
|
73
|
+
try {
|
|
74
|
+
const result = await ollama.webFetch({ url });
|
|
75
|
+
return result;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.message?.includes('unauthorized') || error.message?.includes('API key')) {
|
|
78
|
+
return {
|
|
79
|
+
error: 'Web fetch requires OLLAMA_API_KEY. Set it in your environment.',
|
|
80
|
+
suggestion: 'Get your API key at https://ollama.com/settings/keys'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* List available models
|
|
89
|
+
* @returns {Promise<Array>} - List of models
|
|
90
|
+
*/
|
|
91
|
+
export async function listModels() {
|
|
92
|
+
const response = await ollama.list();
|
|
93
|
+
return response.models;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a model is available
|
|
98
|
+
* @param {string} modelName - Model to check
|
|
99
|
+
* @returns {Promise<boolean>} - True if available
|
|
100
|
+
*/
|
|
101
|
+
export async function isModelAvailable(modelName = DEFAULT_MODEL) {
|
|
102
|
+
try {
|
|
103
|
+
const models = await listModels();
|
|
104
|
+
return models.some((m) => m.name === modelName || (
|
|
105
|
+
!modelName.includes(':') && m.name.split(':')[0] === modelName
|
|
106
|
+
));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pull a model if not available
|
|
114
|
+
* @param {string} modelName - Model to pull
|
|
115
|
+
* @param {Function} onProgress - Progress callback
|
|
116
|
+
*/
|
|
117
|
+
export async function pullModel(modelName, onProgress) {
|
|
118
|
+
const stream = await ollama.pull({ model: modelName, stream: true });
|
|
119
|
+
for await (const part of stream) {
|
|
120
|
+
if (onProgress) {
|
|
121
|
+
onProgress(part);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { ollama, DEFAULT_MODEL, DEFAULT_HOST };
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry
|
|
3
|
+
* Manages all available tools and their schemas for the agent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as shellTools from './tools/shell.js';
|
|
7
|
+
import * as filesystemTools from './tools/filesystem.js';
|
|
8
|
+
import * as systemTools from './tools/system.js';
|
|
9
|
+
import * as webTools from './tools/web.js';
|
|
10
|
+
import * as documentTools from './tools/documents.js';
|
|
11
|
+
import * as contextTools from './tools/context.js';
|
|
12
|
+
import * as deepResearchTools from './tools/deep-research.js';
|
|
13
|
+
|
|
14
|
+
// Collect all tool modules
|
|
15
|
+
const toolModules = {
|
|
16
|
+
...shellTools,
|
|
17
|
+
...filesystemTools,
|
|
18
|
+
...systemTools,
|
|
19
|
+
...webTools,
|
|
20
|
+
...documentTools,
|
|
21
|
+
...contextTools,
|
|
22
|
+
...deepResearchTools,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get all tool schemas for Ollama API
|
|
27
|
+
* @returns {Array} - Array of tool definitions
|
|
28
|
+
*/
|
|
29
|
+
export function getAllToolSchemas() {
|
|
30
|
+
return Object.values(toolModules)
|
|
31
|
+
.filter(tool => tool.schema)
|
|
32
|
+
.map(tool => tool.schema);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a tool executor by name
|
|
37
|
+
* @param {string} name - Tool name
|
|
38
|
+
* @returns {Function|null} - Tool executor function
|
|
39
|
+
*/
|
|
40
|
+
export function getToolExecutor(name) {
|
|
41
|
+
const tool = Object.values(toolModules).find(t => t.schema?.function?.name === name);
|
|
42
|
+
return tool?.execute || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute a tool by name with arguments
|
|
47
|
+
* @param {string} name - Tool name
|
|
48
|
+
* @param {Object} args - Tool arguments
|
|
49
|
+
* @returns {Promise<string>} - Tool result as string
|
|
50
|
+
*/
|
|
51
|
+
export async function executeTool(name, args) {
|
|
52
|
+
const executor = getToolExecutor(name);
|
|
53
|
+
if (!executor) {
|
|
54
|
+
return JSON.stringify({ error: `Tool '${name}' not found` });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await executor(args);
|
|
59
|
+
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
error: error.message,
|
|
63
|
+
tool: name,
|
|
64
|
+
args
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get tool categories for help display
|
|
71
|
+
* @returns {Object} - Tools grouped by category
|
|
72
|
+
*/
|
|
73
|
+
export function getToolsByCategory() {
|
|
74
|
+
return {
|
|
75
|
+
'Shell': Object.values(shellTools).filter(t => t.schema).map(t => ({
|
|
76
|
+
name: t.schema.function.name,
|
|
77
|
+
description: t.schema.function.description
|
|
78
|
+
})),
|
|
79
|
+
'File System': Object.values(filesystemTools).filter(t => t.schema).map(t => ({
|
|
80
|
+
name: t.schema.function.name,
|
|
81
|
+
description: t.schema.function.description
|
|
82
|
+
})),
|
|
83
|
+
'System': Object.values(systemTools).filter(t => t.schema).map(t => ({
|
|
84
|
+
name: t.schema.function.name,
|
|
85
|
+
description: t.schema.function.description
|
|
86
|
+
})),
|
|
87
|
+
'Web': Object.values(webTools).filter(t => t.schema).map(t => ({
|
|
88
|
+
name: t.schema.function.name,
|
|
89
|
+
description: t.schema.function.description
|
|
90
|
+
})),
|
|
91
|
+
'Documents': Object.values(documentTools).filter(t => t.schema).map(t => ({
|
|
92
|
+
name: t.schema.function.name,
|
|
93
|
+
description: t.schema.function.description
|
|
94
|
+
})),
|
|
95
|
+
'Context': Object.values(contextTools).filter(t => t.schema).map(t => ({
|
|
96
|
+
name: t.schema.function.name,
|
|
97
|
+
description: t.schema.function.description
|
|
98
|
+
})),
|
|
99
|
+
'Deep Research': Object.values(deepResearchTools).filter(t => t.schema).map(t => ({
|
|
100
|
+
name: t.schema.function.name,
|
|
101
|
+
description: t.schema.function.description
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get total number of available tools
|
|
108
|
+
* @returns {number} - Tool count
|
|
109
|
+
*/
|
|
110
|
+
export function getToolCount() {
|
|
111
|
+
return getAllToolSchemas().length;
|
|
112
|
+
}
|