kob-cli 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/AGENTS.md +351 -0
- package/INSTALL.md +84 -0
- package/LICENSE +34 -0
- package/MANUAL.md +526 -0
- package/QUICKSTART.md +147 -0
- package/README.md +359 -0
- package/SPECTS.md +581 -0
- package/bin/cli.js +21 -0
- package/package.json +76 -0
- package/src/commands/ask.ts +55 -0
- package/src/commands/auth.ts +72 -0
- package/src/commands/chat.ts +146 -0
- package/src/commands/code.ts +78 -0
- package/src/commands/models.ts +66 -0
- package/src/commands/skills.ts +36 -0
- package/src/commands/stream.ts +53 -0
- package/src/index.ts +84 -0
- package/src/types/index.ts +95 -0
- package/src/ui/code-tui.tsx +440 -0
- package/src/utils/api.ts +208 -0
- package/src/utils/config.ts +45 -0
- package/src/utils/errors.ts +43 -0
- package/src/utils/format.ts +27 -0
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { CliConfig } from '../types/index.js';
|
|
2
|
+
import { ApiError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export interface ChatCompletionChunk {
|
|
5
|
+
id: string;
|
|
6
|
+
object: string;
|
|
7
|
+
created: number;
|
|
8
|
+
model: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
choices: {
|
|
11
|
+
index: number;
|
|
12
|
+
delta: { content?: string; role?: string };
|
|
13
|
+
finish_reason: string | null;
|
|
14
|
+
}[];
|
|
15
|
+
usage?: {
|
|
16
|
+
prompt_tokens: number;
|
|
17
|
+
completion_tokens: number;
|
|
18
|
+
total_tokens: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatCompletionResult {
|
|
23
|
+
content: string;
|
|
24
|
+
model: string;
|
|
25
|
+
usage: {
|
|
26
|
+
input_tokens: number;
|
|
27
|
+
output_tokens: number;
|
|
28
|
+
total_tokens: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class KobApiClient {
|
|
33
|
+
private baseUrl: string;
|
|
34
|
+
private apiKey: string;
|
|
35
|
+
private apiToken?: string;
|
|
36
|
+
private bearerToken?: string;
|
|
37
|
+
|
|
38
|
+
constructor(config: CliConfig) {
|
|
39
|
+
this.baseUrl = config.baseUrl;
|
|
40
|
+
this.apiKey = config.apiKey;
|
|
41
|
+
this.apiToken = config.apiToken;
|
|
42
|
+
this.bearerToken = config.bearerToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private getHeaders(): Record<string, string> {
|
|
46
|
+
return {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getAuthBody(): Record<string, string> {
|
|
52
|
+
const auth: Record<string, string> = {
|
|
53
|
+
api_key: this.apiKey,
|
|
54
|
+
};
|
|
55
|
+
if (this.apiToken) {
|
|
56
|
+
auth.api_token = this.apiToken;
|
|
57
|
+
}
|
|
58
|
+
return auth;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getBearerHeaders(): Record<string, string> {
|
|
62
|
+
return {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'Authorization': `Bearer ${this.bearerToken || this.apiKey}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// V2 streaming chat completions (OpenAI-compatible)
|
|
69
|
+
async *chatStream(model: string, messages: { role: string; content: string }[], options?: {
|
|
70
|
+
temperature?: number;
|
|
71
|
+
max_tokens?: number;
|
|
72
|
+
system_prompt?: string;
|
|
73
|
+
}): AsyncGenerator<ChatCompletionChunk> {
|
|
74
|
+
const url = `${this.baseUrl}/api/v2/chat/completions`;
|
|
75
|
+
|
|
76
|
+
const body: any = {
|
|
77
|
+
model,
|
|
78
|
+
messages: [...messages],
|
|
79
|
+
stream: true,
|
|
80
|
+
};
|
|
81
|
+
if (options?.temperature !== undefined) body.temperature = options.temperature;
|
|
82
|
+
if (options?.max_tokens !== undefined) body.max_tokens = options.max_tokens;
|
|
83
|
+
if (options?.system_prompt) {
|
|
84
|
+
body.messages.unshift({ role: 'system', content: options.system_prompt });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: this.getBearerHeaders(),
|
|
90
|
+
body: JSON.stringify(body),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
let msg = 'Request failed';
|
|
96
|
+
try { const d = JSON.parse(text); msg = d.error?.message || d.message || msg; } catch {}
|
|
97
|
+
throw new ApiError(msg, response.status);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!response.body) {
|
|
101
|
+
throw new ApiError('No response body', 500);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reader = response.body.getReader();
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
let buffer = '';
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
while (true) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done) break;
|
|
112
|
+
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
const lines = buffer.split('\n');
|
|
115
|
+
buffer = lines.pop() || '';
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (!line.startsWith('data: ')) continue;
|
|
119
|
+
const jsonStr = line.slice(6).trim();
|
|
120
|
+
if (jsonStr === '[DONE]') return;
|
|
121
|
+
try {
|
|
122
|
+
const chunk: ChatCompletionChunk = JSON.parse(jsonStr);
|
|
123
|
+
yield chunk;
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
reader.releaseLock();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// V2 chat completions (accumulates stream, returns final result)
|
|
133
|
+
async chatComplete(model: string, messages: { role: string; content: string }[], options?: {
|
|
134
|
+
temperature?: number;
|
|
135
|
+
max_tokens?: number;
|
|
136
|
+
system_prompt?: string;
|
|
137
|
+
}): Promise<ChatCompletionResult> {
|
|
138
|
+
let content = '';
|
|
139
|
+
let finalModel = model;
|
|
140
|
+
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
141
|
+
|
|
142
|
+
for await (const chunk of this.chatStream(model, messages, options)) {
|
|
143
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
144
|
+
if (delta) content += delta;
|
|
145
|
+
if (chunk.model) finalModel = chunk.model;
|
|
146
|
+
if (chunk.usage) {
|
|
147
|
+
usage = {
|
|
148
|
+
input_tokens: chunk.usage.prompt_tokens || 0,
|
|
149
|
+
output_tokens: chunk.usage.completion_tokens || 0,
|
|
150
|
+
total_tokens: chunk.usage.total_tokens || 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { content, model: finalModel, usage };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Old methods (for auth:verify, models, etc.)
|
|
159
|
+
async post<T>(endpoint: string, body: Record<string, any> = {}): Promise<T> {
|
|
160
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
161
|
+
const requestBody = { ...this.getAuthBody(), ...body };
|
|
162
|
+
|
|
163
|
+
const response = await fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: this.getHeaders(),
|
|
166
|
+
body: JSON.stringify(requestBody),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const text = await response.text();
|
|
170
|
+
let data: any;
|
|
171
|
+
try { data = JSON.parse(text); } catch {
|
|
172
|
+
throw new ApiError(
|
|
173
|
+
`Server returned non-JSON response (${response.status}). Expected API endpoint, got HTML. Check KOB_API_BASE_URL and endpoint path.`,
|
|
174
|
+
response.status
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new ApiError(data.message || 'Request failed', response.status);
|
|
180
|
+
}
|
|
181
|
+
if (!data.success) {
|
|
182
|
+
throw new ApiError(data.message || 'Request failed', response.status);
|
|
183
|
+
}
|
|
184
|
+
return data as T;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async get<T>(endpoint: string, params: Record<string, string> = {}): Promise<T> {
|
|
188
|
+
const queryParams = new URLSearchParams({ api_key: this.apiKey, ...params });
|
|
189
|
+
if (this.apiToken) queryParams.set('api_token', this.apiToken);
|
|
190
|
+
|
|
191
|
+
const response = await fetch(`${this.baseUrl}${endpoint}?${queryParams.toString()}`, {
|
|
192
|
+
method: 'GET', headers: this.getHeaders(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const text = await response.text();
|
|
196
|
+
let data: any;
|
|
197
|
+
try { data = JSON.parse(text); } catch {
|
|
198
|
+
throw new ApiError(
|
|
199
|
+
`Server returned non-JSON response (${response.status}). Expected API endpoint, got HTML. Check KOB_API_BASE_URL and endpoint path.`,
|
|
200
|
+
response.status
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!response.ok) throw new ApiError(data.message || 'Request failed', response.status);
|
|
205
|
+
if (!data.success) throw new ApiError(data.message || 'Request failed', response.status);
|
|
206
|
+
return data as T;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CliConfig } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
export function getConfig(): CliConfig {
|
|
4
|
+
let baseUrl = process.env.KOB_API_BASE_URL || 'https://www.kob-ai.dev';
|
|
5
|
+
|
|
6
|
+
// Normalize base URL: strip /v1 /v2 etc. if present (Next.js subpath)
|
|
7
|
+
// API endpoints use /api/ prefix directly, not under /v1/
|
|
8
|
+
baseUrl = baseUrl.replace(/\/v\d+\/?$/, '').replace(/\/+$/, '');
|
|
9
|
+
|
|
10
|
+
const rawKey = process.env.KOB_API_KEY || '';
|
|
11
|
+
const modelId = process.env.KOB_MODEL_ID;
|
|
12
|
+
|
|
13
|
+
if (!rawKey) {
|
|
14
|
+
console.error('❌ Error: KOB_API_KEY environment variable is required');
|
|
15
|
+
console.error('Please set it in your .env file or export it in your shell');
|
|
16
|
+
console.error('Example:');
|
|
17
|
+
console.error(' export KOB_API_KEY=kob_your_key');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Support combined format: "kob_xxx:token" or just "kob_xxx"
|
|
22
|
+
const colonIndex = rawKey.indexOf(':');
|
|
23
|
+
const apiKey = colonIndex !== -1 ? rawKey.substring(0, colonIndex) : rawKey;
|
|
24
|
+
const apiToken = colonIndex !== -1 ? rawKey.substring(colonIndex + 1) : undefined;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
baseUrl,
|
|
28
|
+
apiKey,
|
|
29
|
+
apiToken,
|
|
30
|
+
bearerToken: rawKey,
|
|
31
|
+
modelId: modelId || undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateConfig(config: CliConfig): void {
|
|
36
|
+
if (!config.baseUrl) {
|
|
37
|
+
console.error('❌ Error: KOB_API_BASE_URL is not set');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!config.apiKey) {
|
|
42
|
+
console.error('❌ Error: KOB_API_KEY is not set');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
public statusCode?: number;
|
|
5
|
+
public success: boolean = false;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, statusCode?: number) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function handleApiError(error: unknown): never {
|
|
15
|
+
if (error instanceof ApiError) {
|
|
16
|
+
console.error(chalk.red(`\n❌ API Error: ${error.message}`));
|
|
17
|
+
|
|
18
|
+
if (error.statusCode === 401) {
|
|
19
|
+
console.error(chalk.yellow('\nAuthentication failed. Please check your API credentials.'));
|
|
20
|
+
console.error(chalk.yellow('Make sure KOB_API_KEY is correct (format: kob_xxx:your_token).'));
|
|
21
|
+
} else if (error.statusCode === 402) {
|
|
22
|
+
console.error(chalk.yellow('\nInsufficient credits. Please top up your account.'));
|
|
23
|
+
} else if (error.statusCode === 404) {
|
|
24
|
+
console.error(chalk.yellow('\nResource not found.'));
|
|
25
|
+
} else if (error.statusCode && error.statusCode >= 500) {
|
|
26
|
+
console.error(chalk.yellow('\nServer error. Please try again later.'));
|
|
27
|
+
}
|
|
28
|
+
} else if (error instanceof Error) {
|
|
29
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}`));
|
|
30
|
+
} else {
|
|
31
|
+
console.error(chalk.red('\n❌ An unexpected error occurred'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateRequired(value: string | undefined, fieldName: string): string {
|
|
38
|
+
if (!value || value.trim() === '') {
|
|
39
|
+
console.error(chalk.red(`\n❌ Error: ${fieldName} is required`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// Format date to readable string
|
|
4
|
+
export function formatDate(dateString: string): string {
|
|
5
|
+
const date = new Date(dateString);
|
|
6
|
+
return date.toLocaleString('en-US', {
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'short',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
hour: '2-digit',
|
|
11
|
+
minute: '2-digit',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Format usage statistics
|
|
16
|
+
export function formatUsage(usage: any, creditBalance: number): string {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
lines.push(chalk.dim('─'.repeat(80)));
|
|
19
|
+
lines.push(chalk.bold.cyan('📊 Usage Statistics:'));
|
|
20
|
+
lines.push(` Input Tokens: ${chalk.yellow(usage.input_tokens?.toString() || '0')}`);
|
|
21
|
+
lines.push(` Output Tokens: ${chalk.yellow(usage.output_tokens?.toString() || '0')}`);
|
|
22
|
+
lines.push(` Total Tokens: ${chalk.yellow(usage.total_tokens?.toString() || '0')}`);
|
|
23
|
+
lines.push(` Cost: $${chalk.yellow((usage.cost_usd || 0).toFixed(6))}`);
|
|
24
|
+
lines.push(` Credits Used: ${chalk.red(usage.credits_used?.toString() || '0')}`);
|
|
25
|
+
lines.push(` Credits Remaining: ${chalk.green(creditBalance.toString())}`);
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
}
|