@ayoxx/kundex 0.1.7 → 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.
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/config.ts +19 -3
- package/src/index.ts +551 -17
- package/src/repl.ts +403 -34
- package/src/sdk.ts +320 -35
- package/src/tools.ts +171 -13
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Kundex CLI & SDK
|
|
2
2
|
|
|
3
3
|
The Kundex terminal AI coding agent — usable as a global CLI (`kundex`) or as
|
|
4
|
-
an SDK (`import { ... } from "kundex"`).
|
|
4
|
+
an SDK (`import { ... } from "@ayoxx/kundex"`).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npm install -g kundex
|
|
9
|
+
npm install -g @ayoxx/kundex
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
@@ -20,15 +20,15 @@ kundex # start an interactive agent session
|
|
|
20
20
|
|
|
21
21
|
This export ships a GitHub Actions workflow at
|
|
22
22
|
`.github/workflows/publish-cli.yml` that publishes to npm whenever you push a
|
|
23
|
-
git tag matching `cli-v*` (e.g. `v0.2.0`):
|
|
23
|
+
git tag matching `cli-v*` (e.g. `cli-v0.2.0`):
|
|
24
24
|
|
|
25
25
|
1. Push this directory to a GitHub repo.
|
|
26
26
|
2. In the repo's Settings > Secrets and variables > Actions, add an
|
|
27
27
|
`NPM_TOKEN` secret (an npm automation token with publish rights).
|
|
28
28
|
3. Bump `version` in `package.json`, commit, then:
|
|
29
29
|
```bash
|
|
30
|
-
git tag v0.2.0
|
|
31
|
-
git push origin v0.2.0
|
|
30
|
+
git tag cli-v0.2.0
|
|
31
|
+
git push origin cli-v0.2.0
|
|
32
32
|
```
|
|
33
33
|
4. The workflow installs dependencies, type-checks, and runs `npm publish`.
|
|
34
34
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -33,8 +33,24 @@ export function saveConfig(config: Partial<KundexConfig>): KundexConfig {
|
|
|
33
33
|
return merged;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export const KUNDEX_BASE_URL = "https://
|
|
36
|
+
export const KUNDEX_BASE_URL = "https://api.kundex.com.ng";
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the base URL to use for API calls.
|
|
40
|
+
*
|
|
41
|
+
* Priority:
|
|
42
|
+
* 1. KUNDEX_BASE_URL environment variable (useful for local dev)
|
|
43
|
+
* 2. Stored baseUrl from config (~/.kundex/config.json)
|
|
44
|
+
* 3. Hard-coded default: https://api.kundex.com.ng
|
|
45
|
+
*
|
|
46
|
+
* Previously, this function ignored the stored baseUrl completely.
|
|
47
|
+
* That made `kundex config set baseUrl <url>` a no-op, which broke
|
|
48
|
+
* local development and self-hosted deployments.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveBaseUrl(config: KundexConfig): string | null {
|
|
51
|
+
return (
|
|
52
|
+
process.env.KUNDEX_BASE_URL?.trim() ||
|
|
53
|
+
config.baseUrl?.trim() ||
|
|
54
|
+
KUNDEX_BASE_URL
|
|
55
|
+
);
|
|
40
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,41 +1,575 @@
|
|
|
1
|
-
import { loadConfig, resolveBaseUrl } from "./config";
|
|
1
|
+
import { loadConfig, saveConfig, resolveBaseUrl } from "./config";
|
|
2
2
|
import { runLogin } from "./login";
|
|
3
3
|
import { runRepl } from "./repl";
|
|
4
|
+
import { Kundex } from "./sdk";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const VERSION = "0.2.0";
|
|
9
|
+
|
|
10
|
+
// ─── Help text ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const HELP = `
|
|
13
|
+
\x1b[1m\x1b[36mkundex\x1b[0m — AI coding agent CLI v${VERSION}
|
|
14
|
+
|
|
15
|
+
\x1b[1mUsage:\x1b[0m
|
|
16
|
+
kundex [command] [options]
|
|
17
|
+
|
|
18
|
+
\x1b[1mAuthentication:\x1b[0m
|
|
19
|
+
login Save your Kundex API key
|
|
20
|
+
logout Remove saved credentials
|
|
21
|
+
|
|
22
|
+
\x1b[1mAgent:\x1b[0m
|
|
23
|
+
(no command) Start the interactive AI coding agent REPL
|
|
24
|
+
run <message> Run a single agent turn (non-interactive)
|
|
25
|
+
|
|
26
|
+
\x1b[1mWorkspaces:\x1b[0m
|
|
27
|
+
workspace list List all your agent workspaces
|
|
28
|
+
workspace new [cwd] Create a new workspace (default: current dir)
|
|
29
|
+
workspace delete <id> Delete a workspace
|
|
30
|
+
|
|
31
|
+
\x1b[1mFiles:\x1b[0m
|
|
32
|
+
files list <wsId> List files in a workspace
|
|
33
|
+
files get <wsId> <path> Print a file's content to stdout
|
|
34
|
+
files push <wsId> <localPath> Upload a local file to a workspace
|
|
35
|
+
files push <wsId> <localPath> <remotePath>
|
|
36
|
+
files pull <wsId> <remotePath> Download a workspace file to stdout (or --out)
|
|
37
|
+
|
|
38
|
+
\x1b[1mModels:\x1b[0m
|
|
39
|
+
models List available models
|
|
40
|
+
|
|
41
|
+
\x1b[1mConfiguration:\x1b[0m
|
|
42
|
+
config show Show current configuration
|
|
43
|
+
config set <key> <val> Set a config value
|
|
44
|
+
Keys: model, baseUrl
|
|
45
|
+
|
|
46
|
+
\x1b[1mOther:\x1b[0m
|
|
47
|
+
help, --help, -h Show this help
|
|
48
|
+
version, --version, -v Show version
|
|
49
|
+
|
|
50
|
+
\x1b[1mExamples:\x1b[0m
|
|
51
|
+
kundex login
|
|
52
|
+
kundex # start REPL in current directory
|
|
53
|
+
kundex run "Fix all TypeScript errors"
|
|
54
|
+
kundex models
|
|
55
|
+
kundex workspace list
|
|
56
|
+
kundex workspace new /path/to/project
|
|
57
|
+
kundex workspace delete 42
|
|
58
|
+
kundex files list 17
|
|
59
|
+
kundex files get 17 src/index.ts
|
|
60
|
+
kundex files push 17 ./README.md
|
|
61
|
+
kundex files pull 17 src/index.ts --out ./downloaded.ts
|
|
62
|
+
kundex config set model openai/gpt-oss-120b
|
|
63
|
+
kundex config set baseUrl https://api.kundex.com.ng
|
|
64
|
+
`.trim();
|
|
65
|
+
|
|
66
|
+
// ─── Colour helpers ───────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const c = {
|
|
69
|
+
reset: "\x1b[0m",
|
|
70
|
+
bold: "\x1b[1m",
|
|
71
|
+
dim: "\x1b[2m",
|
|
72
|
+
cyan: "\x1b[36m",
|
|
73
|
+
green: "\x1b[32m",
|
|
74
|
+
yellow: "\x1b[33m",
|
|
75
|
+
red: "\x1b[31m",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function ok(msg: string) { console.log(`${c.green}${c.bold} ✓${c.reset} ${msg}`); }
|
|
79
|
+
function errMsg(msg: string) { console.error(`${c.red}${c.bold} ✗${c.reset} ${msg}`); }
|
|
80
|
+
function dim(msg: string) { console.log(`${c.dim} ${msg}${c.reset}`); }
|
|
81
|
+
function header(msg: string) { console.log(`\n${c.bold}${c.cyan} ${msg}${c.reset}`); }
|
|
82
|
+
|
|
83
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
86
|
+
heavstal: "Heavstal",
|
|
87
|
+
gemini: "Gemini",
|
|
88
|
+
groq: "Groq",
|
|
89
|
+
mistral: "Mistral",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
async function cmdModels(config: ReturnType<typeof loadConfig>, baseUrl: string) {
|
|
93
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
94
|
+
header("Available models");
|
|
95
|
+
try {
|
|
96
|
+
const models = await kundex.models.list();
|
|
97
|
+
if (!models.length) { dim("No models found."); return; }
|
|
98
|
+
const nameWidth = Math.max(...models.map((m) => m.name.length), 4);
|
|
99
|
+
const idWidth = Math.max(...models.map((m) => m.id.length), 2);
|
|
100
|
+
const providerWidth = Math.max(...models.map((m) => (m.provider?.length ?? 0)), 8);
|
|
101
|
+
console.log(`\n ${c.dim}${"Name".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} ${"Provider".padEnd(providerWidth)} Context${c.reset}`);
|
|
102
|
+
console.log(` ${c.dim}${"─".repeat(nameWidth + idWidth + providerWidth + 18)}${c.reset}`);
|
|
103
|
+
|
|
104
|
+
// Group by provider for cleaner output
|
|
105
|
+
const grouped: Record<string, typeof models> = {};
|
|
106
|
+
const order: string[] = [];
|
|
107
|
+
for (const m of models) {
|
|
108
|
+
const p = m.provider ?? "other";
|
|
109
|
+
if (!grouped[p]) { grouped[p] = []; order.push(p); }
|
|
110
|
+
grouped[p].push(m);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const provider of order) {
|
|
114
|
+
for (const m of grouped[provider]) {
|
|
115
|
+
const ctx = m.contextWindow >= 1_000_000
|
|
116
|
+
? `${(m.contextWindow / 1_000_000).toFixed(1)}M`
|
|
117
|
+
: m.contextWindow >= 1000
|
|
118
|
+
? `${(m.contextWindow / 1000).toFixed(0)}k`
|
|
119
|
+
: String(m.contextWindow);
|
|
120
|
+
const providerLabel = (PROVIDER_DISPLAY[m.provider ?? ""] ?? m.provider ?? "").padEnd(providerWidth);
|
|
121
|
+
console.log(
|
|
122
|
+
` ${c.bold}${m.name.padEnd(nameWidth)}${c.reset}` +
|
|
123
|
+
` ${c.dim}${m.id.padEnd(idWidth)}${c.reset}` +
|
|
124
|
+
` ${c.cyan}${providerLabel}${c.reset}` +
|
|
125
|
+
` ${c.dim}${ctx}${c.reset}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
errMsg(`Failed to list models: ${(e as Error).message}`);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function cmdWorkspaceList(config: ReturnType<typeof loadConfig>, baseUrl: string) {
|
|
137
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
138
|
+
header("Workspaces");
|
|
139
|
+
try {
|
|
140
|
+
const sessions = await kundex.agent.listSessions();
|
|
141
|
+
|
|
142
|
+
if (!sessions.length) {
|
|
143
|
+
dim("No workspaces yet. Create one with: kundex workspace new <directory>");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n ${c.dim}${"ID".padEnd(6)} ${"Name".padEnd(28)} ${"Directory".padEnd(34)} Model${c.reset}`);
|
|
148
|
+
console.log(` ${c.dim}${"─".repeat(90)}${c.reset}`);
|
|
149
|
+
for (const s of sessions) {
|
|
150
|
+
const cwd = (s.cwd ?? "").length > 32 ? `…${s.cwd.slice(-31)}` : (s.cwd ?? "");
|
|
151
|
+
const name = (s.name ?? `Workspace #${s.id}`).slice(0, 26);
|
|
152
|
+
console.log(` ${c.bold}${String(s.id).padEnd(6)}${c.reset} ${name.padEnd(28)} ${c.dim}${cwd.padEnd(34)} ${s.model}${c.reset}`);
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
errMsg(`Failed to list workspaces: ${(e as Error).message}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function cmdWorkspaceNew(
|
|
162
|
+
config: ReturnType<typeof loadConfig>,
|
|
163
|
+
baseUrl: string,
|
|
164
|
+
args: string[],
|
|
165
|
+
) {
|
|
166
|
+
const cwd = args[0] ?? process.cwd();
|
|
167
|
+
|
|
168
|
+
// Parse optional flags: --model <model>, --name <name>
|
|
169
|
+
let model = config.defaultModel ?? "openai/gpt-oss-120b";
|
|
170
|
+
let name: string | undefined;
|
|
171
|
+
for (let i = 0; i < args.length; i++) {
|
|
172
|
+
if (args[i] === "--model" && args[i + 1]) { model = args[++i]; }
|
|
173
|
+
else if (args[i] === "--name" && args[i + 1]) { name = args[++i]; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const session = await kundex.agent.createSession({ cwd, model, name });
|
|
180
|
+
ok(`Created workspace #${session.id}`);
|
|
181
|
+
dim(`Name: ${session.name}`);
|
|
182
|
+
dim(`Directory: ${cwd}`);
|
|
183
|
+
dim(`Model: ${session.model}`);
|
|
184
|
+
console.log();
|
|
185
|
+
} catch (e) {
|
|
186
|
+
errMsg(`Failed to create workspace: ${(e as Error).message}`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function cmdWorkspaceDelete(
|
|
192
|
+
config: ReturnType<typeof loadConfig>,
|
|
193
|
+
baseUrl: string,
|
|
194
|
+
idStr: string,
|
|
195
|
+
) {
|
|
196
|
+
const id = Number(idStr);
|
|
197
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
198
|
+
errMsg("Usage: kundex workspace delete <id>");
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await kundex.agent.deleteSession(id);
|
|
207
|
+
ok(`Deleted workspace #${id}`);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
errMsg(`Failed to delete workspace: ${(e as Error).message}`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Files ─────────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
async function cmdFilesList(
|
|
217
|
+
config: ReturnType<typeof loadConfig>,
|
|
218
|
+
baseUrl: string,
|
|
219
|
+
wsIdStr: string,
|
|
220
|
+
) {
|
|
221
|
+
const wsId = Number(wsIdStr);
|
|
222
|
+
if (!Number.isFinite(wsId) || wsId <= 0) {
|
|
223
|
+
errMsg("Usage: kundex files list <wsId>");
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
229
|
+
header(`Files in workspace #${wsId}`);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const { files } = await kundex.files.list(wsId);
|
|
233
|
+
if (!files.length) {
|
|
234
|
+
dim("No files yet.");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`\n ${c.dim}${"Path".padEnd(44)} ${"Size".padEnd(10)} MIME${c.reset}`);
|
|
239
|
+
console.log(` ${c.dim}${"─".repeat(80)}${c.reset}`);
|
|
240
|
+
for (const f of files) {
|
|
241
|
+
const size = f.size < 1024 ? `${f.size} B`
|
|
242
|
+
: f.size < 1024 * 1024 ? `${(f.size / 1024).toFixed(1)} KB`
|
|
243
|
+
: `${(f.size / 1024 / 1024).toFixed(1)} MB`;
|
|
244
|
+
const displayPath = f.path.length > 42 ? `…${f.path.slice(-41)}` : f.path;
|
|
245
|
+
console.log(` ${displayPath.padEnd(44)} ${size.padEnd(10)} ${c.dim}${f.mimeType}${c.reset}`);
|
|
246
|
+
}
|
|
247
|
+
console.log();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
errMsg(`Failed to list files: ${(e as Error).message}`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function cmdFilesGet(
|
|
255
|
+
config: ReturnType<typeof loadConfig>,
|
|
256
|
+
baseUrl: string,
|
|
257
|
+
wsIdStr: string,
|
|
258
|
+
remotePath: string,
|
|
259
|
+
) {
|
|
260
|
+
const wsId = Number(wsIdStr);
|
|
261
|
+
if (!Number.isFinite(wsId) || wsId <= 0 || !remotePath) {
|
|
262
|
+
errMsg("Usage: kundex files get <wsId> <path>");
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const file = await kundex.files.get(wsId, remotePath);
|
|
271
|
+
process.stdout.write(file.content ?? "");
|
|
272
|
+
} catch (e) {
|
|
273
|
+
errMsg(`Failed to get file: ${(e as Error).message}`);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function cmdFilesPush(
|
|
279
|
+
config: ReturnType<typeof loadConfig>,
|
|
280
|
+
baseUrl: string,
|
|
281
|
+
wsIdStr: string,
|
|
282
|
+
localPath: string,
|
|
283
|
+
remotePath?: string,
|
|
284
|
+
) {
|
|
285
|
+
const wsId = Number(wsIdStr);
|
|
286
|
+
if (!Number.isFinite(wsId) || wsId <= 0 || !localPath) {
|
|
287
|
+
errMsg("Usage: kundex files push <wsId> <localPath> [remotePath]");
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
293
|
+
const destPath = remotePath ?? path.basename(localPath);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const content = await fs.readFile(localPath, "utf-8");
|
|
297
|
+
const name = path.basename(destPath);
|
|
298
|
+
await kundex.files.upsert(wsId, { path: destPath, name, content });
|
|
299
|
+
ok(`Pushed ${localPath} → workspace #${wsId}:${destPath}`);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
errMsg(`Failed to push file: ${(e as Error).message}`);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function cmdFilesPull(
|
|
307
|
+
config: ReturnType<typeof loadConfig>,
|
|
308
|
+
baseUrl: string,
|
|
309
|
+
wsIdStr: string,
|
|
310
|
+
remotePath: string,
|
|
311
|
+
outPath?: string,
|
|
312
|
+
) {
|
|
313
|
+
const wsId = Number(wsIdStr);
|
|
314
|
+
if (!Number.isFinite(wsId) || wsId <= 0 || !remotePath) {
|
|
315
|
+
errMsg("Usage: kundex files pull <wsId> <remotePath> [--out <localPath>]");
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const file = await kundex.files.get(wsId, remotePath);
|
|
324
|
+
const content = file.content ?? "";
|
|
325
|
+
|
|
326
|
+
if (outPath) {
|
|
327
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
328
|
+
await fs.writeFile(outPath, content, "utf-8");
|
|
329
|
+
ok(`Saved ${remotePath} → ${outPath} (${content.length} bytes)`);
|
|
330
|
+
} else {
|
|
331
|
+
process.stdout.write(content);
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
errMsg(`Failed to pull file: ${(e as Error).message}`);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Run (single-turn non-interactive) ─────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
async function cmdRun(
|
|
342
|
+
config: ReturnType<typeof loadConfig>,
|
|
343
|
+
baseUrl: string,
|
|
344
|
+
message: string,
|
|
345
|
+
args: string[],
|
|
346
|
+
) {
|
|
347
|
+
if (!message) {
|
|
348
|
+
errMsg("Usage: kundex run <message>");
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Parse optional flags: --model, --workspace, --cwd
|
|
354
|
+
let model = config.defaultModel ?? "openai/gpt-oss-120b";
|
|
355
|
+
let wsId: number | null = null;
|
|
356
|
+
let cwd = process.cwd();
|
|
357
|
+
for (let i = 0; i < args.length; i++) {
|
|
358
|
+
if (args[i] === "--model" && args[i + 1]) model = args[++i];
|
|
359
|
+
else if ((args[i] === "--workspace" || args[i] === "-w") && args[i + 1]) wsId = Number(args[++i]);
|
|
360
|
+
else if (args[i] === "--cwd" && args[i + 1]) cwd = args[++i];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const kundex = new Kundex({ apiKey: config.apiKey!, baseUrl });
|
|
364
|
+
let sessionId: number;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
if (wsId !== null && Number.isFinite(wsId)) {
|
|
368
|
+
sessionId = wsId;
|
|
369
|
+
} else {
|
|
370
|
+
const session = await kundex.agent.createSession({ cwd, model });
|
|
371
|
+
sessionId = session.id;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
process.stdout.write(`\n`);
|
|
375
|
+
const { executeToolCall } = await import("./tools");
|
|
376
|
+
|
|
377
|
+
let turn = await kundex.agent.streamMessage(
|
|
378
|
+
{ sessionId, message },
|
|
379
|
+
(delta) => process.stdout.write(delta),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Handle tool calls in a loop until the turn is done
|
|
383
|
+
while (!turn.done && turn.toolCalls && turn.toolCalls.length > 0) {
|
|
384
|
+
for (const call of turn.toolCalls) {
|
|
385
|
+
const { result, isError } = await executeToolCall(cwd, call);
|
|
386
|
+
turn = await kundex.agent.submitToolResult(
|
|
387
|
+
{ sessionId, toolCallId: call.id, result, isError },
|
|
388
|
+
(delta) => process.stdout.write(delta),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
process.stdout.write("\n");
|
|
394
|
+
} catch (e) {
|
|
395
|
+
errMsg(`Agent error: ${(e as Error).message}`);
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Config commands ──────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
function cmdConfigShow(config: ReturnType<typeof loadConfig>, baseUrl: string) {
|
|
403
|
+
header("Current configuration");
|
|
404
|
+
console.log();
|
|
405
|
+
const rows = [
|
|
406
|
+
["API key", config.apiKey ? `${config.apiKey.slice(0, 8)}…` : "(not set)"],
|
|
407
|
+
["Base URL", baseUrl],
|
|
408
|
+
["Model", config.defaultModel ?? "(default)"],
|
|
409
|
+
];
|
|
410
|
+
for (const [k, v] of rows) {
|
|
411
|
+
console.log(` ${c.dim}${k.padEnd(12)}${c.reset} ${c.bold}${v}${c.reset}`);
|
|
412
|
+
}
|
|
413
|
+
console.log();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function cmdConfigSet(key: string, value: string) {
|
|
417
|
+
const allowed = ["model", "baseUrl"];
|
|
418
|
+
if (!allowed.includes(key)) {
|
|
419
|
+
errMsg(`Unknown config key "${key}". Allowed keys: ${allowed.join(", ")}`);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const map: Record<string, keyof ReturnType<typeof loadConfig>> = {
|
|
425
|
+
model: "defaultModel",
|
|
426
|
+
baseUrl: "baseUrl",
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
saveConfig({ [map[key]]: value });
|
|
430
|
+
ok(`Set ${key} = ${value}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function cmdLogout() {
|
|
434
|
+
saveConfig({ apiKey: null, baseUrl: null, defaultModel: null });
|
|
435
|
+
ok("Logged out. Run `kundex login` to authenticate again.");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
4
439
|
|
|
5
440
|
async function main(): Promise<void> {
|
|
6
|
-
const
|
|
441
|
+
const args = process.argv.slice(2);
|
|
442
|
+
const [command, sub, ...rest] = args;
|
|
7
443
|
|
|
8
|
-
|
|
9
|
-
|
|
444
|
+
// Version
|
|
445
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
446
|
+
console.log(`kundex v${VERSION}`);
|
|
10
447
|
return;
|
|
11
448
|
}
|
|
12
449
|
|
|
450
|
+
// Help
|
|
13
451
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
14
|
-
console.log(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
452
|
+
console.log(HELP);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// No command — drop into REPL if logged in, else show help
|
|
457
|
+
if (!command) {
|
|
458
|
+
const config = loadConfig();
|
|
459
|
+
const baseUrl = resolveBaseUrl(config);
|
|
460
|
+
if (config.apiKey && baseUrl) {
|
|
461
|
+
await runRepl(config, config.apiKey, baseUrl);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
console.log(HELP);
|
|
465
|
+
console.log(`\n${c.yellow} Run \`kundex login\` to get started.${c.reset}\n`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Login / logout
|
|
470
|
+
if (command === "login") {
|
|
471
|
+
await runLogin();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (command === "logout") {
|
|
475
|
+
cmdLogout();
|
|
23
476
|
return;
|
|
24
477
|
}
|
|
25
478
|
|
|
479
|
+
// Load config — required for all remaining commands
|
|
26
480
|
const config = loadConfig();
|
|
27
481
|
const baseUrl = resolveBaseUrl(config);
|
|
28
482
|
|
|
29
483
|
if (!config.apiKey || !baseUrl) {
|
|
30
|
-
|
|
484
|
+
errMsg('Not logged in. Run "kundex login" first.');
|
|
31
485
|
process.exitCode = 1;
|
|
32
486
|
return;
|
|
33
487
|
}
|
|
34
488
|
|
|
35
|
-
|
|
489
|
+
// Models
|
|
490
|
+
if (command === "models") {
|
|
491
|
+
await cmdModels(config, baseUrl);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Config
|
|
496
|
+
if (command === "config") {
|
|
497
|
+
if (sub === "show" || !sub) {
|
|
498
|
+
cmdConfigShow(config, baseUrl);
|
|
499
|
+
} else if (sub === "set") {
|
|
500
|
+
const [key, value] = rest;
|
|
501
|
+
if (!key || !value) {
|
|
502
|
+
errMsg("Usage: kundex config set <key> <value>");
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
} else {
|
|
505
|
+
cmdConfigSet(key, value);
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
errMsg(`Unknown config subcommand "${sub}". Use: show, set`);
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Workspace
|
|
515
|
+
if (command === "workspace") {
|
|
516
|
+
if (sub === "list" || !sub) {
|
|
517
|
+
await cmdWorkspaceList(config, baseUrl);
|
|
518
|
+
} else if (sub === "new") {
|
|
519
|
+
await cmdWorkspaceNew(config, baseUrl, rest);
|
|
520
|
+
} else if (sub === "delete") {
|
|
521
|
+
await cmdWorkspaceDelete(config, baseUrl, rest[0]);
|
|
522
|
+
} else {
|
|
523
|
+
errMsg(`Unknown workspace subcommand "${sub}". Use: list, new, delete`);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Files
|
|
530
|
+
if (command === "files") {
|
|
531
|
+
if (sub === "list") {
|
|
532
|
+
await cmdFilesList(config, baseUrl, rest[0]);
|
|
533
|
+
} else if (sub === "get") {
|
|
534
|
+
await cmdFilesGet(config, baseUrl, rest[0], rest[1]);
|
|
535
|
+
} else if (sub === "push") {
|
|
536
|
+
await cmdFilesPush(config, baseUrl, rest[0], rest[1], rest[2]);
|
|
537
|
+
} else if (sub === "pull") {
|
|
538
|
+
// Support --out flag
|
|
539
|
+
let outPath: string | undefined;
|
|
540
|
+
const remaining: string[] = [];
|
|
541
|
+
for (let i = 0; i < rest.length; i++) {
|
|
542
|
+
if (rest[i] === "--out" && rest[i + 1]) { outPath = rest[++i]; }
|
|
543
|
+
else remaining.push(rest[i]);
|
|
544
|
+
}
|
|
545
|
+
await cmdFilesPull(config, baseUrl, remaining[0], remaining[1], outPath);
|
|
546
|
+
} else {
|
|
547
|
+
errMsg(`Unknown files subcommand "${sub}". Use: list, get, push, pull`);
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Run (single-turn agent)
|
|
554
|
+
if (command === "run") {
|
|
555
|
+
// "kundex run <message>" — join remaining args as the message
|
|
556
|
+
const allArgs = [sub, ...rest].filter(Boolean);
|
|
557
|
+
// Find message (everything before the first -- flag or flag-like arg)
|
|
558
|
+
const flagStart = allArgs.findIndex((a) => a.startsWith("--"));
|
|
559
|
+
const messageParts = flagStart >= 0 ? allArgs.slice(0, flagStart) : allArgs;
|
|
560
|
+
const flagArgs = flagStart >= 0 ? allArgs.slice(flagStart) : [];
|
|
561
|
+
const message = messageParts.join(" ").trim();
|
|
562
|
+
|
|
563
|
+
await cmdRun(config, baseUrl, message, flagArgs);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Unknown command — suggest help
|
|
568
|
+
errMsg(`Unknown command "${command}". Run \`kundex help\` to see available commands.`);
|
|
569
|
+
process.exitCode = 1;
|
|
36
570
|
}
|
|
37
571
|
|
|
38
|
-
main().catch((
|
|
39
|
-
console.error(
|
|
572
|
+
main().catch((e) => {
|
|
573
|
+
console.error(`${c.red} Fatal: ${e instanceof Error ? e.message : e}${c.reset}`);
|
|
40
574
|
process.exitCode = 1;
|
|
41
575
|
});
|