@theonlykaks/kaks 0.0.1

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/.env.example ADDED
@@ -0,0 +1,2 @@
1
+ GEMINI_API_KEY=your_own_gemini_api_key
2
+ OPENAI_API_KEY=your_own_openai_api_key
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arush Khasru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/cli.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config';
4
+ import process from 'node:process';
5
+
6
+ import { Command } from 'commander';
7
+
8
+ import { registerAskCommand } from './src/commands/ask.js';
9
+ import { registerConfigCommand } from './src/commands/config.js';
10
+ import { registerExplainCommand } from './src/commands/explain.js';
11
+ import { registerGoCommand } from './src/commands/go.js';
12
+ import { registerInitCommand } from './src/commands/init.js';
13
+ import { registerOpenCommand } from './src/commands/open.js';
14
+ import { registerStartCommand } from './src/commands/start.js';
15
+ import { registerSummarizeCommand } from './src/commands/summarize.js';
16
+ import { CONFIG_PATH, pathExists } from './src/commands/shared.js';
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('kaks')
22
+ .description('AI-powered developer assistant and workspace launcher')
23
+ .version('0.0.1')
24
+ .showHelpAfterError()
25
+ .showSuggestionAfterError()
26
+ .addHelpText('after', `
27
+
28
+ Examples:
29
+ $ kaks init
30
+ $ kaks ask "How do I read a file async in Node.js?"
31
+ $ kaks explain package.json
32
+ $ kaks summarize app.log --tail 200
33
+ $ kaks open myapp
34
+ $ kaks start myapp
35
+ $ kaks go github.com
36
+ `);
37
+
38
+ registerAskCommand(program);
39
+ registerExplainCommand(program);
40
+ registerSummarizeCommand(program);
41
+ registerOpenCommand(program);
42
+ registerStartCommand(program);
43
+ registerGoCommand(program);
44
+ registerConfigCommand(program);
45
+ registerInitCommand(program);
46
+
47
+ if (process.argv.length <= 2) {
48
+ if (!(await pathExists(CONFIG_PATH))) {
49
+ console.log('Welcome to kaks-cli.');
50
+ console.log('It looks like this is your first time. Run: kaks init\n');
51
+ }
52
+
53
+ program.outputHelp();
54
+ } else {
55
+ await program.parseAsync(process.argv);
56
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@theonlykaks/kaks",
3
+ "version": "0.0.1",
4
+ "description": "Hello_to_the_cli",
5
+ "keywords": [],
6
+ "homepage": "https://github.com/ArushKhasru/kaks#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/ArushKhasru/kaks/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ArushKhasru/kaks.git"
13
+ },
14
+ "license": "ISC",
15
+ "author": "kaks",
16
+ "type": "module",
17
+ "main": "index.js",
18
+ "bin": {
19
+ "kaks": "cli.js"
20
+ },
21
+ "scripts": {
22
+ "test": "echo \"Error: no test specified\" && exit 1"
23
+ },
24
+ "dependencies": {
25
+ "axios": "^1.16.1",
26
+ "chalk": "^5.6.2",
27
+ "commander": "^14.0.3",
28
+ "dotenv": "^17.4.2",
29
+ "execa": "^9.6.1",
30
+ "inquirer": "^13.4.3",
31
+ "nodemon": "^3.1.14",
32
+ "ora": "^9.4.0"
33
+ }
34
+ }
package/readme.md ADDED
@@ -0,0 +1,88 @@
1
+ # kaks CLI
2
+
3
+ AI-powered developer assistant and workspace launcher for Windows-first workflows.
4
+
5
+ **Status:** Work in progress. Core commands are implemented and usable.
6
+
7
+ ## What works today
8
+ - `kaks init` interactive setup for provider, model, editor, and optional project registration
9
+ - `kaks ask` AI Q&A with optional clipboard copy
10
+ - `kaks explain` AI file explanations with detail/section controls
11
+ - `kaks summarize` log summarization with local JSON summary and optional AI insight
12
+ - `kaks open` open project editor, browser URLs, and file explorer
13
+ - `kaks start` run configured services concurrently (attached or detached)
14
+ - `kaks go` normalize/open/copy/print URLs quickly
15
+ - `kaks config` set/get/list/add-project/remove-project/edit global config
16
+
17
+ ## Install (local dev)
18
+ ```bash
19
+ git clone https://github.com/ArushKhasru/Kaks.git
20
+ cd kaks
21
+ npm install
22
+ npm link
23
+ ```
24
+
25
+ Then run:
26
+ ```bash
27
+ kaks --help
28
+ ```
29
+
30
+ You can also run directly:
31
+ ```bash
32
+ node cli.js --help
33
+ ```
34
+
35
+ ## Quick start
36
+ ```bash
37
+ kaks init
38
+ kaks ask "How do I read a file async in Node.js?"
39
+ kaks explain package.json
40
+ kaks summarize app.log --tail 200
41
+ kaks open myapp
42
+ kaks start myapp
43
+ kaks go github.com
44
+ ```
45
+
46
+ ## Configuration
47
+ Global config is stored at `~/.kaks/config.json`. Project-local config is `.kaks.json`.
48
+
49
+ Environment variables:
50
+ - `GEMINI_API_KEY`
51
+ - `OPENAI_API_KEY`
52
+
53
+ `kaks init` can also write the key into a local `.env` file.
54
+
55
+ Example config:
56
+ ```jsonc
57
+ {
58
+ "ai": {
59
+ "provider": "gemini",
60
+ "model": "gemini-2.0-flash",
61
+ "temperature": 0.7,
62
+ "maxTokens": 2048
63
+ },
64
+ "projects": {
65
+ "myapp": {
66
+ "path": "D:\\projects\\myapp",
67
+ "browser": "http://localhost:3000",
68
+ "editor": "code",
69
+ "services": [
70
+ { "name": "frontend", "cmd": "npm run dev", "cwd": "./client", "port": 3000 },
71
+ { "name": "backend", "cmd": "npm run dev", "cwd": "./server", "port": 5000 }
72
+ ]
73
+ }
74
+ },
75
+ "defaults": {
76
+ "editor": "code",
77
+ "browser": "default",
78
+ "shell": "powershell"
79
+ }
80
+ }
81
+ ```
82
+
83
+ `kaks ask` reads `ai.context` from `.kaks.json` to enrich prompts.
84
+
85
+ ## AI providers
86
+ - **Gemini**: uses `GEMINI_API_KEY`
87
+ - **OpenAI**: uses `OPENAI_API_KEY`
88
+ - **Ollama**: uses `http://localhost:11434` with no API key
@@ -0,0 +1,81 @@
1
+ import inquirer from 'inquirer';
2
+
3
+ import {
4
+ completeWithAi,
5
+ copyToClipboard,
6
+ handleCommandError,
7
+ loadGlobalConfig,
8
+ loadLocalConfig,
9
+ runWithSpinner,
10
+ } from './shared.js';
11
+
12
+ const SYSTEM_PROMPT = [
13
+ 'You are a senior developer assistant.',
14
+ 'Answer concisely, use markdown when helpful, and include code examples for coding questions.',
15
+ 'Prefer practical, directly usable guidance.',
16
+ ].join(' ');
17
+
18
+ export function registerAskCommand(program) {
19
+ program
20
+ .command('ask [question...]')
21
+ .description('Ask an AI-powered developer question')
22
+ .option('--model <model>', 'Override the configured model')
23
+ .option('--no-stream', 'Disable streaming output')
24
+ .option('--copy', 'Copy the answer to the clipboard')
25
+ .addHelpText('after', `
26
+
27
+ Examples:
28
+ $ kaks ask "How do I read a file async in Node.js?"
29
+ $ kaks ask --model gpt-4o-mini "Explain Promise.allSettled"
30
+ `)
31
+ .action(async (questionParts = [], options) => {
32
+ try {
33
+ await ask(questionParts, options);
34
+ } catch (error) {
35
+ handleCommandError(error);
36
+ }
37
+ });
38
+ }
39
+
40
+ export async function ask(questionParts = [], options = {}) {
41
+ const question = await resolveQuestion(questionParts);
42
+ const config = await loadGlobalConfig();
43
+ const localConfig = await loadLocalConfig();
44
+
45
+ const context = localConfig?.ai?.context
46
+ ? `\n\nProject context:\n${localConfig.ai.context}`
47
+ : '';
48
+
49
+ const answer = await runWithSpinner('Reading...', () => completeWithAi({
50
+ systemPrompt: SYSTEM_PROMPT,
51
+ userPrompt: `${question}${context}`,
52
+ config,
53
+ model: options.model,
54
+ }));
55
+
56
+ console.log(`\n${answer}\n`);
57
+
58
+ if (options.copy) {
59
+ await copyToClipboard(answer);
60
+ console.log('Copied answer to clipboard.');
61
+ }
62
+ }
63
+
64
+ async function resolveQuestion(questionParts) {
65
+ const question = Array.isArray(questionParts) ? questionParts.join(' ').trim() : String(questionParts ?? '').trim();
66
+
67
+ if (question) {
68
+ return question;
69
+ }
70
+
71
+ const answer = await inquirer.prompt([
72
+ {
73
+ type: 'input',
74
+ name: 'question',
75
+ message: 'Question',
76
+ validate: (value) => Boolean(value.trim()) || 'Enter a question.',
77
+ },
78
+ ]);
79
+
80
+ return answer.question.trim();
81
+ }
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+
4
+ import inquirer from 'inquirer';
5
+
6
+ import {
7
+ CONFIG_PATH,
8
+ CliError,
9
+ deleteByPath,
10
+ getByPath,
11
+ handleCommandError,
12
+ launchEditor,
13
+ loadGlobalConfig,
14
+ parseConfigValue,
15
+ resolveUserPath,
16
+ saveGlobalConfig,
17
+ setByPath,
18
+ validateConfigValue,
19
+ } from './shared.js';
20
+
21
+ export function registerConfigCommand(program) {
22
+ const configCommand = program
23
+ .command('config')
24
+ .description('Manage global kaks configuration');
25
+
26
+ configCommand
27
+ .command('set <key> <value>')
28
+ .description('Set a config value using dot notation')
29
+ .action(async (key, rawValue) => {
30
+ try {
31
+ await setConfig(key, rawValue);
32
+ } catch (error) {
33
+ handleCommandError(error);
34
+ }
35
+ });
36
+
37
+ configCommand
38
+ .command('get <key>')
39
+ .description('Read a config value using dot notation')
40
+ .action(async (key) => {
41
+ try {
42
+ await getConfig(key);
43
+ } catch (error) {
44
+ handleCommandError(error);
45
+ }
46
+ });
47
+
48
+ configCommand
49
+ .command('list')
50
+ .description('Print the full global configuration')
51
+ .action(async () => {
52
+ try {
53
+ await listConfig();
54
+ } catch (error) {
55
+ handleCommandError(error);
56
+ }
57
+ });
58
+
59
+ configCommand
60
+ .command('add-project <name>')
61
+ .description('Register a project preset')
62
+ .option('--path <path>', 'Project root path')
63
+ .option('--browser <url>', 'Default URL to open for this project')
64
+ .option('--editor <editor>', 'Editor command for this project')
65
+ .action(async (name, options) => {
66
+ try {
67
+ await addProject(name, options);
68
+ } catch (error) {
69
+ handleCommandError(error);
70
+ }
71
+ });
72
+
73
+ configCommand
74
+ .command('remove-project <name>')
75
+ .description('Remove a project preset')
76
+ .option('--yes', 'Skip confirmation')
77
+ .action(async (name, options) => {
78
+ try {
79
+ await removeProject(name, options);
80
+ } catch (error) {
81
+ handleCommandError(error);
82
+ }
83
+ });
84
+
85
+ configCommand
86
+ .command('edit')
87
+ .description('Open the global config file in your editor')
88
+ .action(async () => {
89
+ try {
90
+ await editConfig();
91
+ } catch (error) {
92
+ handleCommandError(error);
93
+ }
94
+ });
95
+ }
96
+
97
+ export async function setConfig(key, rawValue) {
98
+ const value = parseConfigValue(rawValue);
99
+ validateConfigValue(key, value);
100
+
101
+ const config = await loadGlobalConfig();
102
+ setByPath(config, key, value);
103
+ await saveGlobalConfig(config);
104
+ console.log(`Set ${key}.`);
105
+ }
106
+
107
+ export async function getConfig(key) {
108
+ const config = await loadGlobalConfig();
109
+ const value = getByPath(config, key);
110
+
111
+ if (value === undefined) {
112
+ throw new CliError(`Config key not found: ${key}`);
113
+ }
114
+
115
+ console.log(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
116
+ }
117
+
118
+ export async function listConfig() {
119
+ const config = await loadGlobalConfig();
120
+ console.log(JSON.stringify(config, null, 2));
121
+ }
122
+
123
+ export async function addProject(name, options = {}) {
124
+ const config = await loadGlobalConfig();
125
+ const answers = await promptForMissingProjectFields(name, options);
126
+ const projectPath = resolveUserPath(answers.path);
127
+
128
+ config.projects ??= {};
129
+ config.projects[name] = {
130
+ path: projectPath,
131
+ browser: answers.browser || undefined,
132
+ editor: answers.editor || undefined,
133
+ };
134
+
135
+ await saveGlobalConfig(config);
136
+ console.log(`Added project "${name}" -> ${projectPath}`);
137
+ }
138
+
139
+ export async function removeProject(name, options = {}) {
140
+ const config = await loadGlobalConfig();
141
+
142
+ if (!config.projects?.[name]) {
143
+ throw new CliError(`Project not found: ${name}`);
144
+ }
145
+
146
+ if (!options.yes) {
147
+ const { confirmed } = await inquirer.prompt([
148
+ {
149
+ type: 'confirm',
150
+ name: 'confirmed',
151
+ message: `Remove project "${name}"?`,
152
+ default: false,
153
+ },
154
+ ]);
155
+
156
+ if (!confirmed) {
157
+ console.log('Canceled.');
158
+ return;
159
+ }
160
+ }
161
+
162
+ deleteByPath(config, `projects.${name}`);
163
+ await saveGlobalConfig(config);
164
+ console.log(`Removed project "${name}".`);
165
+ }
166
+
167
+ export async function editConfig() {
168
+ const config = await loadGlobalConfig();
169
+ await saveGlobalConfig(config);
170
+ await launchEditor(config.defaults?.editor ?? 'code', CONFIG_PATH);
171
+ console.log(`Opening ${CONFIG_PATH}`);
172
+ }
173
+
174
+ async function promptForMissingProjectFields(name, options) {
175
+ const questions = [];
176
+
177
+ if (!options.path) {
178
+ questions.push({
179
+ type: 'input',
180
+ name: 'path',
181
+ message: `Path for ${name}`,
182
+ default: process.cwd(),
183
+ filter: (value) => path.resolve(value),
184
+ validate: (value) => Boolean(value.trim()) || 'Enter a project path.',
185
+ });
186
+ }
187
+
188
+ if (!options.browser) {
189
+ questions.push({
190
+ type: 'input',
191
+ name: 'browser',
192
+ message: 'Default browser URL',
193
+ default: '',
194
+ });
195
+ }
196
+
197
+ if (!options.editor) {
198
+ questions.push({
199
+ type: 'input',
200
+ name: 'editor',
201
+ message: 'Editor command',
202
+ default: '',
203
+ });
204
+ }
205
+
206
+ const answers = questions.length ? await inquirer.prompt(questions) : {};
207
+
208
+ return {
209
+ path: options.path ?? answers.path,
210
+ browser: options.browser ?? answers.browser,
211
+ editor: options.editor ?? answers.editor,
212
+ };
213
+ }
@@ -0,0 +1,79 @@
1
+ import { CliError, completeWithAi, detectLanguage, handleCommandError, loadGlobalConfig, readTextFileWithLimits, runWithSpinner } from './shared.js';
2
+
3
+ const SYSTEM_PROMPT = [
4
+ 'You are a code and configuration file explainer.',
5
+ 'Explain the file in plain English for a developer.',
6
+ 'Cover purpose, structure, important sections, and any risks or noteworthy details.',
7
+ ].join(' ');
8
+
9
+ const VALID_DETAIL_LEVELS = new Set(['low', 'medium', 'high']);
10
+
11
+ export function registerExplainCommand(program) {
12
+ program
13
+ .command('explain <filepath>')
14
+ .description('Explain a source, config, or text file with AI')
15
+ .option('--detail <level>', 'Depth of explanation: low, medium, or high', 'medium')
16
+ .option('--section <name>', 'Explain only a named section or topic')
17
+ .option('--model <model>', 'Override the configured model')
18
+ .addHelpText('after', `
19
+
20
+ Examples:
21
+ $ kaks explain docker-compose.yml
22
+ $ kaks explain package.json --detail high
23
+ $ kaks explain src/app.js --section middleware
24
+ `)
25
+ .action(async (filepath, options) => {
26
+ try {
27
+ await explain(filepath, options);
28
+ } catch (error) {
29
+ handleCommandError(error);
30
+ }
31
+ });
32
+ }
33
+
34
+ export async function explain(filepath, options = {}) {
35
+ const detail = String(options.detail ?? 'medium').toLowerCase();
36
+ if (!VALID_DETAIL_LEVELS.has(detail)) {
37
+ throw new CliError('Invalid detail level. Use one of: low, medium, high.');
38
+ }
39
+
40
+ const file = await readTextFileWithLimits(filepath);
41
+
42
+ if (!file.content.trim()) {
43
+ throw new CliError(`File is empty: ${filepath}`);
44
+ }
45
+
46
+ if (file.warned) {
47
+ console.warn(`Large file: ${file.displayPath} (${file.size} bytes).`);
48
+ }
49
+
50
+ if (file.truncated) {
51
+ console.warn('File exceeds 500KB, so only the first 500KB will be explained.');
52
+ }
53
+
54
+ const config = await loadGlobalConfig();
55
+ const language = detectLanguage(file.absolutePath);
56
+ const section = options.section ? `\nFocus only on this section or topic: ${options.section}` : '';
57
+
58
+ const prompt = [
59
+ `File: ${file.displayPath}`,
60
+ `Detected format: ${language}`,
61
+ `Detail level: ${detail}`,
62
+ section,
63
+ '',
64
+ 'Content:',
65
+ `\`\`\`${language}`,
66
+ file.content,
67
+ '```',
68
+ ].filter(Boolean).join('\n');
69
+
70
+ const explanation = await runWithSpinner(`Reading ${file.displayPath}...`, () => completeWithAi({
71
+ systemPrompt: SYSTEM_PROMPT,
72
+ userPrompt: prompt,
73
+ config,
74
+ model: options.model,
75
+ }));
76
+
77
+ console.log(`\nFile Explanation: ${file.displayPath}\n`);
78
+ console.log(`${explanation}\n`);
79
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ assertValidUrl,
3
+ copyToClipboard,
4
+ handleCommandError,
5
+ normalizeUrl,
6
+ openTarget,
7
+ } from './shared.js';
8
+
9
+ export function registerGoCommand(program) {
10
+ program
11
+ .command('go <url>')
12
+ .description('Normalize and open a URL quickly')
13
+ .option('--browser <name>', 'Browser override: chrome, firefox, edge, or default', 'default')
14
+ .option('--copy', 'Copy the normalized URL instead of opening it')
15
+ .option('--print', 'Print the normalized URL instead of opening it')
16
+ .addHelpText('after', `
17
+
18
+ Examples:
19
+ $ kaks go github.com
20
+ $ kaks go github.com/kaksKhasru/kaks
21
+ $ kaks go localhost:3000
22
+ $ kaks go docs.google.com --copy
23
+ `)
24
+ .action(async (url, options) => {
25
+ try {
26
+ await go(url, options);
27
+ } catch (error) {
28
+ handleCommandError(error);
29
+ }
30
+ });
31
+ }
32
+
33
+ export async function go(url, options = {}) {
34
+ const normalizedUrl = normalizeUrl(url);
35
+ assertValidUrl(normalizedUrl);
36
+
37
+ if (options.print) {
38
+ console.log(normalizedUrl);
39
+ return;
40
+ }
41
+
42
+ if (options.copy) {
43
+ await copyToClipboard(normalizedUrl);
44
+ console.log(`Copied to clipboard: ${normalizedUrl}`);
45
+ return;
46
+ }
47
+
48
+ await openTarget(normalizedUrl, options.browser);
49
+ const suffix = options.browser && options.browser !== 'default' ? ` (${options.browser})` : '';
50
+ console.log(`Opening ${normalizedUrl}${suffix}`);
51
+ }