jawere 1.4.0 → 1.5.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/dist/agent.js DELETED
@@ -1,321 +0,0 @@
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
- }
package/dist/config.d.ts DELETED
@@ -1,19 +0,0 @@
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 DELETED
@@ -1,53 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
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>;
@@ -1,99 +0,0 @@
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
- }
package/dist/crypto.d.ts DELETED
@@ -1,12 +0,0 @@
1
- /** Encrypt a string and return base64-encoded ciphertext (iv + tag + data). */
2
- export declare function encrypt(plaintext: string): string;
3
- /** Decrypt a base64-encoded ciphertext. Returns null if decryption fails. */
4
- export declare function decrypt(encoded: string): string | null;
5
- /** Save encrypted API key to ~/.jawere/key.enc */
6
- export declare function saveKey(apiKey: string): Promise<void>;
7
- /** Load and decrypt API key from ~/.jawere/key.enc. Returns null if not found or corrupt. */
8
- export declare function loadKey(): Promise<string | null>;
9
- /** Check if a saved key exists */
10
- export declare function hasKey(): Promise<boolean>;
11
- /** Delete the saved key */
12
- export declare function deleteKey(): Promise<void>;
package/dist/crypto.js DELETED
@@ -1,79 +0,0 @@
1
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
2
- import { readFile, writeFile, mkdir, access } from 'fs/promises';
3
- import { constants } from 'fs';
4
- import { homedir, hostname, userInfo } from 'os';
5
- import { join } from 'path';
6
- const ALGORITHM = 'aes-256-gcm';
7
- const IV_LENGTH = 16;
8
- const TAG_LENGTH = 16;
9
- const SALT = 'jawere-agent-key-vault-2026';
10
- /** Derive a 256-bit key from machine identity. Not perfect security but better than plaintext. */
11
- function deriveKey() {
12
- const machineId = `${hostname()}-${userInfo().username}-jawere`;
13
- return scryptSync(machineId, SALT, 32);
14
- }
15
- /** Encrypt a string and return base64-encoded ciphertext (iv + tag + data). */
16
- export function encrypt(plaintext) {
17
- const key = deriveKey();
18
- const iv = randomBytes(IV_LENGTH);
19
- const cipher = createCipheriv(ALGORITHM, key, iv);
20
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
21
- const tag = cipher.getAuthTag();
22
- // Format: iv (16) + tag (16) + ciphertext
23
- return Buffer.concat([iv, tag, encrypted]).toString('base64');
24
- }
25
- /** Decrypt a base64-encoded ciphertext. Returns null if decryption fails. */
26
- export function decrypt(encoded) {
27
- try {
28
- const key = deriveKey();
29
- const buf = Buffer.from(encoded, 'base64');
30
- const iv = buf.subarray(0, IV_LENGTH);
31
- const tag = buf.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
32
- const encrypted = buf.subarray(IV_LENGTH + TAG_LENGTH);
33
- const decipher = createDecipheriv(ALGORITHM, key, iv);
34
- decipher.setAuthTag(tag);
35
- return decipher.update(encrypted) + decipher.final('utf-8');
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
- const KEY_FILE = join(homedir(), '.jawere', 'key.enc');
42
- /** Save encrypted API key to ~/.jawere/key.enc */
43
- export async function saveKey(apiKey) {
44
- const dir = join(homedir(), '.jawere');
45
- await mkdir(dir, { recursive: true });
46
- const encrypted = encrypt(apiKey.trim());
47
- await writeFile(KEY_FILE, encrypted, 'utf-8');
48
- }
49
- /** Load and decrypt API key from ~/.jawere/key.enc. Returns null if not found or corrupt. */
50
- export async function loadKey() {
51
- try {
52
- await access(KEY_FILE, constants.R_OK);
53
- const encrypted = await readFile(KEY_FILE, 'utf-8');
54
- return decrypt(encrypted.trim());
55
- }
56
- catch {
57
- return null;
58
- }
59
- }
60
- /** Check if a saved key exists */
61
- export async function hasKey() {
62
- try {
63
- await access(KEY_FILE, constants.R_OK);
64
- return true;
65
- }
66
- catch {
67
- return false;
68
- }
69
- }
70
- /** Delete the saved key */
71
- export async function deleteKey() {
72
- try {
73
- const { unlink } = await import('fs/promises');
74
- await unlink(KEY_FILE);
75
- }
76
- catch {
77
- // Ignore if not found
78
- }
79
- }
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/prompt.d.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Create a prompt function appropriate for the current terminal.
3
- * Returns multilinePrompt for TTYs, simplePrompt for pipes/files.
4
- */
5
- export declare function createPrompt(): {
6
- prompt: () => Promise<string>;
7
- enableBracketedPaste: () => void;
8
- disableBracketedPaste: () => void;
9
- };