junecoder 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/cli.js +7 -11
  2. package/package.json +8 -2
  3. package/tui.mjs +60 -3
package/cli.js CHANGED
@@ -9,10 +9,11 @@
9
9
  * junecoder "write a hello world" # Single-shot (non-TUI)
10
10
  *
11
11
  * API keys can be provided via:
12
- * 1. .env file in project directory (DEEPSEEK_API_KEY=sk-...)
13
- * 2. ~/.junecoder/.env
14
- * 3. DEEPSEEK_API_KEY environment variable
15
- * 4. ~/.junecoder/config.json
12
+ * 1. Interactive setup on first run (TUI)
13
+ * 2. .env file in project directory (DEEPSEEK_API_KEY=sk-...)
14
+ * 3. ~/.junecoder/.env
15
+ * 4. DEEPSEEK_API_KEY environment variable
16
+ * 5. ~/.junecoder/config.json
16
17
  */
17
18
  import { existsSync, statSync, readFileSync } from 'node:fs';
18
19
  import { resolve, join } from 'node:path';
@@ -153,12 +154,7 @@ if (existsSync(targetDir) && statSync(targetDir).isDirectory()) {
153
154
  const config = loadConfig();
154
155
  const apiKey = config.provider.apiKey || process.env.DEEPSEEK_API_KEY;
155
156
 
156
- if (!apiKey) {
157
- console.error('Error: No API key found. Set DEEPSEEK_API_KEY in .env or in ~/.junecoder/config.json.');
158
- process.exit(1);
159
- }
160
-
161
- config.provider.apiKey = apiKey;
157
+ config.provider.apiKey = apiKey || '';
162
158
 
163
159
  // Create Agent instance with target cwd
164
160
  const agent = createAgent({
@@ -196,4 +192,4 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
196
192
  }
197
193
 
198
194
  const restored = loadSession(agent.cwd);
199
- startTUI(agent, { projectDir: agent.cwd, restored });
195
+ startTUI(agent, { projectDir: agent.cwd, restored, needsSetup: !apiKey });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junecoder",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Zero Npm Dependencies Agent Framework",
5
5
  "main": "agent.mjs",
6
6
  "type": "module",
@@ -20,7 +20,13 @@
20
20
  "engines": {
21
21
  "node": ">=21.7"
22
22
  },
23
- "keywords": ["agent", "react", "llm", "coding", "ai"],
23
+ "keywords": [
24
+ "agent",
25
+ "react",
26
+ "llm",
27
+ "coding",
28
+ "ai"
29
+ ],
24
30
  "author": "",
25
31
  "license": "ISC"
26
32
  }
package/tui.mjs CHANGED
@@ -8,7 +8,9 @@
8
8
  */
9
9
  import { emitKeypressEvents } from "node:readline";
10
10
  import { PassThrough } from "node:stream";
11
- import { basename } from "node:path";
11
+ import { basename, join } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
12
14
  import { runAgent, ContinueError } from "./agent.mjs";
13
15
  import { estimateTokens } from "./context.mjs";
14
16
  import {
@@ -326,9 +328,57 @@ export async function startTUI(agent, opts = {}) {
326
328
  if (str && str.length > 0 && !key.ctrl && !key.meta && str !== "\r" && str !== "\n") { for (const ch of str) { state.input.splice(state.cursor, 0, ch); state.cursor++; } }
327
329
  }
328
330
 
331
+ // ─── First-run setup: capture API key from input box ──────────────────────────
332
+ let setupMode = opts.needsSetup;
333
+ if (setupMode) {
334
+ pushLine("", C.dim);
335
+ pushLine("\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510", C.assistant);
336
+ pushLine("\u2502 Welcome to JuneCoder! \u2502", C.assistant);
337
+ pushLine("\u2502 \u2502", C.assistant);
338
+ pushLine("\u2502 Before you start, you need a DeepSeek API key. \u2502", C.assistant);
339
+ pushLine("\u2502 Get one at: https://platform.deepseek.com/api_keys \u2502", C.assistant);
340
+ pushLine("\u2502 \u2502", C.assistant);
341
+ pushLine("\u2502 Paste your key below and press Enter. \u2502", C.assistant);
342
+ pushLine("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518", C.assistant);
343
+ pushLine("", C.dim);
344
+ state.status = "Paste your DeepSeek API key and press Enter (https://platform.deepseek.com/api_keys)";
345
+ }
346
+
329
347
  async function submit() {
330
348
  const text = state.input.join("").trim();
331
349
  if (!text || state.processing) return;
350
+
351
+ // ─── Setup mode: first input is the API key ──────────────────────────────
352
+ if (setupMode) {
353
+ setupMode = false;
354
+ state.input = []; state.cursor = 0;
355
+ const trimmed = text.trim();
356
+
357
+ if (!trimmed) {
358
+ pushLine("No API key provided. Exiting.", C.error);
359
+ render();
360
+ await new Promise(r => setTimeout(r, 1500));
361
+ cleanup();
362
+ process.exit(1);
363
+ }
364
+
365
+ if (!trimmed.startsWith("sk-")) {
366
+ pushLine("Warning: API key doesn't start with 'sk-'. It may not work.", C.warn);
367
+ }
368
+
369
+ const configDir = join(homedir(), '.junecoder');
370
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
371
+ writeFileSync(join(configDir, '.env'), `DEEPSEEK_API_KEY=${trimmed}\n`, 'utf-8');
372
+ process.env.DEEPSEEK_API_KEY = trimmed;
373
+ agent.provider.apiKey = trimmed;
374
+
375
+ pushLine("API key saved to ~/.junecoder/.env", C.tool);
376
+ pushLine("", C.dim);
377
+ state.status = "Ready";
378
+ render();
379
+ return;
380
+ }
381
+
332
382
  state.input = []; state.cursor = 0; state.history.push(text); state.historyIndex = -1; state.scroll = 0;
333
383
  if (text.startsWith("/")) { await handleSlash(text); return; }
334
384
  pushLabel("\u276f You:", ansi.bold + C.user); pushLine(text, C.text);
@@ -369,10 +419,17 @@ export async function startTUI(agent, opts = {}) {
369
419
  async function handleSlash(text) {
370
420
  const cmd = text.slice(1).split(/\s+/)[0].toLowerCase();
371
421
  switch (cmd) {
372
- case "help": for (const l of ["/help /plan /auto /model /session /clear /tasks /stats /new /distill /quit"]) pushLine(l, C.dim); break;
422
+ case "help": for (const l of ["/help /plan /auto /key /model /session /clear /tasks /stats /new /distill /quit"]) pushLine(l, C.dim); break;
373
423
  case "plan": agent.planMode = !agent.planMode; pushLine(" Plan mode " + (agent.planMode ? "ON" : "OFF"), C.tool); break;
374
424
  case "auto": agent.autoApprove = !agent.autoApprove; pushLine(" Auto-approve " + (agent.autoApprove ? "ON" : "OFF"), agent.autoApprove ? C.warn : C.tool); break;
375
425
  case "model": pushLine(" Model: " + agent.provider.model + " | Provider: " + (agent.provider.type || "?"), C.dim); break;
426
+ case "key": {
427
+ pushLine(" Current key: " + (agent.provider.apiKey ? agent.provider.apiKey.slice(0, 8) + "..." : "(none)"), C.dim);
428
+ pushLine(" To change, paste your new API key and press Enter.", C.tool);
429
+ setupMode = true;
430
+ state.status = "Paste your DeepSeek API key and press Enter (https://platform.deepseek.com/api_keys)";
431
+ break;
432
+ }
376
433
  case "session": { const slots = listSlots(agent.cwd); pushLine("Sessions:", C.tool); if (slots.length === 0) pushLine(" (none)", C.dim); else for (const s of slots) pushLine(" " + s.label, C.dim); break; }
377
434
  case "clear": archiveCurrent(agent.cwd); agent.history = []; state.lines = []; state.streaming = ""; state.reasoning = ""; state.tasks = []; state.scroll = 0; pushLine("Cleared (archived).", C.warn); break;
378
435
  case "new": archiveCurrent(agent.cwd); agent.history = []; state.lines = []; state.streaming = ""; state.reasoning = ""; state.toolStreams = {}; state.tasks = []; state.scroll = 0; state.tokens = { prompt: 0, completion: 0 }; pushLine("New session.", C.tool); break;
@@ -427,7 +484,7 @@ export async function startTUI(agent, opts = {}) {
427
484
  if (restored.tasks) { state.tasks = restored.tasks; agent.tasks = restored.tasks; }
428
485
  if (restored.planMode !== undefined) agent.planMode = restored.planMode;
429
486
  state.status = "Session restored";
430
- } else {
487
+ } else if (!setupMode) {
431
488
  pushLine("", C.dim);
432
489
  pushLine("JuneCoder TUI \u2014 " + (agent.provider.model || ""), ansi.bold + C.tool);
433
490
  pushLine("Type /help for commands, Ctrl+C to quit.", C.dim);