junecoder 1.0.10 → 1.0.12

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/agent.mjs CHANGED
@@ -263,7 +263,7 @@ function defaultCallbacks(overrides = {}) {
263
263
  onToolCall: () => {},
264
264
  onToolResult: () => {},
265
265
  onToolOutput: () => {},
266
- onPermissionRequest: async () => true,
266
+ onPermissionRequest: async () => ({ allowed: true }),
267
267
  onCompress: () => {},
268
268
  onUsage: () => {},
269
269
  onTurnEnd: () => {},
package/executor.mjs CHANGED
@@ -66,10 +66,16 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
66
66
 
67
67
  // Permission request for non-readonly tools
68
68
  let permissionDenied = false;
69
+ let denyReason = '';
69
70
  if (!tool.readonly && cb.onPermissionRequest) {
70
71
  try {
71
- const allowed = await cb.onPermissionRequest(tool, args);
72
- if (!allowed) {
72
+ const result = await cb.onPermissionRequest(tool, args);
73
+ if (typeof result === 'object' && result !== null) {
74
+ if (!result.allowed) {
75
+ permissionDenied = true;
76
+ denyReason = result.reason || '';
77
+ }
78
+ } else if (!result) {
73
79
  permissionDenied = true;
74
80
  }
75
81
  } catch {
@@ -78,11 +84,12 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
78
84
  }
79
85
 
80
86
  if (permissionDenied) {
87
+ const reasonSuffix = denyReason ? ` Reason: ${denyReason}` : '';
81
88
  prepared.push({
82
89
  id,
83
90
  name,
84
91
  denied: true,
85
- output: `Permission denied for "${name}".`,
92
+ output: `Permission denied for "${name}".${reasonSuffix}`,
86
93
  });
87
94
  continue;
88
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junecoder",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Zero Npm Dependencies Agent Framework",
5
5
  "main": "agent.mjs",
6
6
  "type": "module",
package/tui.mjs CHANGED
@@ -227,8 +227,10 @@ export async function startTUI(agent, opts = {}) {
227
227
 
228
228
  let borderColor = C.tool, title;
229
229
  if (state.question) { borderColor = C.tool; title = " " + sliceByWidth(state.question.text, W - 6) + " "; }
230
+ else if (state.permission?.reasonMode) { borderColor = C.warn; title = " Denied \u2014 why? (Enter to send, Esc to skip) "; }
230
231
  else if (state.permission) { borderColor = C.warn; title = state.permission.name === "continue" ? " Continue? (y/n) " : " Allow " + state.permission.name + "? (y/n/a) "; }
231
232
  else if (state.processing) title = " Processing... ";
233
+ else if (setupMode) title = " API Key ";
232
234
  else title = " Input ";
233
235
  const topBorder = "\u256d\u2500" + title + "\u2500".repeat(Math.max(0, W - 3 - stringWidth(title))) + "\u256e";
234
236
  out.push(`${borderColor}${topBorder}${ansi.reset}${ansi.clearLine}`);
@@ -271,9 +273,16 @@ export async function startTUI(agent, opts = {}) {
271
273
  keyStream.on("keypress", (str, key) => {
272
274
  if (state.permission) {
273
275
  const ch = (str || key.name || "").toLowerCase();
274
- if (ch === "y" || key.name === "y") state.permission.resolve(true);
275
- else if (ch === "n" || key.name === "n") state.permission.resolve(false);
276
- else if (ch === "a" || key.name === "a") { agent.autoApprove = true; state.permission.resolve(true); pushLine(" [auto] Auto-approve ON.", C.warn); }
276
+ if (ch === "y" || key.name === "y") state.permission.resolve({ allowed: true });
277
+ else if (ch === "a" || key.name === "a") { agent.autoApprove = true; state.permission.resolve({ allowed: true }); pushLine(" [auto] Auto-approve ON.", C.warn); }
278
+ else if (ch === "n" || key.name === "n") {
279
+ // Enter reason mode
280
+ state.permission.reasonMode = true;
281
+ state.input = []; state.cursor = 0;
282
+ state.status = "Denied — why? (Enter to send, Esc to skip)";
283
+ render();
284
+ return;
285
+ }
277
286
  else return;
278
287
  state.permission = null; state.permissionPreview = []; state.status = state.processing ? "Processing..." : "Ready"; render(); return;
279
288
  }
@@ -292,6 +301,14 @@ export async function startTUI(agent, opts = {}) {
292
301
  if (state.question.options.length === 0) { editInput(str, key); render(); return; }
293
302
  return;
294
303
  }
304
+ if (state.permission?.reasonMode && key.name === "escape") {
305
+ state.input = []; state.cursor = 0;
306
+ const resolve = state.permission.resolve;
307
+ state.permission = null; state.permissionPreview = [];
308
+ state.status = state.processing ? "Processing..." : "Ready";
309
+ resolve({ allowed: false });
310
+ render(); return;
311
+ }
295
312
  if (key.name === "c" && key.ctrl) {
296
313
  if (state.processing && state.controller) { state.controller.abort(); state.status = "Aborting..."; render(); }
297
314
  else { cleanup(); process.exit(0); }
@@ -338,20 +355,47 @@ export async function startTUI(agent, opts = {}) {
338
355
  pushLine("Before you start, you need a DeepSeek API key.", C.text);
339
356
  pushLine("Get one at: https://platform.deepseek.com/api_keys", C.dim);
340
357
  pushLine("", C.dim);
341
- state.status = "Paste your DeepSeek API key and press Enter (https://platform.deepseek.com/api_keys)";
358
+ state.status = "Paste your DeepSeek API key and press Enter";
342
359
  }
343
360
 
344
361
  async function submit() {
345
362
  const text = state.input.join("").trim();
346
- if (!text || state.processing) return;
363
+ if (!text || (state.processing && !state.permission?.reasonMode)) return;
364
+
365
+ // ─── Permission reason mode ────────────────────────────────────────────
366
+ if (state.permission?.reasonMode) {
367
+ const reason = text;
368
+ state.input = []; state.cursor = 0;
369
+ const resolve = state.permission.resolve;
370
+ state.permission = null; state.permissionPreview = [];
371
+ state.status = state.processing ? "Processing..." : "Ready";
372
+ pushLine(" Denied: " + (reason || "(no reason)"), C.warn);
373
+ resolve({ allowed: false, reason: reason || undefined });
374
+ render(); return;
375
+ }
347
376
 
348
377
  // ─── Setup mode: first input is the API key ──────────────────────────────
349
378
  if (setupMode) {
350
- setupMode = false;
351
379
  state.input = []; state.cursor = 0;
352
380
  const trimmed = text.trim();
353
381
 
382
+ // Allow slash commands to pass through during setup (e.g. /key cancel)
383
+ if (trimmed.startsWith("/")) {
384
+ setupMode = false;
385
+ await handleSlash(trimmed);
386
+ return;
387
+ }
388
+
389
+ // Empty input: exit on first-run, cancel on re-key
354
390
  if (!trimmed) {
391
+ if (agent.provider.apiKey) {
392
+ // Already have a key — user is just changing their mind
393
+ setupMode = false;
394
+ pushLine("Key unchanged.", C.dim);
395
+ state.status = "Ready";
396
+ render();
397
+ return;
398
+ }
355
399
  pushLine("No API key provided. Exiting.", C.error);
356
400
  render();
357
401
  await new Promise(r => setTimeout(r, 1500));
@@ -362,8 +406,8 @@ export async function startTUI(agent, opts = {}) {
362
406
  if (!trimmed.startsWith("sk-")) {
363
407
  pushLine("Invalid key — DeepSeek API keys must start with 'sk-'. Try again:", C.error);
364
408
  pushLine("", C.dim);
365
- setupMode = true; // stay in setup mode
366
- state.status = "Paste your DeepSeek API key and press Enter (https://platform.deepseek.com/api_keys)";
409
+ setupMode = true;
410
+ state.status = "Paste your DeepSeek API key and press Enter";
367
411
  render();
368
412
  return;
369
413
  }
@@ -376,6 +420,7 @@ export async function startTUI(agent, opts = {}) {
376
420
 
377
421
  pushLine("API key saved to ~/.junecoder/.env", C.tool);
378
422
  pushLine("", C.dim);
423
+ setupMode = false;
379
424
  state.status = "Ready";
380
425
  render();
381
426
  return;
@@ -406,7 +451,7 @@ export async function startTUI(agent, opts = {}) {
406
451
  try { await runAgent(agent, text, callbacks, { signal: state.controller.signal, resume }); flushStream(); break; }
407
452
  catch (error) { flushStream();
408
453
  if (error.name === "AbortError" || state.controller?.signal.aborted) { pushLine("[Aborted]", C.warn); break; }
409
- if (error instanceof ContinueError) { pushLabel("\u276f Continue", ansi.bold + C.warn); pushLine("Ran " + error.turn + " turns (limit " + error.turn + "). Continue?", C.warn); const willContinue = await new Promise(resolve => { state.permission = { name: "continue", args: { turns: error.turn }, resolve }; state.status = "Continue after " + error.turn + " turns?"; render(); }); state.permission = null; if (!willContinue) { pushLine("[Cancelled]", C.warn); break; } pushLine("[Continuing\u2026]", C.tool); state.controller = new AbortController(); continue; }
454
+ if (error instanceof ContinueError) { pushLabel("\u276f Continue", ansi.bold + C.warn); pushLine("Ran " + error.turn + " turns (limit " + error.turn + "). Continue?", C.warn); const willContinue = await new Promise(resolve => { state.permission = { name: "continue", args: { turns: error.turn }, resolve, reasonMode: false }; state.status = "Continue after " + error.turn + " turns?"; render(); }); state.permission = null; state.permissionPreview = []; if (!willContinue.allowed) { pushLine("[Cancelled]", C.warn); break; } pushLine("[Continuing\u2026]", C.tool); state.controller = new AbortController(); continue; }
410
455
  pushLine("[error] " + error.message, C.error); break;
411
456
  }
412
457
  }
@@ -415,7 +460,7 @@ export async function startTUI(agent, opts = {}) {
415
460
  try { saveSession(agent, state.lines); } catch {} render();
416
461
  }
417
462
  function flushStream() { if (state.reasoning) { pushLine(state.reasoning, C.reason); state.reasoning = ""; } if (state.streaming) { pushLine(state.streaming, C.text); state.streaming = ""; } }
418
- function askPermission(name, args) { if (agent.autoApprove) { pushLine(" [auto] " + name + " " + summarize(args), C.warn); return Promise.resolve(true); } state.permissionPreview = formatPermission(name, args); return new Promise(resolve => { state.permission = { name, args, resolve }; state.status = "Waiting: " + name; render(); }); }
463
+ function askPermission(name, args) { if (agent.autoApprove) { pushLine(" [auto] " + name + " " + summarize(args), C.warn); return Promise.resolve({ allowed: true }); } state.permissionPreview = formatPermission(name, args); return new Promise(resolve => { state.permission = { name, args, resolve, reasonMode: false }; state.status = "Waiting: " + name; render(); }); }
419
464
  function askQuestion(text) { if (state.question) return Promise.resolve("(already waiting)"); pushLabel("\u276f Question", ansi.bold + C.tool); for (const line of text.split("\n")) pushLine(" " + line, C.text); return new Promise(resolve => { state.question = { text, options: [], resolve }; state.status = "Waiting..."; render(); }); }
420
465
 
421
466
  async function handleSlash(text) {
@@ -427,9 +472,9 @@ export async function startTUI(agent, opts = {}) {
427
472
  case "model": pushLine(" Model: " + agent.provider.model + " | Provider: " + (agent.provider.type || "?"), C.dim); break;
428
473
  case "key": {
429
474
  pushLine(" Current key: " + (agent.provider.apiKey ? agent.provider.apiKey.slice(0, 8) + "..." : "(none)"), C.dim);
430
- pushLine(" To change, paste your new API key and press Enter.", C.tool);
475
+ pushLine(" Paste a new key, /cmd to cancel, or Enter to keep current.", C.tool);
431
476
  setupMode = true;
432
- state.status = "Paste your DeepSeek API key and press Enter (https://platform.deepseek.com/api_keys)";
477
+ state.status = "Paste your DeepSeek API key and press Enter";
433
478
  break;
434
479
  }
435
480
  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; }
@@ -457,8 +502,8 @@ export async function startTUI(agent, opts = {}) {
457
502
  for (const c of candidates) {
458
503
  pushLine(`\n [${c.type}] ${c.title}`, C.text);
459
504
  pushLine(` ${c.content.slice(0, 200)}...`, C.dim);
460
- const accept = await askPermission("distill-save", { title: c.title });
461
- if (accept) {
505
+ const result = await askPermission("distill-save", { title: c.title });
506
+ if (result.allowed) {
462
507
  const status = await saveCandidate(agent.memory, c, {});
463
508
  pushLine(` ✓ ${status}`, C.tool);
464
509
  saved++;