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/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 };
@@ -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
+ }