jawere 1.0.13

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 jawere
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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # jawere — AI Coding Agent
2
+
3
+ Terminal-based AI coding assistant powered by DeepSeek. Runs as an interactive REPL — describe tasks in natural language, and the agent reads, edits, writes, and executes shell commands autonomously.
4
+
5
+ ## Features
6
+
7
+ - **Autonomous Agent Loop** — Tool-calling loop (up to 500 turns) with seven filesystem tools
8
+ - **Codebase Scanner** — Pre-scans the project on startup, generating `.codebase/tree.yaml` and `.codebase/meta.json` so the LLM has structural context before the first prompt
9
+ - **Interactive REPL** — Multiline input (Shift+Enter), session resumption, and persistent conversation history backed by Convex
10
+ - **Encrypted API Key** — AES-256-GCM encrypted key storage at `~/.jawere/key.enc`
11
+ - **Session Persistence** — Full message history, tool calls, and token usage stored in Convex
12
+ - **Dual Environment** — Dev/prod modes with separate Convex deployments
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ git clone git@github.com:jawere/agent.git
18
+ cd agent
19
+ npm install
20
+ npx tsx src/index.ts --setup # one-time API key setup
21
+ npm start
22
+ ```
23
+
24
+ Or set the key via environment variable:
25
+
26
+ ```bash
27
+ export DEEPSEEK_API_KEY=sk-...
28
+ npm start
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ | Command | Description |
34
+ |---------|-------------|
35
+ | `/help` | Show available commands |
36
+ | `/sessions` | List recent Convex sessions |
37
+ | `/load <id>` | Resume a previous session |
38
+ | `/key` | Show API key status |
39
+ | `/setup` | Re-enter and save API key |
40
+ | `/clear` | Clear screen and start fresh |
41
+ | `/exit`, `/quit` | Exit |
42
+
43
+ ## Tools
44
+
45
+ The LLM has access to seven filesystem tools:
46
+
47
+ - `bash` — Execute shell commands with configurable timeout (max 300s). Output truncated at 2000 lines / 50KB.
48
+ - `read` — Read file contents with line offset/limit for large files.
49
+ - `edit` — Precise exact-text replacement with uniqueness validation. Supports batched edits.
50
+ - `write` — Create or overwrite files. Auto-creates parent directories.
51
+ - `ls` — List directory contents with sizes.
52
+ - `find` — Find files by fuzzy name or glob pattern. Skips hidden dirs and node_modules.
53
+ - `grep` — Search file contents with regex. Supports file glob filtering.
54
+
55
+ ## Configuration
56
+
57
+ | Variable | Default | Description |
58
+ |----------|---------|-------------|
59
+ | `DEEPSEEK_API_KEY` | — | DeepSeek API key |
60
+ | `DEEPSEEK_MODEL` | `deepseek-v4-pro` | Model name |
61
+ | `WORK_DIR` | `process.cwd()` | Working directory |
62
+ | `CONVEX_URL` | auto | Convex deployment URL |
63
+ | `NODE_ENV` | auto-detected | `development` or `production` |
64
+
65
+ ## Project Structure
66
+
67
+ ```
68
+ ├── bin/jawere.js # CLI entry point (global install)
69
+ ├── src/
70
+ │ ├── index.ts # REPL main loop, command handling
71
+ │ ├── agent.ts # Agent loop: LLM calls + tool execution
72
+ │ ├── config.ts # Configuration (env, key, mode)
73
+ │ ├── tools.ts # Tool definitions & implementations
74
+ │ ├── convex-client.ts # HTTP client for Convex backend
75
+ │ ├── crypto.ts # Encrypted API key storage (AES-256-GCM)
76
+ │ ├── scanner.ts # Codebase pre-scanner (.codebase/)
77
+ │ ├── spinner.ts # Terminal spinner animations
78
+ │ └── system-prompt.ts # System prompt template
79
+ ├── convex/ # Convex backend (schema, mutations, queries)
80
+ ├── scripts/build.js # esbuild bundler
81
+ ├── dist/ # Compiled output
82
+ ├── package.json
83
+ └── tsconfig.json
84
+ ```
85
+
86
+ ## Scripts
87
+
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | `npm start` | Run in dev mode |
91
+ | `npm run start:prod` | Run in prod mode |
92
+ | `npm run build` | Bundle with esbuild |
93
+ | `npm run dev` | Run `convex dev` for backend |
94
+ | `npm run deploy:dev` | Deploy Convex backend (dev) |
95
+ | `npm run deploy:prod` | Deploy Convex backend (prod) |
96
+ | `npm run seed:dev` | Seed dev database |
97
+ | `npm run seed:prod` | Seed prod database |
98
+
99
+ ## License
100
+
101
+ MIT
package/bin/jawere.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js').catch((err) => {
3
+ console.error('Fatal error:', err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,15 @@
1
+ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
2
+ export interface AgentOptions {
3
+ sessionId?: string;
4
+ title?: string;
5
+ history?: ChatCompletionMessageParam[];
6
+ signal?: AbortSignal;
7
+ }
8
+ export interface AgentResult {
9
+ text: string;
10
+ sessionId: string;
11
+ history: ChatCompletionMessageParam[];
12
+ }
13
+ /** Print the assistant's FINAL response — the summary shown when all work is done. */
14
+ export declare function printAssistantResponse(text: string): void;
15
+ export declare function runAgent(userMessage: string, options?: AgentOptions): Promise<AgentResult>;
package/dist/agent.js ADDED
@@ -0,0 +1,321 @@
1
+ import OpenAI from 'openai';
2
+ import { SYSTEM_PROMPT } from './system-prompt.js';
3
+ import { TOOL_DEFS, executeTool } from './tools.js';
4
+ import { loadConfig } from './config.js';
5
+ import { createSpinner } from './spinner.js';
6
+ import { createSession, appendUserMessage, appendAssistantMessage, appendToolResult, } from './convex-client.js';
7
+ const MAX_TURNS = 500;
8
+ const MAX_OUTPUT_TOKENS = 300_000; // 300k max output tokens
9
+ // ── Terminal display helpers ────────────────────────────────────────
10
+ const COL = () => process.stdout.columns || 80;
11
+ // Gruvbox color palette
12
+ const GRUVBOX_GREEN = '\x1b[38;2;184;187;3m'; // bright green #b8bb26
13
+ const GRUVBOX_GRAY = '\x1b[38;2;146;131;116m'; // gray #928374
14
+ const GRUVBOX_RED = '\x1b[38;2;251;73;52m'; // bright red #fb4934
15
+ const GRUVBOX_FG = '\x1b[38;2;235;219;178m'; // foreground #ebdbb2
16
+ const GRUVBOX_DIM = '\x1b[38;2;102;92;84m'; // dark gray #665c54
17
+ const GRUVBOX_AQUA = '\x1b[38;2;142;192;124m'; // aqua #8ec07c
18
+ const RESET = '\x1b[0m';
19
+ /** File-oriented tools get green; bash/grep/find get grey */
20
+ const FILE_TOOLS = new Set(['read', 'write', 'edit']);
21
+ /** Build a compact tool detail string from args */
22
+ function toolDetail(name, args) {
23
+ switch (name) {
24
+ case 'read': {
25
+ let d = String(args.path || '?');
26
+ if (args.offset)
27
+ d += ` [L${args.offset}${args.limit ? `-${Number(args.offset) + Number(args.limit) - 1}` : '+'}]`;
28
+ return d;
29
+ }
30
+ case 'write':
31
+ return String(args.path || '?');
32
+ case 'edit': {
33
+ let d = String(args.path || '?');
34
+ if (Array.isArray(args.edits))
35
+ d += ` (${args.edits.length} edit${args.edits.length !== 1 ? 's' : ''})`;
36
+ return d;
37
+ }
38
+ case 'bash':
39
+ case 'grep':
40
+ case 'find':
41
+ case 'ls':
42
+ return String(args.command || args.pattern || args.path || '?');
43
+ default:
44
+ return JSON.stringify(args);
45
+ }
46
+ }
47
+ /** Print a compact tool line: " tool: detail… ✓" with status right-aligned.
48
+ * Before printing, stop the spinner so the line renders clean. */
49
+ function displayToolLine(name, args, ok, spin) {
50
+ // Stop spinner temporarily to print the tool line cleanly
51
+ if (spin?.running) {
52
+ spin.stop();
53
+ }
54
+ const statusIcon = ok ? '✓' : '✗';
55
+ const statusColor = ok ? GRUVBOX_GREEN : GRUVBOX_RED;
56
+ const toolColor = FILE_TOOLS.has(name) ? GRUVBOX_GREEN : GRUVBOX_GRAY;
57
+ const prefix = `${toolColor}${name}${RESET}: `;
58
+ const suffix = ` ${statusColor}${statusIcon}${RESET}`;
59
+ let detail = toolDetail(name, args);
60
+ process.stdout.write(`${prefix}${detail}${suffix}\n`);
61
+ // Restart spinner after the tool line
62
+ if (spin) {
63
+ spin.start('Working…');
64
+ }
65
+ }
66
+ // ── Convex helpers ──────────────────────────────────────────────────
67
+ async function safeCall(fn, label) {
68
+ try {
69
+ return await fn();
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ // ── API retry with exponential backoff ─────────────────────────────
76
+ async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
77
+ let lastErr;
78
+ for (let i = 0; i <= maxRetries; i++) {
79
+ try {
80
+ return await fn();
81
+ }
82
+ catch (err) {
83
+ lastErr = err;
84
+ if (err.name === 'AbortError' || err.name === 'Canceled')
85
+ throw err;
86
+ if (i < maxRetries && (err.status === 429 || err.status >= 500)) {
87
+ const delay = baseDelay * Math.pow(2, i) + Math.random() * 1000;
88
+ process.stderr.write(`${GRUVBOX_GRAY}[retry ${i + 1}/${maxRetries}] ${err.status || 'error'}, waiting ${(delay / 1000).toFixed(1)}s...${RESET}\n`);
89
+ await new Promise((r) => setTimeout(r, delay));
90
+ continue;
91
+ }
92
+ throw lastErr;
93
+ }
94
+ }
95
+ throw lastErr;
96
+ }
97
+ // ── Response formatting ─────────────────────────────────────────────
98
+ function stripThinking(text) {
99
+ let cleaned = text.replace(/<thinking[^>]*>[\s\S]*?<\/thinking>/gi, '');
100
+ cleaned = cleaned.replace(/<\/think>[\s\S]*?<\/think>/g, '');
101
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
102
+ return cleaned.trim();
103
+ }
104
+ /** Print the assistant's FINAL response — the summary shown when all work is done. */
105
+ export function printAssistantResponse(text) {
106
+ const cols = COL();
107
+ const FG = '\x1b[38;2;235;219;178m'; // #ebdbb2
108
+ const YELLOW = '\x1b[38;2;250;189;47m'; // #fabd2f
109
+ const BLUE = '\x1b[38;2;131;165;152m'; // #83a598
110
+ const GRAY = '\x1b[38;2;146;131;116m'; // #928374
111
+ const DIM = '\x1b[38;2;102;92;84m'; // #665c54
112
+ const CODE = '\x1b[38;2;213;196;161m'; // #d5c4a1
113
+ const AQUA = '\x1b[38;2;142;192;124m'; // #8ec07c
114
+ const RESET2 = '\x1b[0m';
115
+ const pathRe = /\b(?:\.?\/?[\w.-]+)+\/[\w.-]+(?:\/[\w.-]+)*(?:\.\w+)?\b/;
116
+ const sep = DIM + '─'.repeat(Math.min(cols - 2, 60)) + RESET2;
117
+ console.log('');
118
+ console.log(sep);
119
+ const lines = text.split('\n');
120
+ for (const line of lines) {
121
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
122
+ if (stripped.trim() === '') {
123
+ console.log('');
124
+ continue;
125
+ }
126
+ if (/^[─═━]{3,}/.test(stripped.trim()) && stripped.trim().length > 3) {
127
+ console.log(DIM + line + RESET2);
128
+ continue;
129
+ }
130
+ if (/^\s*─+\s*.+\s*─+\s*$/.test(stripped)) {
131
+ console.log(YELLOW + line + RESET2);
132
+ continue;
133
+ }
134
+ let baseColor = FG;
135
+ let bulletMarker = '';
136
+ let bulletRest = '';
137
+ let bulletPrefix = '';
138
+ const bulletMatch = stripped.match(/^(\s*)([•\-]|\d+\.)(\s)/);
139
+ if (bulletMatch) {
140
+ bulletPrefix = line.slice(0, bulletMatch[1].length);
141
+ bulletMarker = line.slice(bulletMatch[1].length, bulletMatch[1].length + bulletMatch[2].length);
142
+ bulletRest = line.slice(bulletMatch[1].length + bulletMatch[2].length);
143
+ }
144
+ else if (/^ {2,}(?!•|-|\d+\.)(\S)/.test(stripped)) {
145
+ baseColor = CODE;
146
+ }
147
+ const content = bulletMatch ? bulletRest : line;
148
+ if (pathRe.test(stripped)) {
149
+ let colored = '';
150
+ let lastIdx = 0;
151
+ let match;
152
+ const re = new RegExp(pathRe.source, 'g');
153
+ while ((match = re.exec(content)) !== null) {
154
+ colored += content.slice(lastIdx, match.index) + BLUE + match[0] + RESET2;
155
+ lastIdx = match.index + match[0].length;
156
+ }
157
+ colored += content.slice(lastIdx);
158
+ if (bulletMatch) {
159
+ console.log(bulletPrefix + AQUA + bulletMarker + RESET2 + baseColor + colored + RESET2);
160
+ }
161
+ else {
162
+ console.log(baseColor + colored + RESET2);
163
+ }
164
+ }
165
+ else if (bulletMatch) {
166
+ console.log(bulletPrefix + AQUA + bulletMarker + RESET2 + baseColor + bulletRest + RESET2);
167
+ }
168
+ else {
169
+ console.log(baseColor + line + RESET2);
170
+ }
171
+ }
172
+ console.log(sep);
173
+ console.log('');
174
+ }
175
+ // ── Agent loop ──────────────────────────────────────────────────────
176
+ export async function runAgent(userMessage, options = {}) {
177
+ const config = await loadConfig();
178
+ if (!config.apiKey) {
179
+ throw new Error('No API key configured. Run with --setup to save your DeepSeek API key, or set DEEPSEEK_API_KEY env var.');
180
+ }
181
+ const client = new OpenAI({
182
+ baseURL: config.baseURL,
183
+ apiKey: config.apiKey,
184
+ });
185
+ const toolNames = TOOL_DEFS.map((t) => t.function.name);
186
+ // Create or resume Convex session (best-effort)
187
+ let sessionId = options.sessionId || 'local';
188
+ const hasRealSession = sessionId !== 'local';
189
+ const isNewSession = !options.sessionId;
190
+ if (isNewSession) {
191
+ const created = await safeCall(() => createSession(config.convexUrl, options.title || userMessage.slice(0, 100), config.model, SYSTEM_PROMPT, toolNames), 'createSession');
192
+ if (created)
193
+ sessionId = created;
194
+ }
195
+ if (hasRealSession || sessionId !== 'local') {
196
+ safeCall(() => appendUserMessage(config.convexUrl, sessionId, userMessage), 'appendUserMessage');
197
+ }
198
+ // Build message array: system prompt + existing history + new user message
199
+ const messages = [
200
+ { role: 'system', content: SYSTEM_PROMPT },
201
+ ];
202
+ if (options.history && options.history.length > 0) {
203
+ for (const m of options.history) {
204
+ if (m.role !== 'system') {
205
+ messages.push(m);
206
+ }
207
+ }
208
+ }
209
+ messages.push({ role: 'user', content: userMessage });
210
+ // ── Spinner for the agent loop ─────────────────────────────────
211
+ const spin = createSpinner();
212
+ spin.start('Thinking…');
213
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
214
+ // Check for cancellation (Ctrl+C)
215
+ if (options.signal?.aborted) {
216
+ spin.stop();
217
+ const msg = '\n[Cancelled by user]';
218
+ process.stderr.write(`${msg}\n`);
219
+ return { text: msg, sessionId, history: messages };
220
+ }
221
+ // Update spinner message before API call
222
+ spin.update('Thinking…');
223
+ const response = await withRetry(() => {
224
+ return client.chat.completions.create({
225
+ model: config.model,
226
+ messages,
227
+ tools: TOOL_DEFS,
228
+ tool_choice: 'auto',
229
+ temperature: 0.2,
230
+ max_tokens: MAX_OUTPUT_TOKENS,
231
+ // Enable thinking/reasoning — DeepSeek specific params
232
+ ...{
233
+ thinking: { type: 'enabled' },
234
+ reasoning_effort: 'max',
235
+ },
236
+ });
237
+ }).catch((err) => {
238
+ if (err.name === 'AbortError' || err.name === 'Canceled') {
239
+ return null;
240
+ }
241
+ throw err;
242
+ });
243
+ // Cancelled mid-request
244
+ if (!response) {
245
+ spin.stop();
246
+ const msg = '\n[Cancelled by user]';
247
+ return { text: msg, sessionId, history: messages };
248
+ }
249
+ const choice = response.choices[0];
250
+ if (!choice) {
251
+ spin.stop();
252
+ safeCall(() => appendAssistantMessage(config.convexUrl, sessionId, '(error: no response)', null), 'appendAssistantMessage');
253
+ return { text: 'Error: No response from model.', sessionId, history: messages };
254
+ }
255
+ const { message } = choice;
256
+ const usage = response.usage
257
+ ? {
258
+ input: response.usage.prompt_tokens || 0,
259
+ output: response.usage.completion_tokens || 0,
260
+ total: response.usage.total_tokens || 0,
261
+ }
262
+ : undefined;
263
+ // ── Tool calls — show what commands are being executed ─────────
264
+ if (message.tool_calls && message.tool_calls.length > 0) {
265
+ const toolCallsMeta = message.tool_calls.map((tc) => ({
266
+ id: tc.id,
267
+ name: tc.function.name,
268
+ arguments: tc.function.arguments,
269
+ }));
270
+ safeCall(() => appendAssistantMessage(config.convexUrl, sessionId, message.content || null, toolCallsMeta.length > 0 ? toolCallsMeta : null, usage), 'appendAssistantMessage');
271
+ messages.push({
272
+ role: 'assistant',
273
+ content: message.content || null,
274
+ tool_calls: message.tool_calls,
275
+ });
276
+ // Execute each tool call, showing what's running
277
+ for (const tc of message.tool_calls) {
278
+ let args = {};
279
+ try {
280
+ args = JSON.parse(tc.function.arguments);
281
+ }
282
+ catch { /* keep empty */ }
283
+ // Update spinner to show what we're about to run
284
+ spin.update(`Running ${tc.function.name}…`);
285
+ const result = await executeTool({
286
+ id: tc.id,
287
+ function: {
288
+ name: tc.function.name,
289
+ arguments: tc.function.arguments,
290
+ },
291
+ }, config.workDir);
292
+ const ok = !result.content.startsWith('Error');
293
+ // Display the tool line (stops spinner, prints line, restarts spinner)
294
+ displayToolLine(tc.function.name, args, ok, spin);
295
+ messages.push({
296
+ role: 'tool',
297
+ tool_call_id: result.tool_call_id,
298
+ content: result.content,
299
+ });
300
+ const isError = !ok;
301
+ safeCall(() => appendToolResult(config.convexUrl, sessionId, result.tool_call_id, tc.function.name, result.content, isError), 'appendToolResult');
302
+ }
303
+ // Continue loop to next API call (spinner still running)
304
+ continue;
305
+ }
306
+ // ── Final text response — the summary ──────────────────────────
307
+ spin.stop();
308
+ const rawText = message.content || '';
309
+ const text = stripThinking(rawText) || '(empty response)';
310
+ safeCall(() => appendAssistantMessage(config.convexUrl, sessionId, text, null, usage), 'appendAssistantMessage');
311
+ // Print a blank line then the final summary
312
+ console.log('');
313
+ printAssistantResponse(text);
314
+ return { text, sessionId, history: messages };
315
+ }
316
+ // Max turns reached
317
+ spin.stop();
318
+ const msg = 'Agent reached maximum turns without completing the task.';
319
+ safeCall(() => appendAssistantMessage(config.convexUrl, sessionId, msg, null), 'appendAssistantMessage');
320
+ return { text: msg, sessionId, history: messages };
321
+ }
@@ -0,0 +1,19 @@
1
+ export interface Config {
2
+ /** DeepSeek API base URL */
3
+ baseURL: string;
4
+ /** API key (loaded from encrypted storage or env var) */
5
+ apiKey: string;
6
+ /** Model name */
7
+ model: string;
8
+ /** Working directory */
9
+ workDir: string;
10
+ /** Convex deployment URL */
11
+ convexUrl: string;
12
+ /** Whether the key came from encrypted storage */
13
+ keyFromFile: boolean;
14
+ /** Whether running in dev mode */
15
+ isDev: boolean;
16
+ }
17
+ export declare function loadConfig(): Promise<Config>;
18
+ /** Check if an API key is configured */
19
+ export declare function hasApiKey(): Promise<boolean>;
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ import { loadKey, hasKey } from './crypto.js';
2
+ // Convex deployments
3
+ const CONVEX_DEV = 'https://dazzling-jackal-33.convex.cloud';
4
+ const CONVEX_PROD = 'https://friendly-pigeon-624.convex.cloud';
5
+ function isDevMode() {
6
+ // Explicit env var always wins
7
+ if (process.env.NODE_ENV === 'production')
8
+ return false;
9
+ if (process.env.NODE_ENV === 'development')
10
+ return true;
11
+ // Heuristic: running via tsx (src/*.ts) = dev; compiled dist/*.js = prod
12
+ const main = process.argv[1] || '';
13
+ if (main.endsWith('.ts') || main.includes('/src/'))
14
+ return true;
15
+ // Heuristic: node_modules/.bin or global install = prod
16
+ if (main.includes('/node_modules/') || main.includes('/.local/lib/node_modules/'))
17
+ return false;
18
+ // Default to dev (safer for local dev)
19
+ return true;
20
+ }
21
+ let cachedConfig = null;
22
+ export async function loadConfig() {
23
+ if (cachedConfig)
24
+ return cachedConfig;
25
+ // Try encrypted file first, then env var
26
+ let apiKey = '';
27
+ let keyFromFile = false;
28
+ const savedKey = await loadKey();
29
+ if (savedKey) {
30
+ apiKey = savedKey;
31
+ keyFromFile = true;
32
+ }
33
+ else if (process.env.DEEPSEEK_API_KEY) {
34
+ apiKey = process.env.DEEPSEEK_API_KEY;
35
+ }
36
+ const isDev = isDevMode();
37
+ cachedConfig = {
38
+ baseURL: 'https://api.deepseek.com/v1',
39
+ apiKey,
40
+ model: process.env.DEEPSEEK_MODEL || 'deepseek-v4-pro',
41
+ workDir: process.env.WORK_DIR || process.cwd(),
42
+ convexUrl: process.env.CONVEX_URL || (isDev ? CONVEX_DEV : CONVEX_PROD),
43
+ keyFromFile,
44
+ isDev,
45
+ };
46
+ return cachedConfig;
47
+ }
48
+ /** Check if an API key is configured */
49
+ export async function hasApiKey() {
50
+ if (process.env.DEEPSEEK_API_KEY)
51
+ return true;
52
+ return hasKey();
53
+ }
@@ -0,0 +1,41 @@
1
+ export interface SessionInfo {
2
+ _id: string;
3
+ _creationTime: number;
4
+ title: string;
5
+ model: string;
6
+ systemPrompt: string;
7
+ toolNames: string[];
8
+ createdAt: number;
9
+ updatedAt: number;
10
+ }
11
+ export interface MessageContent {
12
+ type: string;
13
+ text?: string;
14
+ id?: string;
15
+ name?: string;
16
+ arguments?: unknown;
17
+ toolCallId?: string;
18
+ content?: string;
19
+ }
20
+ /** Create a new session in Convex. Returns the session ID. */
21
+ export declare function createSession(convexUrl: string, title: string, model: string, systemPrompt: string, toolNames: string[]): Promise<string>;
22
+ /** Append a user message to the session */
23
+ export declare function appendUserMessage(convexUrl: string, sessionId: string, text: string): Promise<string>;
24
+ /** Append an assistant message (with optional tool calls and usage) */
25
+ export declare function appendAssistantMessage(convexUrl: string, sessionId: string, text: string | null, toolCalls: Array<{
26
+ id: string;
27
+ name: string;
28
+ arguments: string;
29
+ }> | null, usage?: {
30
+ input: number;
31
+ output: number;
32
+ total: number;
33
+ }): Promise<string>;
34
+ /** Append a tool result message */
35
+ export declare function appendToolResult(convexUrl: string, sessionId: string, toolCallId: string, toolName: string, result: string, isError: boolean): Promise<string>;
36
+ /** List recent sessions */
37
+ export declare function listSessions(convexUrl: string): Promise<SessionInfo[]>;
38
+ /** Get a full session with messages */
39
+ export declare function getSession(convexUrl: string, sessionId: string): Promise<(SessionInfo & {
40
+ messages: any[];
41
+ }) | null>;
@@ -0,0 +1,99 @@
1
+ // ── Types ───────────────────────────────────────────────────────────
2
+ // ── Convex HTTP helpers ─────────────────────────────────────────────
3
+ async function convexMutation(convexUrl, name, args) {
4
+ const res = await fetch(`${convexUrl}/api/mutation`, {
5
+ method: 'POST',
6
+ headers: { 'content-type': 'application/json' },
7
+ body: JSON.stringify({ path: name, format: 'json', args }),
8
+ });
9
+ if (!res.ok) {
10
+ const err = await res.text();
11
+ throw new Error(`Convex mutation ${name} failed (${res.status}): ${err}`);
12
+ }
13
+ const json = await res.json();
14
+ if (json.status === 'error') {
15
+ throw new Error(`Convex mutation ${name} error: ${json.errorMessage || JSON.stringify(json)}`);
16
+ }
17
+ // Convex wraps results in { status: "success", value: ... }
18
+ return json.value;
19
+ }
20
+ async function convexQuery(convexUrl, name, args) {
21
+ const res = await fetch(`${convexUrl}/api/query`, {
22
+ method: 'POST',
23
+ headers: { 'content-type': 'application/json' },
24
+ body: JSON.stringify({ path: name, format: 'json', args }),
25
+ });
26
+ if (!res.ok) {
27
+ const err = await res.text();
28
+ throw new Error(`Convex query ${name} failed (${res.status}): ${err}`);
29
+ }
30
+ const json = await res.json();
31
+ if (json.status === 'error') {
32
+ throw new Error(`Convex query ${name} error: ${json.errorMessage || JSON.stringify(json)}`);
33
+ }
34
+ // Convex wraps results in { status: "success", value: ... }
35
+ return json.value;
36
+ }
37
+ // ── Session operations ──────────────────────────────────────────────
38
+ /** Create a new session in Convex. Returns the session ID. */
39
+ export async function createSession(convexUrl, title, model, systemPrompt, toolNames) {
40
+ return convexMutation(convexUrl, 'sessions:create', {
41
+ title,
42
+ model,
43
+ systemPrompt,
44
+ toolNames,
45
+ });
46
+ }
47
+ /** Append a user message to the session */
48
+ export async function appendUserMessage(convexUrl, sessionId, text) {
49
+ return convexMutation(convexUrl, 'sessions:appendMessage', {
50
+ sessionId,
51
+ role: 'user',
52
+ content: [{ type: 'text', text }],
53
+ timestamp: Date.now(),
54
+ });
55
+ }
56
+ /** Append an assistant message (with optional tool calls and usage) */
57
+ export async function appendAssistantMessage(convexUrl, sessionId, text, toolCalls, usage) {
58
+ const content = [];
59
+ if (text) {
60
+ content.push({ type: 'text', text });
61
+ }
62
+ if (toolCalls) {
63
+ for (const tc of toolCalls) {
64
+ content.push({
65
+ type: 'tool_call',
66
+ id: tc.id,
67
+ name: tc.name,
68
+ arguments: tc.arguments,
69
+ });
70
+ }
71
+ }
72
+ return convexMutation(convexUrl, 'sessions:appendMessage', {
73
+ sessionId,
74
+ role: 'assistant',
75
+ content,
76
+ timestamp: Date.now(),
77
+ usage,
78
+ });
79
+ }
80
+ /** Append a tool result message */
81
+ export async function appendToolResult(convexUrl, sessionId, toolCallId, toolName, result, isError) {
82
+ return convexMutation(convexUrl, 'sessions:appendMessage', {
83
+ sessionId,
84
+ role: 'toolResult',
85
+ content: [{ type: 'tool_result', toolCallId, content: result }],
86
+ toolCallId,
87
+ toolName,
88
+ isError,
89
+ timestamp: Date.now(),
90
+ });
91
+ }
92
+ /** List recent sessions */
93
+ export async function listSessions(convexUrl) {
94
+ return convexQuery(convexUrl, 'sessions:list', {});
95
+ }
96
+ /** Get a full session with messages */
97
+ export async function getSession(convexUrl, sessionId) {
98
+ return convexQuery(convexUrl, 'sessions:get', { sessionId });
99
+ }