kodenique-agent 0.8.0 → 0.8.1
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/README.md +7 -3
- package/cli.mjs +18 -18
- package/commands.mjs +1 -2
- package/input.mjs +93 -0
- package/models.mjs +6 -1
- package/package.json +3 -1
- package/render.mjs +10 -11
- package/select.mjs +65 -0
- package/ui.mjs +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ kodenique-agent
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Update with `npm install -g kodenique-agent@latest`.
|
|
17
|
-
A release archive can also be installed with `npm install -g ./kodenique-agent-0.8.
|
|
17
|
+
A release archive can also be installed with `npm install -g ./kodenique-agent-0.8.1.tgz`.
|
|
18
18
|
|
|
19
19
|
The wizard asks for their API key (hidden while typing), optional Anthropic workspace ID, model from the live Claude API list, and whether to enable thinking summaries. Use a separate key per teammate; don't include your personal key in the package.
|
|
20
20
|
|
|
@@ -59,11 +59,15 @@ node /path/to/kodenique-agent/cli.mjs /path/to/project
|
|
|
59
59
|
❯ 1. Yes
|
|
60
60
|
2. Yes, and auto-approve edits this session
|
|
61
61
|
3. No
|
|
62
|
+
4. Show full change
|
|
63
|
+
↑/↓ to select · Enter to confirm · Esc = No
|
|
62
64
|
|
|
63
65
|
⏺ Fixed: getDay() returned the weekday, not the day of month.
|
|
64
66
|
⎿ Done · 9.4s · session tokens 5120 in / 640 out
|
|
65
67
|
```
|
|
66
68
|
|
|
69
|
+
Menus never need typing: approvals, `/model`, `/approval` and `/thinking` open a list. Move with **↑/↓** (or j/k), confirm with **Enter**, press a number to pick directly, and **Esc** cancels (for approvals, Esc means No). Typing `/` shows matching commands under the prompt; use ↑/↓ to highlight one, **Tab** to fill it in, **Enter** to run it.
|
|
70
|
+
|
|
67
71
|
Keys: **Esc** interrupts the current turn. The partial turn is removed from context and nothing more runs, but a shell command that already started finishes. **Shift+Tab** at the prompt cycles approval modes (ask → auto-edit → auto-all). Replies can use up to 16,000 output tokens (32,000 with thinking).
|
|
68
72
|
|
|
69
73
|
Thinking is optional. Enable with `/thinking on`, disable with `/thinking off`. It requests adaptive thinking with summarized display; use a compatible model such as Sonnet 4.6. Summaries are exactly what the API returns, not full private reasoning. Claude can skip thinking for simple prompts. Thinking can consume additional tokens, and the app makes no fixed-cost guarantee.
|
|
@@ -84,7 +88,7 @@ Thinking is optional. Enable with `/thinking on`, disable with `/thinking off`.
|
|
|
84
88
|
| `/help` | Show help |
|
|
85
89
|
| `/compact` | Replace the conversation with a model-written summary to free up context |
|
|
86
90
|
| `/clear` | Reset conversation context |
|
|
87
|
-
| `/thinking
|
|
91
|
+
| `/thinking` (or `/thinking on`/`off`) | Choose thinking on or off for this session |
|
|
88
92
|
| `/usage` | Show reported session input/output tokens |
|
|
89
93
|
| `/exit` or Ctrl+C | Exit |
|
|
90
94
|
|
|
@@ -112,7 +116,7 @@ The package allowlist contains only the CLI, agent, stream parser, config and mo
|
|
|
112
116
|
|
|
113
117
|
## Validation and protocol
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
Sixteen tests cover file restrictions and approvals, concurrent edits, real local HTTP request/response and errors, workspace headers, fragmented streamed thinking/text/tool JSON, signature preservation, interrupted streams, private config/environment overrides, paginated model discovery/selection, approval mode enforcement, UI previews, and local project selection/completion. They do not establish live model availability or account billing. No live Claude request was made for this release.
|
|
116
120
|
|
|
117
121
|
API contract: POST `https://api.anthropic.com/v1/messages`, `x-api-key`, `anthropic-version: 2023-06-01`, optional `anthropic-workspace-id`. Streaming uses SSE. Thinking blocks/signatures are preserved in conversation history, followed by all matching tool results.
|
|
118
122
|
|
package/cli.mjs
CHANGED
|
@@ -5,11 +5,13 @@ import {commands,completeCommand,resolveProject} from './commands.mjs';
|
|
|
5
5
|
import { executeTool, printable, requestMessage, runTurn } from './agent.mjs';
|
|
6
6
|
import {createUI,parseArgs,approvalModes,nextMode,VERSION} from './ui.mjs';
|
|
7
7
|
import {turnHandlers} from './render.mjs';
|
|
8
|
+
import {createInputFrame} from './input.mjs';
|
|
9
|
+
import {createKeys,select} from './select.mjs';
|
|
8
10
|
import { pickModel } from './models.mjs';
|
|
9
11
|
import { configPath, loadConfig, saveConfig, resolveConfig } from './config.mjs';
|
|
10
12
|
|
|
11
13
|
const args=process.argv.slice(2);
|
|
12
|
-
const help=`kodenique-agent [project-folder] [--approval ask|auto-edit|auto-all]\nkodenique-agent --auto Auto-approve edits and commands for this session\nkodenique-agent setup Configure your own API key and workspace\nkodenique-agent --version\n\nChat: / /project [folder] /files /approval /status /model /models /help /compact /clear /thinking on|off /usage /exit\nEnvironment overrides: ANTHROPIC_API_KEY, ANTHROPIC_WORKSPACE_ID,\nANTHROPIC_MODEL, ANTHROPIC_THINKING=on|off\nRequires Node.js 22+, macOS or Linux. Ask is the default. Auto-all runs shell commands with your full permissions.`;
|
|
14
|
+
const help=`kodenique-agent [project-folder] [--approval ask|auto-edit|auto-all]\nkodenique-agent --auto Auto-approve edits and commands for this session\nkodenique-agent setup Configure your own API key and workspace\nkodenique-agent --version\n\nChat: / /project [folder] /files /approval /status /model /models /help /compact /clear /thinking [on|off] /usage /exit\nEnvironment overrides: ANTHROPIC_API_KEY, ANTHROPIC_WORKSPACE_ID,\nANTHROPIC_MODEL, ANTHROPIC_THINKING=on|off\nRequires Node.js 22+, macOS or Linux. Ask is the default. Auto-all runs shell commands with your full permissions.`;
|
|
13
15
|
if(args.includes('--help')) { console.log(help); process.exit(0); }
|
|
14
16
|
if(args.includes('--version')) { console.log(`kodenique-agent ${VERSION}`); process.exit(0); }
|
|
15
17
|
let options;
|
|
@@ -17,11 +19,12 @@ try { options=parseArgs(args); } catch(e) {console.error(e.message);process.exit
|
|
|
17
19
|
const state={approval:options.mode,approving:false};
|
|
18
20
|
let turn=null;
|
|
19
21
|
if(!process.stdin.isTTY) { console.error('Run in an interactive terminal.'); process.exit(1); }
|
|
20
|
-
let muted=false;
|
|
21
|
-
const terminalOutput=new Writable({write(chunk,encoding,done){ if(!muted) process.stdout.write(chunk); done(); }});
|
|
22
|
+
let muted=false,input=null;
|
|
23
|
+
const terminalOutput=new Writable({write(chunk,encoding,done){ if(!muted) {process.stdout.write(chunk);input?.schedule();} done(); }});
|
|
22
24
|
terminalOutput.isTTY=true;
|
|
23
|
-
const
|
|
24
|
-
rl
|
|
25
|
+
const keys=createKeys();
|
|
26
|
+
const rl=createInterface({input:keys.input,output:terminalOutput,terminal:true,completer:completeCommand});
|
|
27
|
+
rl.on('SIGINT',()=>{ muted=false; process.stdout.write('\r\n\x1b[0J'); console.log('Exited.'); rl.close(); process.exit(130); });
|
|
25
28
|
const ask=async(prompt,secret=false)=>{
|
|
26
29
|
if(!secret) return (await rl.question(prompt)).trim();
|
|
27
30
|
process.stdout.write(prompt); muted=true;
|
|
@@ -32,18 +35,13 @@ let config;
|
|
|
32
35
|
const clean=value=>printable(config?.key ? String(value).split(config.key).join('[REDACTED]') : value);
|
|
33
36
|
const show=value=>console.log(clean(value));
|
|
34
37
|
const ui=createUI(clean);
|
|
38
|
+
const choose=opts=>select({keys,ui,...opts});
|
|
39
|
+
input=createInputFrame({rl,ui,keys,onShiftTab:()=>{state.approval=nextMode(state.approval);},getStatus:()=>ui.modeLine({...frame,mode:state.approval})});
|
|
35
40
|
const commandMenu=()=>{ui.label('Commands · type a command, Tab completes');for(const [command,description] of commands) show(` ${command.padEnd(16)} ${description}`);};
|
|
36
41
|
let chatPrompt=false;
|
|
37
|
-
let frame=
|
|
42
|
+
let frame={model:'',root:'',thinking:false};
|
|
38
43
|
process.stdin.on('keypress',(text,key)=>{
|
|
39
44
|
if(key?.name==='escape' && turn && !state.approving) turn.abort();
|
|
40
|
-
if(chatPrompt && frame && key?.name==='tab' && key.shift) {
|
|
41
|
-
state.approval=nextMode(state.approval);
|
|
42
|
-
process.stdout.write('\x1b7\x1b[1A\r\x1b[2K'+ui.modeLine({...frame,mode:state.approval})+'\x1b8');
|
|
43
|
-
}
|
|
44
|
-
if(chatPrompt && text==='/' && rl.line==='/') {
|
|
45
|
-
process.stdout.write('\n'); commandMenu(); rl.prompt(true);
|
|
46
|
-
}
|
|
47
45
|
});
|
|
48
46
|
try {
|
|
49
47
|
const saved=await loadConfig(); config=resolveConfig(saved);
|
|
@@ -53,7 +51,7 @@ try {
|
|
|
53
51
|
if(!key) throw Error('API key required.');
|
|
54
52
|
const workspaceId=await ask(`Anthropic workspace ID [${config.workspaceId||'optional for scoped keys'}] (- clears): `);
|
|
55
53
|
const selectedWorkspace=workspaceId==='-'?'':workspaceId||config.workspaceId||'';
|
|
56
|
-
const model=await pickModel({...config,key,workspaceId:selectedWorkspace},{ask,show});
|
|
54
|
+
const model=await pickModel({...config,key,workspaceId:selectedWorkspace},{ask,show,choose});
|
|
57
55
|
const thinking=await ask(`Enable Claude thinking summaries? [${config.thinking?'Y/n':'y/N'}]: `);
|
|
58
56
|
const next={key,workspaceId:selectedWorkspace,model:model||config.model,thinking:thinking?thinking.toLowerCase()==='y':config.thinking};
|
|
59
57
|
await saveConfig(next); config=resolveConfig(next);
|
|
@@ -68,8 +66,9 @@ try {
|
|
|
68
66
|
while(!rl.closed) {
|
|
69
67
|
chatPrompt=true;
|
|
70
68
|
frame={model:config.model,root,thinking:config.thinking};
|
|
71
|
-
|
|
69
|
+
input.open();
|
|
72
70
|
const prompt=await ask(ui.prompt(state.approval));
|
|
71
|
+
input.close(prompt);
|
|
73
72
|
chatPrompt=false;
|
|
74
73
|
if(!prompt) continue;
|
|
75
74
|
if(prompt==='/exit') break;
|
|
@@ -92,7 +91,7 @@ try {
|
|
|
92
91
|
if(prompt==='/help') { show(help); continue; }
|
|
93
92
|
if(prompt==='/clear') { messages=[]; show('Conversation cleared.'); continue; }
|
|
94
93
|
if(prompt==='/model' || prompt==='/models') {
|
|
95
|
-
const model=await pickModel(config,{ask,show});
|
|
94
|
+
const model=await pickModel(config,{ask,show,choose});
|
|
96
95
|
if(model!==config.model) {
|
|
97
96
|
try { await saveConfig({...await loadConfig(),model}); }
|
|
98
97
|
catch(e) { show(`Could not save model: ${e.message}. Current model unchanged.`);continue; }
|
|
@@ -104,7 +103,7 @@ try {
|
|
|
104
103
|
}
|
|
105
104
|
if(prompt==='/status') {ui.header({model:config.model,root,mode:state.approval,thinking:config.thinking});continue;}
|
|
106
105
|
if(prompt==='/approval' || prompt.startsWith('/approval ')) {
|
|
107
|
-
const next=prompt==='/approval'?await
|
|
106
|
+
const next=prompt==='/approval'?approvalModes[await choose({title:'Approval mode',initial:approvalModes.indexOf(state.approval),options:[{label:'ask',hint:'ask before edits & commands'},{label:'auto-edit',hint:'auto-approve file edits, ask for commands'},{label:'auto-all',hint:'auto-approve edits and shell commands'}]})]:prompt.slice(10).trim();
|
|
108
107
|
if(!next) continue;
|
|
109
108
|
if(!approvalModes.includes(next)) {ui.error('Choose ask, auto-edit, or auto-all.');continue;}
|
|
110
109
|
state.approval=next;ui.success(`Approval mode: ${state.approval} (this session only)`);
|
|
@@ -125,6 +124,7 @@ try {
|
|
|
125
124
|
continue;
|
|
126
125
|
}
|
|
127
126
|
if(prompt==='/usage') { show(`Session usage: ${usage.input} input / ${usage.output} output tokens (reported by API).`); continue; }
|
|
127
|
+
if(prompt==='/thinking') {const i=await choose({title:'Thinking summaries',initial:config.thinking?0:1,options:[{label:'On',hint:'show thinking summaries (uses more tokens)'},{label:'Off'}]});if(i>=0){config.thinking=i===0;ui.done(`Thinking summaries ${config.thinking?'on':'off'} for this session`);}continue;}
|
|
128
128
|
if(prompt==='/thinking on' || prompt==='/thinking off') { config.thinking=prompt.endsWith(' on'); show(`Thinking summaries ${config.thinking?'on':'off'} for this session.`); continue; }
|
|
129
129
|
if(prompt.startsWith('/')) { show('Unknown command. Use /help.'); continue; }
|
|
130
130
|
const before=messages.length;
|
|
@@ -132,7 +132,7 @@ try {
|
|
|
132
132
|
const start=Date.now();
|
|
133
133
|
turn=new AbortController();
|
|
134
134
|
try {
|
|
135
|
-
const {request,execute}=turnHandlers({ui,config,root,
|
|
135
|
+
const {request,execute}=turnHandlers({ui,config,root,choose,show,state,signal:turn.signal});
|
|
136
136
|
await runTurn({messages,streamed:true,signal:turn.signal,output:show,onUsage:u=>{usage.input+=u.input_tokens||0;usage.output+=u.output_tokens||0;},request,execute});
|
|
137
137
|
ui.done(`Done · ${((Date.now()-start)/1000).toFixed(1)}s · session tokens ${usage.input} in / ${usage.output} out`);
|
|
138
138
|
} catch(e) {
|
package/commands.mjs
CHANGED
|
@@ -7,8 +7,7 @@ export const commands=[
|
|
|
7
7
|
['/files','List files in the current project'],
|
|
8
8
|
['/model','Fetch and choose a Claude model'],
|
|
9
9
|
['/approval','Choose ask, auto-edit, or auto-all'],
|
|
10
|
-
['/thinking
|
|
11
|
-
['/thinking off','Disable thinking summaries'],
|
|
10
|
+
['/thinking','Turn thinking summaries on or off'],
|
|
12
11
|
['/status','Show project, model, and approval mode'],
|
|
13
12
|
['/usage','Show session token usage'],
|
|
14
13
|
['/compact','Summarize the conversation to free up context'],
|
package/input.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { commands } from './commands.mjs';
|
|
2
|
+
|
|
3
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
4
|
+
const plain = s => String(s).replace(ANSI, '');
|
|
5
|
+
|
|
6
|
+
// Commands matching what's typed so far ("/co" → /compact).
|
|
7
|
+
export function commandMatches(line) {
|
|
8
|
+
if (!line.startsWith('/') || line.includes(' ')) return [];
|
|
9
|
+
return commands.filter(([c]) => c.startsWith(line));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Claude Code–style input: a rule above and below the prompt, with the mode
|
|
13
|
+
// line (or selectable /command suggestions) underneath. Readline clears
|
|
14
|
+
// everything below the cursor on refresh, so the footer is redrawn after writes.
|
|
15
|
+
export function createInputFrame({ rl, ui, getStatus, keys, onShiftTab, output = process.stdout }) {
|
|
16
|
+
let active = false, pending = false, selected = 0, lastLine = '', unhook = null;
|
|
17
|
+
const cols = () => Math.max(20, output.columns || 80);
|
|
18
|
+
const rule = () => ui.paint('2', '─'.repeat(cols() - 1));
|
|
19
|
+
const fit = row => (plain(row).length < cols() ? row : ui.paint('2', plain(row).slice(0, cols() - 2) + '…'));
|
|
20
|
+
const rowsOf = text => {
|
|
21
|
+
try { return rl._getDisplayPos(text).rows; } catch { return Math.floor(plain(text).length / cols()); }
|
|
22
|
+
};
|
|
23
|
+
const matches = () => {
|
|
24
|
+
const line = rl.line || '';
|
|
25
|
+
if (line !== lastLine) { lastLine = line; selected = 0; }
|
|
26
|
+
const found = commandMatches(line).slice(0, 8);
|
|
27
|
+
if (selected >= found.length) selected = 0;
|
|
28
|
+
return found;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const footer = () => {
|
|
32
|
+
const line = rl.line || '';
|
|
33
|
+
const found = matches();
|
|
34
|
+
const rows = found.length
|
|
35
|
+
? found.map(([c, d], i) => (i === selected ? ui.paint('38;5;209', '❯ ' + c.padEnd(16)) : ' ' + ui.paint('36', c.padEnd(16))) + ui.paint('2', d))
|
|
36
|
+
: line.startsWith('/') && !line.includes(' ') ? [ui.paint('2', ' No matching commands')] : [getStatus()];
|
|
37
|
+
if (found.length) rows.push(ui.paint('2', ' ↑/↓ to select · Tab to fill in · Enter to run'));
|
|
38
|
+
return [rule(), ...rows].map(fit);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const draw = () => {
|
|
42
|
+
pending = false;
|
|
43
|
+
if (!active) return;
|
|
44
|
+
const { rows, cols: col } = rl.getCursorPos();
|
|
45
|
+
const end = Math.max(rows, rowsOf(rl.getPrompt() + (rl.line || '')));
|
|
46
|
+
const f = footer();
|
|
47
|
+
output.write(
|
|
48
|
+
(end > rows ? `\x1b[${end - rows}B` : '') + '\r\n' +
|
|
49
|
+
f.map(r => '\x1b[2K' + r).join('\r\n') + '\x1b[0J' +
|
|
50
|
+
`\x1b[${f.length + end - rows}A\x1b[${col + 1}G`,
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Replace the typed line as if the user had typed it.
|
|
55
|
+
const setLine = text => {
|
|
56
|
+
rl.write(null, { ctrl: true, name: 'e' });
|
|
57
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
58
|
+
rl.write(text);
|
|
59
|
+
lastLine = rl.line || ''; selected = 0;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const hook = (s, key = {}) => {
|
|
63
|
+
if (key.name === 'tab' && key.shift) { onShiftTab?.(); frame.schedule(); return true; }
|
|
64
|
+
const found = matches();
|
|
65
|
+
if (!found.length) return false;
|
|
66
|
+
if (key.name === 'up') { selected = (selected - 1 + found.length) % found.length; frame.schedule(); return true; }
|
|
67
|
+
if (key.name === 'down') { selected = (selected + 1) % found.length; frame.schedule(); return true; }
|
|
68
|
+
if (key.name === 'tab') { setLine(found[selected][0]); return true; }
|
|
69
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
70
|
+
if (!commands.some(([c]) => c === (rl.line || '').trim())) setLine(found[selected][0]);
|
|
71
|
+
return false; // let readline submit the line
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const frame = {
|
|
77
|
+
schedule() { if (active && !pending) { pending = true; setImmediate(draw); } },
|
|
78
|
+
open() {
|
|
79
|
+
output.write('\n' + rule() + '\n');
|
|
80
|
+
active = true; selected = 0; lastLine = '';
|
|
81
|
+
unhook = keys?.push(hook) ?? null;
|
|
82
|
+
},
|
|
83
|
+
// After Enter: remove the frame and echo the message like Claude Code.
|
|
84
|
+
close(answer) {
|
|
85
|
+
active = false; unhook?.(); unhook = null;
|
|
86
|
+
const up = rowsOf(rl.getPrompt() + answer) + 2;
|
|
87
|
+
output.write(`\x1b[${up}A\r\x1b[0J`);
|
|
88
|
+
if (answer) output.write(ui.paint('2', '> ') + plain(ui.paint('0', answer)) + '\n');
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
output.on?.('resize', () => frame.schedule());
|
|
92
|
+
return frame;
|
|
93
|
+
}
|
package/models.mjs
CHANGED
|
@@ -20,12 +20,17 @@ export async function listModels(config) {
|
|
|
20
20
|
throw Error('Model list exceeded pagination limit');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export async function pickModel(config,{ask,show}) {
|
|
23
|
+
export async function pickModel(config,{ask,show,choose}) {
|
|
24
24
|
show('Fetching models from Claude…');
|
|
25
25
|
let models;
|
|
26
26
|
try { models=await listModels(config); }
|
|
27
27
|
catch(e) { show(`Could not fetch models: ${e.message}`); return config.model; }
|
|
28
28
|
if(!models.length) { show('No available models returned. Keeping current model.');return config.model; }
|
|
29
|
+
if(choose) {
|
|
30
|
+
const current=Math.max(0,models.findIndex(m=>m.id===config.model));
|
|
31
|
+
const i=await choose({title:'Select a model',initial:current,options:models.map(m=>({label:m.display_name||m.id,hint:m.id+(m.id===config.model?' · current':'')+(m.lifecycle==='deprecated'?' · deprecated':'')}))});
|
|
32
|
+
return i<0?config.model:models[i].id;
|
|
33
|
+
}
|
|
29
34
|
models.forEach((m,i)=>show(`${i+1}. ${m.display_name||m.id} · ${m.id}${m.id===config.model?' [current]':''}${m.lifecycle==='deprecated'?' [deprecated]':''}`));
|
|
30
35
|
while(true) {
|
|
31
36
|
const choice=await ask(`Choose number or model ID (Enter keeps ${config.model}): `);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodenique-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Your team's terminal coding agent, powered directly by the Claude API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"models.mjs",
|
|
22
22
|
"ui.mjs",
|
|
23
23
|
"render.mjs",
|
|
24
|
+
"input.mjs",
|
|
25
|
+
"select.mjs",
|
|
24
26
|
"commands.mjs",
|
|
25
27
|
"README.md"
|
|
26
28
|
],
|
package/render.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export function summarize(call, result) {
|
|
|
20
20
|
return [text.slice(0, 200)];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function turnHandlers({ ui, config, root,
|
|
23
|
+
export function turnHandlers({ ui, config, root, choose, show, state, signal, output = process.stdout }) {
|
|
24
24
|
const request = async history => {
|
|
25
25
|
let open = false, mode = null, pending = '';
|
|
26
26
|
const key = config.key || '\0';
|
|
@@ -58,22 +58,21 @@ export function turnHandlers({ ui, config, root, ask, show, state, signal, outpu
|
|
|
58
58
|
if (write) ui.diff(editPreview(action.before, action.after));
|
|
59
59
|
else ui.note(' $ ' + action.command);
|
|
60
60
|
if (autoApproves(state.approval, action.tool)) { ui.done(`Auto-approved (${state.approval})`); return true; }
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
optionTwo: write ? 'Yes, and auto-approve edits this session' : "Yes, and don't ask again for commands this session",
|
|
64
|
-
});
|
|
61
|
+
const question = write ? `Do you want to ${action.before == null ? 'create' : 'make this edit to'} ${action.path}?` : 'Do you want to run this command?';
|
|
62
|
+
const options = ['Yes', write ? 'Yes, and auto-approve edits this session' : "Yes, and don't ask again for commands this session", 'No', { label: 'Show full change', hint: write ? 'complete before/after' : 'complete command' }];
|
|
65
63
|
state.approving = true;
|
|
66
64
|
try {
|
|
67
65
|
for (;;) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
66
|
+
output.write('\n');
|
|
67
|
+
const i = await choose({ title: question, options, footer: '↑/↓ to select · Enter to confirm · Esc = No' });
|
|
68
|
+
if (i === 3) { show(description); continue; }
|
|
69
|
+
if (i === 0) { ui.done('Approved'); return true; }
|
|
70
|
+
if (i === 1) {
|
|
72
71
|
state.approval = write && state.approval !== 'auto-all' ? 'auto-edit' : 'auto-all';
|
|
73
|
-
ui.done(`
|
|
72
|
+
ui.done(`Approved · approval mode: ${state.approval} (this session)`);
|
|
74
73
|
return true;
|
|
75
74
|
}
|
|
76
|
-
return false;
|
|
75
|
+
ui.done('Denied'); return false;
|
|
77
76
|
}
|
|
78
77
|
} finally { state.approving = false; }
|
|
79
78
|
};
|
package/select.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
3
|
+
|
|
4
|
+
// Readline reads keys from this proxy, so menus can claim keys first.
|
|
5
|
+
// Ctrl+C always reaches readline so exiting keeps working.
|
|
6
|
+
export function createKeys(stdin = process.stdin) {
|
|
7
|
+
emitKeypressEvents(stdin);
|
|
8
|
+
const input = new EventEmitter();
|
|
9
|
+
input.isTTY = true;
|
|
10
|
+
input.setRawMode = mode => { stdin.setRawMode?.(mode); return input; };
|
|
11
|
+
input.resume = () => { stdin.resume(); return input; };
|
|
12
|
+
input.pause = () => { stdin.pause(); return input; };
|
|
13
|
+
const hooks = [];
|
|
14
|
+
const dispatch = (s, key) => {
|
|
15
|
+
if (!(key?.ctrl && key.name === 'c')) for (let i = hooks.length - 1; i >= 0; i--) if (hooks[i](s, key)) return;
|
|
16
|
+
input.emit('keypress', s, key);
|
|
17
|
+
};
|
|
18
|
+
stdin.on('keypress', dispatch);
|
|
19
|
+
return {
|
|
20
|
+
input,
|
|
21
|
+
dispatch,
|
|
22
|
+
// A hook returns true to swallow the key. Returns a remover.
|
|
23
|
+
push(hook) { hooks.push(hook); return () => { const i = hooks.lastIndexOf(hook); if (i >= 0) hooks.splice(i, 1); }; },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Arrow-key menu. Resolves to the chosen index, or -1 on Esc.
|
|
28
|
+
// options: strings or {label, hint}.
|
|
29
|
+
export function select({ keys, ui, output = process.stdout, title, options, initial = 0, footer = '↑/↓ to select · Enter to confirm · Esc to cancel', max = 10 }) {
|
|
30
|
+
const items = options.map(o => (typeof o === 'string' ? { label: o } : o));
|
|
31
|
+
let index = Math.min(Math.max(initial, 0), items.length - 1), drawn = 0;
|
|
32
|
+
const render = () => {
|
|
33
|
+
const start = Math.max(0, Math.min(index - Math.floor(max / 2), items.length - max));
|
|
34
|
+
const visible = items.slice(start, start + max);
|
|
35
|
+
const rows = [];
|
|
36
|
+
if (title) rows.push(' ' + ui.paint('1', title));
|
|
37
|
+
if (start > 0) rows.push(ui.paint('2', ` ↑ ${start} more`));
|
|
38
|
+
visible.forEach((item, n) => {
|
|
39
|
+
const i = start + n, text = `${i + 1}. ${item.label}`;
|
|
40
|
+
const hint = item.hint ? ui.paint('2', ' ' + item.hint) : '';
|
|
41
|
+
rows.push(i === index ? ' ' + ui.paint('38;5;209', '❯ ' + text) + hint : ' ' + text + hint);
|
|
42
|
+
});
|
|
43
|
+
if (start + max < items.length) rows.push(ui.paint('2', ` ↓ ${items.length - start - max} more`));
|
|
44
|
+
rows.push(ui.paint('2', ' ' + footer));
|
|
45
|
+
const up = drawn > 1 ? `\x1b[${drawn - 1}A` : '';
|
|
46
|
+
output.write((drawn ? up + '\r' : '\x1b[?25l') + rows.map(r => '\x1b[2K' + r).join('\n') + '\x1b[0J');
|
|
47
|
+
drawn = rows.length;
|
|
48
|
+
};
|
|
49
|
+
return new Promise(resolve => {
|
|
50
|
+
const finish = value => {
|
|
51
|
+
remove();
|
|
52
|
+
output.write((drawn > 1 ? `\x1b[${drawn - 1}A` : '') + '\r\x1b[0J\x1b[?25h');
|
|
53
|
+
resolve(value);
|
|
54
|
+
};
|
|
55
|
+
const remove = keys.push((s, key = {}) => {
|
|
56
|
+
if (key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl)) { index = (index - 1 + items.length) % items.length; render(); }
|
|
57
|
+
else if (key.name === 'down' || key.name === 'j' || (key.name === 'n' && key.ctrl)) { index = (index + 1) % items.length; render(); }
|
|
58
|
+
else if (key.name === 'return' || key.name === 'enter') finish(index);
|
|
59
|
+
else if (key.name === 'escape') finish(-1);
|
|
60
|
+
else if (/^[1-9]$/.test(s || '') && Number(s) <= items.length) { index = Number(s) - 1; render(); finish(index); }
|
|
61
|
+
return true; // menus swallow every other key
|
|
62
|
+
});
|
|
63
|
+
render();
|
|
64
|
+
});
|
|
65
|
+
}
|
package/ui.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
export const VERSION='0.8.
|
|
2
|
+
export const VERSION='0.8.1';
|
|
3
3
|
export const nextMode=mode=>approvalModes[(approvalModes.indexOf(mode)+1)%approvalModes.length];
|
|
4
4
|
export const approvalModes=['ask','auto-edit','auto-all'];
|
|
5
5
|
export function autoApproves(mode,tool) {
|
|
@@ -88,7 +88,7 @@ export function createUI(clean,output=process.stdout) {
|
|
|
88
88
|
line(paint('2',' Tips for getting started:'));
|
|
89
89
|
line(paint('2',' 1. Ask questions, edit files, or run commands in this project'));
|
|
90
90
|
line(paint('2',' 2. Be specific for the best results'));
|
|
91
|
-
line(paint('2',' 3. Type / for commands
|
|
91
|
+
line(paint('2',' 3. Type / for commands (↑/↓ to pick, Enter to run) · shift+tab switches approval mode'));
|
|
92
92
|
},
|
|
93
93
|
commandMenu:commands=>{
|
|
94
94
|
for(const [command,description] of commands) line(' '+paint('36',command.padEnd(16))+paint('2',description));
|