junecoder 1.0.11 → 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/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.11",
3
+ "version": "1.0.13",
4
4
  "description": "Zero Npm Dependencies Agent Framework",
5
5
  "main": "agent.mjs",
6
6
  "type": "module",
package/tui.mjs CHANGED
@@ -227,6 +227,7 @@ 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... ";
232
233
  else if (setupMode) title = " API Key ";
@@ -271,12 +272,32 @@ export async function startTUI(agent, opts = {}) {
271
272
 
272
273
  keyStream.on("keypress", (str, key) => {
273
274
  if (state.permission) {
274
- const ch = (str || key.name || "").toLowerCase();
275
- if (ch === "y" || key.name === "y") state.permission.resolve(true);
276
- else if (ch === "n" || key.name === "n") state.permission.resolve(false);
277
- else if (ch === "a" || key.name === "a") { agent.autoApprove = true; state.permission.resolve(true); pushLine(" [auto] Auto-approve ON.", C.warn); }
278
- else return;
279
- state.permission = null; state.permissionPreview = []; state.status = state.processing ? "Processing..." : "Ready"; render(); return;
275
+ if (state.permission.reasonMode) {
276
+ // In reason mode: Escape cancels, everything else passes through for typing
277
+ if (key.name === "escape") {
278
+ state.input = []; state.cursor = 0;
279
+ const resolve = state.permission.resolve;
280
+ state.permission = null; state.permissionPreview = [];
281
+ state.status = state.processing ? "Processing..." : "Ready";
282
+ resolve({ allowed: false });
283
+ render(); return;
284
+ }
285
+ // Fall through to normal key handling (typing, Enter→submit, Ctrl+C, etc.)
286
+ } else {
287
+ const ch = (str || key.name || "").toLowerCase();
288
+ if (ch === "y" || key.name === "y") state.permission.resolve({ allowed: true });
289
+ else if (ch === "a" || key.name === "a") { agent.autoApprove = true; state.permission.resolve({ allowed: true }); pushLine(" [auto] Auto-approve ON.", C.warn); }
290
+ else if (ch === "n" || key.name === "n") {
291
+ // Enter reason mode
292
+ state.permission.reasonMode = true;
293
+ state.input = []; state.cursor = 0;
294
+ state.status = "Denied — why? (Enter to send, Esc to skip)";
295
+ render();
296
+ return;
297
+ }
298
+ else return;
299
+ state.permission = null; state.permissionPreview = []; state.status = state.processing ? "Processing..." : "Ready"; render(); return;
300
+ }
280
301
  }
281
302
  if (state.question) {
282
303
  if (key.name === "return" || key.name === "enter") {
@@ -344,7 +365,20 @@ export async function startTUI(agent, opts = {}) {
344
365
 
345
366
  async function submit() {
346
367
  const text = state.input.join("").trim();
347
- if (!text || state.processing) return;
368
+ if (!text && !state.permission?.reasonMode) return;
369
+ if (state.processing && !state.permission?.reasonMode) return;
370
+
371
+ // ─── Permission reason mode ────────────────────────────────────────────
372
+ if (state.permission?.reasonMode) {
373
+ const reason = text;
374
+ state.input = []; state.cursor = 0;
375
+ const resolve = state.permission.resolve;
376
+ state.permission = null; state.permissionPreview = [];
377
+ state.status = state.processing ? "Processing..." : "Ready";
378
+ pushLine(" Denied: " + (reason || "(no reason)"), C.warn);
379
+ resolve({ allowed: false, reason: reason || undefined });
380
+ render(); return;
381
+ }
348
382
 
349
383
  // ─── Setup mode: first input is the API key ──────────────────────────────
350
384
  if (setupMode) {
@@ -423,7 +457,7 @@ export async function startTUI(agent, opts = {}) {
423
457
  try { await runAgent(agent, text, callbacks, { signal: state.controller.signal, resume }); flushStream(); break; }
424
458
  catch (error) { flushStream();
425
459
  if (error.name === "AbortError" || state.controller?.signal.aborted) { pushLine("[Aborted]", C.warn); break; }
426
- 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; }
460
+ 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; }
427
461
  pushLine("[error] " + error.message, C.error); break;
428
462
  }
429
463
  }
@@ -432,7 +466,7 @@ export async function startTUI(agent, opts = {}) {
432
466
  try { saveSession(agent, state.lines); } catch {} render();
433
467
  }
434
468
  function flushStream() { if (state.reasoning) { pushLine(state.reasoning, C.reason); state.reasoning = ""; } if (state.streaming) { pushLine(state.streaming, C.text); state.streaming = ""; } }
435
- 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(); }); }
469
+ 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(); }); }
436
470
  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(); }); }
437
471
 
438
472
  async function handleSlash(text) {
@@ -474,8 +508,8 @@ export async function startTUI(agent, opts = {}) {
474
508
  for (const c of candidates) {
475
509
  pushLine(`\n [${c.type}] ${c.title}`, C.text);
476
510
  pushLine(` ${c.content.slice(0, 200)}...`, C.dim);
477
- const accept = await askPermission("distill-save", { title: c.title });
478
- if (accept) {
511
+ const result = await askPermission("distill-save", { title: c.title });
512
+ if (result.allowed) {
479
513
  const status = await saveCandidate(agent.memory, c, {});
480
514
  pushLine(` ✓ ${status}`, C.tool);
481
515
  saved++;