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 +1 -1
- package/executor.mjs +10 -3
- package/package.json +1 -1
- package/tui.mjs +59 -14
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
|
|
72
|
-
if (
|
|
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
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 === "
|
|
276
|
-
else if (ch === "
|
|
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
|
|
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;
|
|
366
|
-
state.status = "Paste your DeepSeek API key and press Enter
|
|
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("
|
|
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
|
|
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
|
|
461
|
-
if (
|
|
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++;
|