kodenique-agent 0.3.0 → 0.6.0
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 +34 -4
- package/agent.mjs +10 -4
- package/cli.mjs +73 -20
- package/commands.mjs +33 -0
- package/package.json +3 -1
- package/ui.mjs +45 -0
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.
|
|
17
|
+
A release archive can also be installed with `npm install -g ./kodenique-agent-0.6.0.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
|
|
|
@@ -60,13 +60,21 @@ Thinking is optional. Enable with `/thinking on`, disable with `/thinking off`.
|
|
|
60
60
|
| Command | Action |
|
|
61
61
|
| --- | --- |
|
|
62
62
|
| `/model` or `/models` | Fetch available models and select one; saves default and clears conversation on change |
|
|
63
|
+
| `/approval ask` | Ask before writes and commands (default) |
|
|
64
|
+
| `/approval auto-edit` | Automatically approve file edits; ask for shell commands |
|
|
65
|
+
| `/approval auto-all` | Automatically approve file edits and shell commands |
|
|
66
|
+
| `/status` | Show model, project, thinking and approval mode |
|
|
67
|
+
| `/` | Show available commands (menu also opens as you type `/`; Tab completes commands) |
|
|
68
|
+
| `/project /path/to/folder` | Switch local project and clear conversation |
|
|
69
|
+
| `/project` | Prompt for a local project path |
|
|
70
|
+
| `/files` | List the current project directory locally |
|
|
63
71
|
| `/help` | Show help |
|
|
64
72
|
| `/clear` | Reset conversation context |
|
|
65
73
|
| `/thinking on` or `/thinking off` | Toggle thinking for this session |
|
|
66
74
|
| `/usage` | Show reported session input/output tokens |
|
|
67
75
|
| `/exit` or Ctrl+C | Exit |
|
|
68
76
|
|
|
69
|
-
The agent lists and reads files, proposes full-file edits, and runs commands such as searches and tests.
|
|
77
|
+
The agent lists and reads files, proposes full-file edits, and runs commands such as searches and tests. In Ask mode, every write shows a colored change preview and requires `y`; every command shows its exact text and requires `y`. Use `/full` at an approval prompt to see the complete change. Tool activity is visible. It never runs an incomplete streamed tool request.
|
|
70
78
|
|
|
71
79
|
## Boundaries
|
|
72
80
|
|
|
@@ -74,7 +82,7 @@ Approved shell commands have your full local permissions; they are **not sandbox
|
|
|
74
82
|
|
|
75
83
|
File tools reject traversal outside the project, symlinks, `.git`, `.ssh`, `.aws`, `.env*`, `node_modules`, `.pem` and `.key` files. This is not a complete secret detector or protection against malicious concurrent filesystem changes. Prompts and tool results are sent directly to Anthropic. File and command output limits are 100 KB; directory listings stop at 500 entries.
|
|
76
84
|
|
|
77
|
-
Conversations live in memory only. There is no saved history, context compaction, dollar spending cap
|
|
85
|
+
Conversations live in memory only. There is no saved history, context compaction, dollar spending cap. `/clear` reduces growing context. The loop stops after 15 model calls per user turn. Shell commands time out after 60 seconds; API calls after 120 seconds. Ctrl+C exits the agent; already started shell descendants may require separate cleanup.
|
|
78
86
|
|
|
79
87
|
## Build a team release
|
|
80
88
|
|
|
@@ -90,7 +98,7 @@ The package allowlist contains only the CLI, agent, stream parser, config and mo
|
|
|
90
98
|
|
|
91
99
|
## Validation and protocol
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
Twelve 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.
|
|
94
102
|
|
|
95
103
|
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.
|
|
96
104
|
|
|
@@ -100,3 +108,25 @@ Official references:
|
|
|
100
108
|
- https://platform.claude.com/docs/en/manage-claude/authentication
|
|
101
109
|
|
|
102
110
|
Model discovery uses GET `/v1/models` with your API key and workspace header, following all pagination cursors. Opus, Sonnet, and Haiku appear when returned by the API; no hardcoded catalogue is used. Discovery failures keep the current model. See https://platform.claude.com/docs/en/api/models/list.
|
|
111
|
+
|
|
112
|
+
## Automatic approvals
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
kodenique-agent --approval auto-edit
|
|
116
|
+
kodenique-agent --auto
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`--auto` is shorthand for `--approval auto-all`. Modes apply only to the current session and are visible in every prompt. `/approval` opens a mode prompt; `/approval ask` immediately restores manual approvals. Auto-all executes requested shell commands with your full user permissions, without a confirmation prompt. File path restrictions still apply to file tools. No command safety classifier or sandbox is claimed.
|
|
120
|
+
|
|
121
|
+
The terminal UI uses color when supported; set `NO_COLOR=1` for plain text. Changes are presented as compact previews, with complete edits available via `/full` before manual approval.
|
|
122
|
+
|
|
123
|
+
## Open projects on your laptop
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
kodenique-agent /Users/you/development/my-project
|
|
127
|
+
kodenique-agent "/Users/you/My Project" --auto
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Inside chat, type `/` to display the command menu immediately, then finish typing a command or use Tab completion. `/project` prompts for a folder; you can paste or drag a folder path into that prompt. `/project ~/development/another-project` switches directly. Relative paths resolve from the active project; quotes and escaped spaces are accepted. A failed folder switch keeps the current project. A successful switch clears chat context and preserves the session's visible approval mode. `/files` lists the active folder without an API call.
|
|
131
|
+
|
|
132
|
+
The selected folder is the root for file tools and the working directory for shell commands. File content reaches Claude when the agent reads it; opening a folder does not upload the whole project. Your existing approval mode controls edits and commands.
|
package/agent.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export async function executeTool(root, tool, approve) {
|
|
|
30
30
|
const input = tool.input ?? {};
|
|
31
31
|
if(tool.name==='run_command') {
|
|
32
32
|
if(typeof input.command!=='string' || !input.command.trim()) throw Error('Command required');
|
|
33
|
-
if(!await approve(`Run shell command (full local permissions):\n${input.command}
|
|
33
|
+
if(!await approve(`Run shell command (full local permissions):\n${input.command}`,{tool:'run_command',command:input.command})) throw Error('User denied command');
|
|
34
34
|
const env = Object.fromEntries(['PATH','HOME','TMPDIR','LANG','SHELL'].filter(k=>process.env[k]).map(k=>[k,process.env[k]]));
|
|
35
35
|
return await new Promise(resolve=>exec(input.command,{cwd:root,env,timeout:60000,maxBuffer:100000,shell:'/bin/sh'},(error,stdout,stderr)=>resolve(JSON.stringify({exitCode:error?.code??0,stdout,stderr,error:error?.message}))));
|
|
36
36
|
}
|
|
@@ -50,7 +50,7 @@ export async function executeTool(root, tool, approve) {
|
|
|
50
50
|
if(typeof input.content!=='string' || Buffer.byteLength(input.content)>100000) throw Error('Content must be text under 100 KB');
|
|
51
51
|
let old=null;
|
|
52
52
|
try { old=await fs.readFile(target,'utf8'); } catch(e) { if(e.code!=='ENOENT') throw e; }
|
|
53
|
-
if(!await approve(`Write ${input.path}\n--- existing ---\n${old??'(new file)'}\n--- proposed ---\n${input.content}
|
|
53
|
+
if(!await approve(`Write ${input.path}\n--- existing ---\n${old??'(new file)'}\n--- proposed ---\n${input.content}`,{tool:'write_file',path:input.path,before:old,after:input.content})) throw Error('User denied write');
|
|
54
54
|
await safePath(root,input.path);
|
|
55
55
|
let now=null;
|
|
56
56
|
try { now=await fs.readFile(target,'utf8'); } catch(e) { if(e.code!=='ENOENT') throw e; }
|
|
@@ -74,7 +74,10 @@ export async function apiRequest({key,workspaceId,baseUrl='https://api.anthropic
|
|
|
74
74
|
if(key) detail=detail.split(key).join('[REDACTED]');
|
|
75
75
|
detail=printable(detail.replace(/sk-ant-[A-Za-z0-9_-]+/g,'[REDACTED]')).slice(0,1500);
|
|
76
76
|
const hint=response.status===401?'Check your API key.':response.status===429?'Rate limited; wait before trying again.':'Check credits, model access, and request size.';
|
|
77
|
-
|
|
77
|
+
const error=new Error(`Claude API HTTP ${response.status}: ${detail || hint}`);
|
|
78
|
+
error.status=response.status;
|
|
79
|
+
if(response.status===429) error.code='ANTHROPIC_RATE_LIMIT';
|
|
80
|
+
throw error;
|
|
78
81
|
}
|
|
79
82
|
return response;
|
|
80
83
|
}
|
|
@@ -97,7 +100,10 @@ export async function runTurn({messages,request,execute,output=()=>{},onUsage=()
|
|
|
97
100
|
const results=[];
|
|
98
101
|
for(const call of calls) {
|
|
99
102
|
try { results.push({type:'tool_result',tool_use_id:call.id,content:String(await execute(call))}); }
|
|
100
|
-
catch(e) {
|
|
103
|
+
catch(e) {
|
|
104
|
+
if(e.code==='ANTHROPIC_RATE_LIMIT') throw e;
|
|
105
|
+
results.push({type:'tool_result',tool_use_id:call.id,content:e.message,is_error:true});
|
|
106
|
+
}
|
|
101
107
|
}
|
|
102
108
|
messages.push({role:'user',content:results});
|
|
103
109
|
}
|
package/cli.mjs
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { Writable } from 'node:stream';
|
|
4
|
-
import
|
|
4
|
+
import {commands,completeCommand,resolveProject} from './commands.mjs';
|
|
5
5
|
import { executeTool, printable, requestMessage, runTurn } from './agent.mjs';
|
|
6
|
+
import {createUI,parseArgs,autoApproves,approvalModes,editPreview} from './ui.mjs';
|
|
6
7
|
import { pickModel } from './models.mjs';
|
|
7
8
|
import { configPath, loadConfig, saveConfig, resolveConfig } from './config.mjs';
|
|
8
9
|
|
|
9
10
|
const args=process.argv.slice(2);
|
|
10
|
-
const help=`kodenique-agent [project-folder]\nkodenique-agent setup Configure your own API key and workspace\nkodenique-agent --version\n\nChat: /model /models /help /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.
|
|
11
|
+
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 /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.`;
|
|
11
12
|
if(args.includes('--help')) { console.log(help); process.exit(0); }
|
|
12
|
-
if(args.includes('--version')) { console.log('kodenique-agent 0.
|
|
13
|
-
|
|
13
|
+
if(args.includes('--version')) { console.log('kodenique-agent 0.6.0'); process.exit(0); }
|
|
14
|
+
let options;
|
|
15
|
+
try { options=parseArgs(args); } catch(e) {console.error(e.message);process.exit(1);}
|
|
16
|
+
let approval=options.mode;
|
|
14
17
|
if(!process.stdin.isTTY) { console.error('Run in an interactive terminal.'); process.exit(1); }
|
|
15
18
|
let muted=false;
|
|
16
19
|
const terminalOutput=new Writable({write(chunk,encoding,done){ if(!muted) process.stdout.write(chunk); done(); }});
|
|
17
20
|
terminalOutput.isTTY=true;
|
|
18
|
-
const rl=createInterface({input:process.stdin,output:terminalOutput,terminal:true});
|
|
21
|
+
const rl=createInterface({input:process.stdin,output:terminalOutput,terminal:true,completer:completeCommand});
|
|
19
22
|
rl.on('SIGINT',()=>{ muted=false; console.log('\nExited.'); rl.close(); process.exit(130); });
|
|
20
23
|
const ask=async(prompt,secret=false)=>{
|
|
21
24
|
if(!secret) return (await rl.question(prompt)).trim();
|
|
@@ -26,9 +29,17 @@ const ask=async(prompt,secret=false)=>{
|
|
|
26
29
|
let config;
|
|
27
30
|
const clean=value=>printable(config?.key ? String(value).split(config.key).join('[REDACTED]') : value);
|
|
28
31
|
const show=value=>console.log(clean(value));
|
|
32
|
+
const ui=createUI(clean);
|
|
33
|
+
const commandMenu=()=>{ui.label('Commands · type a command, Tab completes');for(const [command,description] of commands) show(` ${command.padEnd(16)} ${description}`);};
|
|
34
|
+
let chatPrompt=false;
|
|
35
|
+
process.stdin.on('keypress',(text)=>{
|
|
36
|
+
if(chatPrompt && text==='/' && rl.line==='/') {
|
|
37
|
+
process.stdout.write('\n'); commandMenu(); rl.prompt(true);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
29
40
|
try {
|
|
30
41
|
const saved=await loadConfig(); config=resolveConfig(saved);
|
|
31
|
-
if(
|
|
42
|
+
if(options.workspace==='setup' || !config.key) {
|
|
32
43
|
show('kodenique-agent · Setup\nUse your own Anthropic API key. It will be stored locally with owner-only file permissions.');
|
|
33
44
|
const key=await ask(`API key${config.key?' (Enter keeps current)':''}: `,true) || config.key;
|
|
34
45
|
if(!key) throw Error('API key required.');
|
|
@@ -39,17 +50,35 @@ try {
|
|
|
39
50
|
const next={key,workspaceId:selectedWorkspace,model:model||config.model,thinking:thinking?thinking.toLowerCase()==='y':config.thinking};
|
|
40
51
|
await saveConfig(next); config=resolveConfig(next);
|
|
41
52
|
show(`Saved to ${configPath}. Environment variables override saved settings.`);
|
|
42
|
-
if(
|
|
53
|
+
if(options.workspace==='setup') { show('Ready. Open a project folder and run kodenique-agent.'); process.exitCode=0; }
|
|
43
54
|
}
|
|
44
|
-
if(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
if(options.workspace!=='setup') {
|
|
56
|
+
let root=await resolveProject(options.workspace||process.cwd());
|
|
57
|
+
ui.header({model:config.model,root,mode:approval,thinking:config.thinking});
|
|
58
|
+
if(approval==='auto-all') ui.note('Auto-all: edits and shell commands run without prompts, with full local permissions.');
|
|
48
59
|
let messages=[]; let usage={input:0,output:0};
|
|
49
60
|
while(!rl.closed) {
|
|
50
|
-
|
|
61
|
+
chatPrompt=true;
|
|
62
|
+
const prompt=await ask(ui.prompt(approval));
|
|
63
|
+
chatPrompt=false;
|
|
51
64
|
if(!prompt) continue;
|
|
52
65
|
if(prompt==='/exit') break;
|
|
66
|
+
if(prompt==='/') {commandMenu();continue;}
|
|
67
|
+
if(prompt==='/project' || prompt.startsWith('/project ')) {
|
|
68
|
+
const input=prompt==='/project'?await ask(`Project folder (current: ${root}; Enter keeps it): `):prompt.slice(9).trim();
|
|
69
|
+
if(!input) continue;
|
|
70
|
+
try {
|
|
71
|
+
const next=await resolveProject(input,root);
|
|
72
|
+
if(next!==root) {root=next;messages=[];ui.success('Project opened. Conversation cleared.');}
|
|
73
|
+
ui.header({model:config.model,root,mode:approval,thinking:config.thinking});
|
|
74
|
+
} catch(e) {ui.error(`Cannot open project: ${e.message}. Current project unchanged.`);}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if(prompt==='/files') {
|
|
78
|
+
try {ui.label(root);show(await executeTool(root,{name:'list_files',input:{path:'.'}},async()=>false));}
|
|
79
|
+
catch(e) {ui.error(e.message);}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
53
82
|
if(prompt==='/help') { show(help); continue; }
|
|
54
83
|
if(prompt==='/clear') { messages=[]; show('Conversation cleared.'); continue; }
|
|
55
84
|
if(prompt==='/model' || prompt==='/models') {
|
|
@@ -63,6 +92,15 @@ try {
|
|
|
63
92
|
}
|
|
64
93
|
continue;
|
|
65
94
|
}
|
|
95
|
+
if(prompt==='/status') {ui.header({model:config.model,root,mode:approval,thinking:config.thinking});continue;}
|
|
96
|
+
if(prompt==='/approval' || prompt.startsWith('/approval ')) {
|
|
97
|
+
const next=prompt==='/approval'?await ask(`Approval mode: ask / auto-edit / auto-all [${approval}]: `):prompt.slice(10).trim();
|
|
98
|
+
if(!next) continue;
|
|
99
|
+
if(!approvalModes.includes(next)) {ui.error('Choose ask, auto-edit, or auto-all.');continue;}
|
|
100
|
+
approval=next;ui.success(`Approval mode: ${approval} (this session only)`);
|
|
101
|
+
if(approval==='auto-all') ui.note('Edits and shell commands run without prompts, with full local permissions.');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
66
104
|
if(prompt==='/usage') { show(`Session usage: ${usage.input} input / ${usage.output} output tokens (reported by API).`); continue; }
|
|
67
105
|
if(prompt==='/thinking on' || prompt==='/thinking off') { config.thinking=prompt.endsWith(' on'); show(`Thinking summaries ${config.thinking?'on':'off'} for this session.`); continue; }
|
|
68
106
|
if(prompt.startsWith('/')) { show('Unknown command. Use /help.'); continue; }
|
|
@@ -91,9 +129,9 @@ try {
|
|
|
91
129
|
if(event.type==='block_start') {
|
|
92
130
|
finish();
|
|
93
131
|
const type=event.block.type;
|
|
94
|
-
if(type==='thinking') {
|
|
132
|
+
if(type==='thinking') {ui.label('◌ Thinking summary');line=true;}
|
|
95
133
|
if(type==='redacted_thinking') show('Thinking… (content not provided by API)');
|
|
96
|
-
if(type==='text') {
|
|
134
|
+
if(type==='text') {ui.label('◇ Claude');line=true;}
|
|
97
135
|
if(type==='tool_use') show(`Preparing ${event.block.name}…`);
|
|
98
136
|
}
|
|
99
137
|
if(event.type==='text' || event.type==='thinking') flush(event.text);
|
|
@@ -101,14 +139,29 @@ try {
|
|
|
101
139
|
}});
|
|
102
140
|
} finally {clearInterval(timer);stopWait();finish();}
|
|
103
141
|
},execute:async call=>{
|
|
104
|
-
|
|
142
|
+
ui.label(`› ${call.name}${call.input?.path?' · '+call.input.path:''}`);
|
|
105
143
|
try {
|
|
106
|
-
const result=await executeTool(root,call,async description=>{
|
|
107
|
-
|
|
108
|
-
|
|
144
|
+
const result=await executeTool(root,call,async (description,action)=>{
|
|
145
|
+
if(action.tool==='write_file') {ui.note(`Edit · ${action.path}`);ui.diff(editPreview(action.before,action.after));}
|
|
146
|
+
else ui.note(`$ ${action.command}`);
|
|
147
|
+
if(autoApproves(approval,action.tool)) {ui.note(`Auto-approved · ${approval}`);return true;}
|
|
148
|
+
while(true) {
|
|
149
|
+
const answer=(await ask('Allow? [y/N] · /full shows complete change: ')).toLowerCase();
|
|
150
|
+
if(answer==='/full') {show(description);continue;}
|
|
151
|
+
return answer==='y';
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
ui.success('✓ Completed'); return result;
|
|
155
|
+
} catch(e) {ui.error(`✗ ${e.message}`);throw e;}
|
|
109
156
|
}});
|
|
110
|
-
|
|
111
|
-
} catch(e) {
|
|
157
|
+
ui.note(`Done · ${((Date.now()-start)/1000).toFixed(1)}s · session tokens ${usage.input} in / ${usage.output} out`);
|
|
158
|
+
} catch(e) {
|
|
159
|
+
if(e.code==='ANTHROPIC_RATE_LIMIT') {
|
|
160
|
+
ui.error('Claude API limit reached. This request stopped; kodenique-agent will not retry or send the failed tool back to Claude.');
|
|
161
|
+
show(e.message);
|
|
162
|
+
if(messages.at(-1)?.role==='user') messages.pop();
|
|
163
|
+
} else show(`Error: ${e.message}\nUse /clear to reset, or try again. For unsupported thinking, use /thinking off.`);
|
|
164
|
+
}
|
|
112
165
|
}
|
|
113
166
|
}
|
|
114
167
|
} catch(e) { show(`Error: ${e.message}`); process.exitCode=1; }
|
package/commands.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
export const commands=[
|
|
6
|
+
['/project','Open a local project folder'],
|
|
7
|
+
['/files','List files in the current project'],
|
|
8
|
+
['/model','Fetch and choose a Claude model'],
|
|
9
|
+
['/approval','Choose ask, auto-edit, or auto-all'],
|
|
10
|
+
['/thinking on','Enable thinking summaries'],
|
|
11
|
+
['/thinking off','Disable thinking summaries'],
|
|
12
|
+
['/status','Show project, model, and approval mode'],
|
|
13
|
+
['/usage','Show session token usage'],
|
|
14
|
+
['/clear','Start a fresh conversation'],
|
|
15
|
+
['/help','Show startup options and commands'],
|
|
16
|
+
['/exit','Close kodenique-agent'],
|
|
17
|
+
];
|
|
18
|
+
export function completeCommand(line) {
|
|
19
|
+
const matches=commands.map(([name])=>name).filter(name=>name.startsWith(line));
|
|
20
|
+
return [line.startsWith('/')?matches:[],line];
|
|
21
|
+
}
|
|
22
|
+
export async function resolveProject(input,base=process.cwd()) {
|
|
23
|
+
let name=input.trim();
|
|
24
|
+
if((name.startsWith('"')&&name.endsWith('"'))||(name.startsWith("'")&&name.endsWith("'"))) name=name.slice(1,-1);
|
|
25
|
+
// Accept Finder/terminal drag-and-drop paths without executing shell expansions.
|
|
26
|
+
name=name.replace(/\\([ ()'"&])/g,'$1');
|
|
27
|
+
if(name==='~') name=os.homedir();
|
|
28
|
+
else if(name.startsWith('~/')) name=path.join(os.homedir(),name.slice(2));
|
|
29
|
+
if(!name) throw Error('Enter a project folder path.');
|
|
30
|
+
const root=await fs.realpath(path.resolve(base,name));
|
|
31
|
+
if(!(await fs.stat(root)).isDirectory()) throw Error('Project must be an existing directory.');
|
|
32
|
+
return root;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodenique-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Your team's terminal coding agent, powered directly by the Claude API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"stream.mjs",
|
|
20
20
|
"config.mjs",
|
|
21
21
|
"models.mjs",
|
|
22
|
+
"ui.mjs",
|
|
23
|
+
"commands.mjs",
|
|
22
24
|
"README.md"
|
|
23
25
|
],
|
|
24
26
|
"scripts": {
|
package/ui.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const approvalModes=['ask','auto-edit','auto-all'];
|
|
2
|
+
export function autoApproves(mode,tool) {
|
|
3
|
+
return mode==='auto-all' && ['write_file','run_command'].includes(tool) || mode==='auto-edit' && tool==='write_file';
|
|
4
|
+
}
|
|
5
|
+
export function parseArgs(args) {
|
|
6
|
+
let mode='ask'; const positional=[];
|
|
7
|
+
for(let i=0;i<args.length;i++) {
|
|
8
|
+
if(args[i]==='--approval') { mode=args[++i]; if(!approvalModes.includes(mode)) throw Error('--approval must be ask, auto-edit, or auto-all'); }
|
|
9
|
+
else if(args[i]==='--auto') mode='auto-all';
|
|
10
|
+
else if(args[i].startsWith('-')) throw Error(`Unknown option: ${args[i]}`);
|
|
11
|
+
else positional.push(args[i]);
|
|
12
|
+
}
|
|
13
|
+
if(positional.length>1) throw Error('Provide one project folder.');
|
|
14
|
+
return {mode,workspace:positional[0]};
|
|
15
|
+
}
|
|
16
|
+
export function editPreview(oldText,newText,maxLines=60) {
|
|
17
|
+
const before=(oldText??'').split('\n'),after=newText.split('\n');
|
|
18
|
+
let prefix=0,suffix=0;
|
|
19
|
+
while(prefix<before.length && prefix<after.length && before[prefix]===after[prefix]) prefix++;
|
|
20
|
+
while(suffix<before.length-prefix && suffix<after.length-prefix && before.at(-1-suffix)===after.at(-1-suffix)) suffix++;
|
|
21
|
+
const changed=[...before.slice(prefix,before.length-suffix).map(s=>'- '+s),...after.slice(prefix,after.length-suffix).map(s=>'+ '+s)];
|
|
22
|
+
if(!changed.length) return '(no changes)';
|
|
23
|
+
const lines=[`@@ line ${prefix+1} @@`,...changed];
|
|
24
|
+
return lines.length>maxLines?lines.slice(0,maxLines).join('\n')+`\n… ${lines.length-maxLines} more lines (/full shows all)`:lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
export function createUI(clean,output=process.stdout) {
|
|
27
|
+
const color=output.isTTY && !process.env.NO_COLOR && process.env.TERM!=='dumb';
|
|
28
|
+
const paint=(code,text)=>color?`\x1b[${code}m${clean(text)}\x1b[0m`:clean(text);
|
|
29
|
+
const line=text=>output.write(text+'\n');
|
|
30
|
+
return {
|
|
31
|
+
label:text=>line(paint('1;36',text)),
|
|
32
|
+
note:text=>line(paint('2',text)),
|
|
33
|
+
success:text=>line(paint('32',text)),
|
|
34
|
+
error:text=>line(paint('31',text)),
|
|
35
|
+
prompt:mode=>`\n\x01${paint(mode==='auto-all'?'33':'36',`${mode} ❯ `)}\x02`,
|
|
36
|
+
header:({model,root,mode,thinking})=>{
|
|
37
|
+
line('');line(paint('1;36',' ◇ KODENIQUE')+paint('2',' / terminal agent · 0.5.0'));
|
|
38
|
+
line(paint('2',' ─────────────────────────────────────────────'));
|
|
39
|
+
line(' '+paint('1',model));line(' '+paint('2',root));
|
|
40
|
+
line(' '+paint(mode==='auto-all'?'33':'36',`approval: ${mode}`)+paint('2',` · thinking: ${thinking?'on':'off'}`));
|
|
41
|
+
line('');line(paint('2',' Type / for commands · Tab to complete · /project to open a folder'));
|
|
42
|
+
},
|
|
43
|
+
diff:text=>{for(const row of text.split('\n')) line(paint(row.startsWith('+ ')?'32':row.startsWith('- ')?'31':'2',row));},
|
|
44
|
+
};
|
|
45
|
+
}
|