tenicli 0.1.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 ADDED
@@ -0,0 +1,135 @@
1
+ <div align="center">
2
+ <pre>
3
+ ██ ██
4
+ ██████████ █████ ████ █ █ ███ ███ █ ███
5
+ ███ ██ █ █ █ ██ █ █ █ █ █
6
+ ██████████ █ ███ █ ██ █ █ █ █
7
+ ██████████ █ █ █ █ █ █ █ █
8
+ ██ ██ ██ █ ████ █ █ ███ ███ ████ ███
9
+ </pre>
10
+
11
+ **⚡ Lightweight AI coding agent for your terminal — fast, compact, multi-provider.**
12
+
13
+ </div>
14
+
15
+ ## Features
16
+
17
+ - **Zero dependencies** — Pure Bun + TypeScript, nothing else
18
+ - **Multi-provider** — Anthropic & OpenAI out of the box, BYOK (Bring Your Own Key)
19
+ - **Agentic** — Autonomous Plan → Execute → Verify loop with 5 built-in tools
20
+ - **Blazing fast** — Sub-200ms startup, compiles to single binary in <1s
21
+ - **Vietnamese-first** — Proper UTF-8 input that actually works (looking at you, Claude Code 👀)
22
+ - **Tokyo Night UI** — Beautiful 256-color terminal theme
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
27
+ # Install & run (requires Bun)
28
+ npx teni
29
+
30
+ # Or clone and run
31
+ git clone https://github.com/Nhqvu2005/TeniCli.git
32
+ cd TeniCli
33
+ bun install
34
+ bun run dev
35
+ ```
36
+
37
+ On first launch, run `/auth` to set your API key:
38
+
39
+ ```
40
+ /auth
41
+ > 1. Anthropic
42
+ > API Key: sk-ant-xxxxx
43
+ ✓ Saved to ~/.tenicli/config.json
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ teni # Interactive chat
50
+ teni "fix the auth bug" # Start with a prompt
51
+ teni -p "explain this" # Non-interactive (print & exit)
52
+ teni -m gpt-4o # Override model
53
+ ```
54
+
55
+ ### In-Chat Commands
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `/model` | Switch AI model (Anthropic / OpenAI) |
60
+ | `/auth` | Configure API key |
61
+ | `/mode` | Toggle ask/auto (confirm before write/exec) |
62
+ | `/compact` | Summarize conversation to save tokens |
63
+ | `/diff` | List all files changed this session |
64
+ | `/undo` | Revert last file write |
65
+ | `/init` | Create `TENICLI.md` project template |
66
+ | `/clear` | Start new conversation |
67
+ | `/cost` | Show token usage |
68
+ | `/help` | List commands |
69
+ | `\\` | Continue input on next line |
70
+
71
+ ## Built-in Tools
72
+
73
+ The agent can autonomously use these tools:
74
+
75
+ | Tool | Description |
76
+ |------|-------------|
77
+ | `read_file` | Read file contents (with optional line range) |
78
+ | `write_file` | Create or overwrite files |
79
+ | `list_dir` | List directory tree |
80
+ | `search_files` | Grep/ripgrep search across codebase |
81
+ | `exec_command` | Execute shell commands (30s timeout) |
82
+
83
+ ## Configuration
84
+
85
+ ### Environment Variables
86
+
87
+ ```bash
88
+ TENICLI_API_KEY # API key (or ANTHROPIC_API_KEY / OPENAI_API_KEY)
89
+ TENICLI_BASE_URL # Custom API endpoint (for proxies)
90
+ TENICLI_MODEL # Default model
91
+ TENICLI_MAX_TOKENS # Max output tokens (default: 8192)
92
+ ```
93
+
94
+ ### System Prompt
95
+
96
+ Create a `TENICLI.md` in your project root (like `CLAUDE.md`) to customize the AI's behavior per project.
97
+
98
+ ### Persistent Config
99
+
100
+ API keys and preferences are stored in `~/.tenicli/config.json`.
101
+
102
+ ## Build
103
+
104
+ ```bash
105
+ # Compile to single binary
106
+ bun run build:win # → teni.exe (Windows)
107
+ bun run build:linux # → teni (Linux)
108
+ bun run build:mac # → teni (macOS)
109
+ ```
110
+
111
+ ## Architecture
112
+
113
+ ```
114
+ src/
115
+ ├── index.ts ← Entry point, CLI args, slash commands
116
+ ├── ui.ts ← Tokyo Night colors, mascot, UTF-8 input
117
+ ├── config.ts ← Multi-provider config, persistent storage
118
+ ├── provider.ts ← Unified streaming (Anthropic + OpenAI SSE)
119
+ ├── tools.ts ← 5 tools: read, write, list, search, exec
120
+ └── chat.ts ← Agentic loop with tool execution cycle
121
+ ```
122
+
123
+ **Total: 6 files, ~700 lines, 0 runtime dependencies.**
124
+
125
+ ## Roadmap
126
+
127
+ - [ ] More providers (Gemini, Ollama/local models)
128
+ - [ ] Web UI for remote access
129
+ - [ ] MCP (Model Context Protocol) support
130
+ - [ ] Session history & replay
131
+ - [ ] Plugin system
132
+
133
+ ## License
134
+
135
+ MIT © [Yan Tenica](https://github.com/Nhqvu2005)
package/README.vi.md ADDED
@@ -0,0 +1,132 @@
1
+ <div align="center">
2
+ <pre>
3
+ ██ ██
4
+ ██████████ █████ ████ █ █ ███ ███ █ ███
5
+ ███ ██ █ █ █ ██ █ █ █ █ █
6
+ ██████████ █ ███ █ ██ █ █ █ █
7
+ ██████████ █ █ █ █ █ █ █ █
8
+ ██ ██ ██ █ ████ █ █ ███ ███ ████ ███
9
+ </pre>
10
+
11
+ **⚡ Trợ lý AI lập trình siêu nhẹ cho terminal — nhanh, đa nền tảng, tự chủ.**
12
+
13
+ </div>
14
+
15
+ ## Tính Năng
16
+
17
+ - **Không phụ thuộc** — Chỉ Bun + TypeScript thuần, không cần cài thêm gì
18
+ - **Đa nhà cung cấp** — Anthropic & OpenAI có sẵn, dùng API key của bạn (BYOK)
19
+ - **Tự động hóa** — Vòng lặp Plan → Execute → Verify với 5 tool tích hợp
20
+ - **Cực nhanh** — Khởi động <200ms, compile thành binary đơn trong <1s
21
+ - **Hỗ trợ tiếng Việt** — Input UTF-8 chuẩn, không bị lỗi mất chữ như Claude Code
22
+ - **Giao diện Tokyo Night** — Theme 256-color đẹp mắt, dễ chịu
23
+
24
+ ## Bắt Đầu Nhanh
25
+
26
+ ```bash
27
+ # Clone và chạy
28
+ git clone https://github.com/Nhqvu2005/TeniCli.git
29
+ cd TeniCli
30
+ bun install
31
+ bun run dev
32
+ ```
33
+
34
+ Lần đầu chạy, dùng `/auth` để cài API key:
35
+
36
+ ```
37
+ /auth
38
+ > 1. Anthropic
39
+ > API Key: sk-ant-xxxxx
40
+ ✓ Đã lưu vào ~/.tenicli/config.json
41
+ ```
42
+
43
+ ## Cách Dùng
44
+
45
+ ```bash
46
+ teni # Chat tương tác
47
+ teni "sửa lỗi đăng nhập" # Bắt đầu với prompt
48
+ teni -p "giải thích code" # Chế độ non-interactive
49
+ teni -m gpt-4o # Chọn model khác
50
+ ```
51
+
52
+ ### Lệnh Trong Chat
53
+
54
+ | Lệnh | Mô tả |
55
+ |-------|-------|
56
+ | `/model` | Chuyển đổi model AI (Anthropic / OpenAI) |
57
+ | `/auth` | Cấu hình API key |
58
+ | `/mode` | Bật/tắt chế độ ask/auto (hỏi trước khi ghi/chạy lệnh) |
59
+ | `/compact` | Tóm tắt hội thoại bằng AI để tiết kiệm token |
60
+ | `/diff` | Xem danh sách file đã thay đổi trong session |
61
+ | `/undo` | Hoàn tác file vừa ghi |
62
+ | `/init` | Tạo file `TENICLI.md` template cho project |
63
+ | `/clear` | Bắt đầu cuộc trò chuyện mới |
64
+ | `/cost` | Xem token đã dùng |
65
+ | `/help` | Danh sách lệnh |
66
+ | `\\\\` | Xuống dòng tiếp tục nhập |
67
+
68
+ ## Tool Tích Hợp
69
+
70
+ Agent có thể tự động sử dụng các tool sau:
71
+
72
+ | Tool | Mô tả |
73
+ |------|-------|
74
+ | `read_file` | Đọc nội dung file (hỗ trợ chọn dòng) |
75
+ | `write_file` | Tạo hoặc ghi đè file |
76
+ | `list_dir` | Liệt kê cây thư mục |
77
+ | `search_files` | Tìm kiếm text trong codebase (grep/ripgrep) |
78
+ | `exec_command` | Chạy lệnh shell (timeout 30s) |
79
+
80
+ ## Cấu Hình
81
+
82
+ ### Biến Môi Trường
83
+
84
+ ```bash
85
+ TENICLI_API_KEY # API key (hoặc ANTHROPIC_API_KEY / OPENAI_API_KEY)
86
+ TENICLI_BASE_URL # Endpoint API tuỳ chỉnh (cho proxy)
87
+ TENICLI_MODEL # Model mặc định
88
+ TENICLI_MAX_TOKENS # Số token output tối đa (mặc định: 8192)
89
+ ```
90
+
91
+ ### System Prompt
92
+
93
+ Tạo file `TENICLI.md` trong thư mục gốc dự án (giống `CLAUDE.md`) để tuỳ chỉnh hành vi AI cho từng project.
94
+
95
+ ### Cấu Hình Bền Vững
96
+
97
+ API keys và tuỳ chọn được lưu tại `~/.tenicli/config.json`.
98
+
99
+ ## Build
100
+
101
+ ```bash
102
+ # Compile thành binary đơn
103
+ bun run build:win # → teni.exe (Windows)
104
+ bun run build:linux # → teni (Linux)
105
+ bun run build:mac # → teni (macOS)
106
+ ```
107
+
108
+ ## Kiến Trúc
109
+
110
+ ```
111
+ src/
112
+ ├── index.ts ← Entry point, phân tích args, lệnh slash
113
+ ├── ui.ts ← Màu Tokyo Night, mascot, input UTF-8
114
+ ├── config.ts ← Config đa provider, lưu trữ bền vững
115
+ ├── provider.ts ← Streaming thống nhất (Anthropic + OpenAI SSE)
116
+ ├── tools.ts ← 5 tools: đọc, ghi, liệt kê, tìm, thực thi
117
+ └── chat.ts ← Vòng lặp agentic với tool execution
118
+ ```
119
+
120
+ **Tổng cộng: 6 files, ~700 dòng, 0 phụ thuộc runtime.**
121
+
122
+ ## Lộ Trình
123
+
124
+ - [ ] Thêm provider (Gemini, Ollama/local models)
125
+ - [ ] Giao diện web để truy cập từ xa
126
+ - [ ] Hỗ trợ MCP (Model Context Protocol)
127
+ - [ ] Lịch sử session & replay
128
+ - [ ] Hệ thống plugin
129
+
130
+ ## Giấy Phép
131
+
132
+ MIT © [Yan Tenica](https://github.com/Nhqvu2005)
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as zQ}from"node:module";var GQ=zQ(import.meta.url);import{existsSync as C,readFileSync as b,writeFileSync as VQ,mkdirSync as XQ}from"fs";import{join as E}from"path";var D=[{id:"claude-sonnet-4-20250514",name:"Claude Sonnet 4",provider:"anthropic",speed:"fast"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5",provider:"anthropic",speed:"fast"},{id:"claude-opus-4-20250514",name:"Claude Opus 4",provider:"anthropic",speed:"slow"},{id:"gpt-4o",name:"GPT-4o",provider:"openai",speed:"fast"},{id:"gpt-4o-mini",name:"GPT-4o Mini",provider:"openai",speed:"fast"},{id:"o3-mini",name:"o3-mini",provider:"openai",speed:"normal"}];function h(){let Q=process.env.HOME||process.env.USERPROFILE||"";return E(Q,".tenicli")}function w(){return E(h(),"config.json")}function F(){try{if(C(w()))return JSON.parse(b(w(),"utf8"))}catch{}return{}}function P(Q){let Z=h();if(!C(Z))XQ(Z,{recursive:!0});let $=F(),z={...$,...Q,keys:{...$.keys,...Q.keys},baseUrls:{...$.baseUrls,...Q.baseUrls}};VQ(w(),JSON.stringify(z,null,2),"utf8")}function m(){let Q=process.cwd();u(E(Q,".tenicli.env")),u(E(Q,".env"));let Z=F(),$=process.env,z=$.TENICLI_MODEL||Z.activeModel||D[0].id,X=D.find((W)=>W.id===z)?.provider||$.TENICLI_PROVIDER||"anthropic",Y=YQ(X,Z,$),K=X==="openai"?"https://api.openai.com":"https://api.anthropic.com",q=$.TENICLI_BASE_URL||Z.baseUrls?.[X]||K;return{provider:{type:X,baseUrl:q,apiKey:Y,model:z},maxTokens:parseInt($.TENICLI_MAX_TOKENS||"8192"),systemPrompt:KQ(Q),cwd:Q}}function YQ(Q,Z,$){if(Q==="anthropic")return $.TENICLI_API_KEY||$.ANTHROPIC_API_KEY||Z.keys?.anthropic||"";if(Q==="openai")return $.TENICLI_API_KEY||$.OPENAI_API_KEY||Z.keys?.openai||"";return $.TENICLI_API_KEY||""}function u(Q){try{if(!C(Q))return;for(let Z of b(Q,"utf8").split(`
3
+ `)){let $=Z.trim();if(!$||$.startsWith("#"))continue;let z=$.indexOf("=");if(z===-1)continue;let V=$.slice(0,z).trim(),X=$.slice(z+1).trim();if(X.startsWith('"')&&X.endsWith('"')||X.startsWith("'")&&X.endsWith("'"))X=X.slice(1,-1);if(!process.env[V])process.env[V]=X}}catch{}}function KQ(Q){for(let Z of[E(Q,"TENICLI.md"),E(h(),"TENICLI.md")])if(C(Z))return b(Z,"utf8");return qQ}var qQ=`You are TeniCLI, a fast AI coding assistant in the terminal.
4
+
5
+ TOOLS: read/write files, execute commands, search code, list directories.
6
+
7
+ RULES:
8
+ - Be concise. Show only what matters.
9
+ - Use tools proactively — read before edit, verify after changes.
10
+ - Ask before destructive operations (delete, overwrite).
11
+ - The user may write in Vietnamese — respond in the same language they use.
12
+ - Write production-quality code matching the project's style.`;async function*x(Q,Z,$,z,V){if(Q.type==="openai")yield*JQ(Q,Z,$,z,V);else yield*WQ(Q,Z,$,z,V)}async function*WQ(Q,Z,$,z,V){let X=`${Q.baseUrl.replace(/\/$/,"")}/v1/messages`,Y={model:Q.model,max_tokens:V,system:$,messages:Z,stream:!0};if(z.length)Y.tools=z;let K=await p(X,Y,{"anthropic-version":"2023-06-01","x-api-key":Q.apiKey,authorization:`Bearer ${Q.apiKey}`});for await(let q of d(K))switch(q.type){case"message_start":if(q.message?.usage)yield{type:"usage",input:q.message.usage.input_tokens||0,output:0};break;case"content_block_start":if(q.content_block?.type==="text")yield{type:"text",text:""};else if(q.content_block?.type==="tool_use")yield{type:"tool_start",id:q.content_block.id,name:q.content_block.name};break;case"content_block_delta":if(q.delta?.type==="text_delta")yield{type:"text",text:q.delta.text};else if(q.delta?.type==="input_json_delta")yield{type:"tool_input",partial:q.delta.partial_json};break;case"content_block_stop":yield{type:"tool_end"};break;case"message_delta":if(q.usage)yield{type:"usage",input:0,output:q.usage.output_tokens||0};yield{type:"done",stopReason:q.delta?.stop_reason||"end_turn"};break}}async function*JQ(Q,Z,$,z,V){let X=`${Q.baseUrl.replace(/\/$/,"")}/v1/chat/completions`,Y=BQ(Z,$),K=z.map((_)=>({type:"function",function:{name:_.name,description:_.description,parameters:_.input_schema}})),q={model:Q.model,max_tokens:V,messages:Y,stream:!0,stream_options:{include_usage:!0}};if(K.length)q.tools=K;let W=await p(X,q,{authorization:`Bearer ${Q.apiKey}`}),N=new Map;for await(let _ of d(W)){let B=_.choices?.[0];if(!B){if(_.usage)yield{type:"usage",input:_.usage.prompt_tokens||0,output:_.usage.completion_tokens||0};continue}let A=B.delta||{};if(A.content)yield{type:"text",text:A.content};if(A.tool_calls)for(let R of A.tool_calls){if(R.id)N.set(R.index,{id:R.id,name:R.function?.name||"",args:""}),yield{type:"tool_start",id:R.id,name:R.function?.name||""};if(R.function?.arguments){let g=N.get(R.index);if(g)g.args+=R.function.arguments;yield{type:"tool_input",partial:R.function.arguments}}}if(B.finish_reason){for(let[,R]of N)yield{type:"tool_end"};yield{type:"done",stopReason:B.finish_reason==="tool_calls"?"tool_use":B.finish_reason}}}}function BQ(Q,Z){let $=[{role:"system",content:Z}];for(let z of Q)if(z.role==="user")if(typeof z.content==="string")$.push({role:"user",content:z.content});else{let V=z.content;for(let X of V)if(X.type==="tool_result")$.push({role:"tool",tool_call_id:X.tool_use_id,content:X.content||""});else $.push({role:"user",content:X.text||""})}else if(typeof z.content==="string")$.push({role:"assistant",content:z.content});else{let V=z.content,X=V.filter((K)=>K.type==="tool_use"),Y=V.filter((K)=>K.type==="text").map((K)=>K.text).join("");if(X.length)$.push({role:"assistant",content:Y||null,tool_calls:X.map((K)=>({id:K.id,type:"function",function:{name:K.name,arguments:JSON.stringify(K.input||{})}}))});else $.push({role:"assistant",content:Y})}return $}async function p(Q,Z,$){let z=await fetch(Q,{method:"POST",headers:{"content-type":"application/json",...$},body:JSON.stringify(Z)});if(!z.ok){let V=await z.text();throw Error(`API ${z.status}: ${V.slice(0,300)}`)}return z}async function*d(Q){let Z=Q.body.getReader(),$=new TextDecoder,z="";while(!0){let{done:V,value:X}=await Z.read();if(V)break;z+=$.decode(X,{stream:!0});let Y=z.split(`
13
+ `);z=Y.pop();for(let K of Y)if(K.startsWith("data: ")){let q=K.slice(6).trim();if(q==="[DONE]")return;try{yield JSON.parse(q)}catch{}}}}import{readFileSync as f,writeFileSync as i,existsSync as S,readdirSync as r,statSync as o,mkdirSync as NQ}from"fs";import{resolve as RQ,relative as k,join as t,dirname as HQ}from"path";var l=(Q)=>`\x1B[${Q}m`,L=(Q,Z)=>($)=>`${l(Q)}${$}${l(Z)}`,H=(Q)=>(Z)=>`\x1B[38;5;${Q}m${Z}\x1B[39m`,G={bold:L("1","22"),dim:L("2","22"),italic:L("3","23"),under:L("4","24"),blue:H(111),purple:H(141),green:H(149),yellow:H(179),pink:H(210),cyan:H(117),gray:H(60),text:H(146),orange:H(215)};var J={prompt:G.blue("❯"),ai:G.purple("◆"),tool:G.yellow("⚙"),ok:G.green("✓"),err:G.pink("✗"),warn:G.yellow("⚠"),arrow:G.gray("→"),dot:G.gray("•"),spinner:["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]};function _Q(){let Q=[" ██ ██ ","██████████","███ ██ █","██████████","██████████"," ██ ██ ██ "],Z={T:["█████"," █ "," █ "," █ "," █ "],E:["████ ","█ ","███ ","█ ","████ "],N:["█ █ ","██ █ ","█ ██ ","█ █ ","█ █ "],I:["███"," █ "," █ "," █ ","███"],space:[" "," "," "," "," "],C:[" ███","█ ","█ ","█ "," ███"],L:["█ ","█ ","█ ","█ ","████"]},$=[Z.T,Z.E,Z.N,Z.I,Z.space,Z.C,Z.L,Z.I],z=["","","","",""];for(let Y=0;Y<5;Y++)z[Y]=$.map((K)=>K[Y]).join(" ");let V=Q.map((Y)=>G.cyan(Y.padEnd(14," "))),X=[" ".repeat(z[0].length),...z].map((Y)=>G.blue(Y));return V.map((Y,K)=>`${Y} ${X[K]||""}`).join(`
14
+ `)}function a(){console.log(),console.log(_Q()),console.log(),console.log(G.gray(" ────────────────────────────────────────────────────────────────────────")),console.log(G.gray(" type to chat")+` ${J.dot} `+G.gray("/help for commands")+` ${J.dot} `+G.gray("v0.1.0")),console.log()}class T{i=0;timer=null;msg;constructor(Q="Thinking"){this.msg=Q}start(){return this.timer=setInterval(()=>{process.stdout.write(`\x1B[2K\r ${G.blue(J.spinner[this.i%J.spinner.length])} ${G.gray(this.msg)}`),this.i++},80),this}stop(){if(this.timer)clearInterval(this.timer),this.timer=null;process.stdout.write("\x1B[2K\r")}}function U(Q){return new Promise((Z,$)=>{process.stdout.write(Q);let z=[],V=(Y)=>{if(Y[0]===3)process.stdout.write(`
15
+ `),process.exit(0);if(Y[0]===4){X(),$(Error("EOF"));return}z.push(Y);let K=Buffer.concat(z).toString("utf8"),q=K.indexOf(`
16
+ `);if(q!==-1)X(),Z(K.slice(0,q).replace(/\r$/,""))},X=()=>{process.stdin.removeListener("data",V)};process.stdin.setEncoding("utf8"),process.stdin.on("data",V),process.stdin.resume()})}async function c(){let Q=[],Z=!0;while(!0){let $=Z?`
17
+ ${J.prompt} `:` ${G.gray("│")} `,z=await U($);if(Z=!1,z.endsWith("\\"))Q.push(z.slice(0,-1));else{Q.push(z);break}}return Q.join(`
18
+ `)}async function y(Q,Z){console.log(`
19
+ ${G.bold(Q)}`),Z.forEach(($,z)=>{let V=G.blue(` ${z+1}.`),X=$.desc?G.gray(` (${$.desc})`):"";console.log(`${V} ${$.label}${X}`)});while(!0){let $=await U(`
20
+ ${G.gray("choose")} ${G.blue("❯")} `),z=parseInt($.trim());if(z>=1&&z<=Z.length)return z-1;console.log(` ${J.warn} enter 1-${Z.length}`)}}function I(Q,Z){console.log(`
21
+ ${J.tool} ${G.yellow(Q)} ${G.gray(Z)}`)}function j(Q){console.error(` ${J.err} ${G.pink(Q)}`)}import{spawn as ZQ}from"child_process";class s{writes=[];recordWrite(Q,Z){let $=S(Q)?f(Q,"utf8"):null;this.writes.push({path:Q,backup:$,newLines:Z.split(`
22
+ `).length,time:new Date})}getChanges(){let Q=new Map;for(let Z of this.writes)Q.set(Z.path,{isNew:Z.backup===null,lines:Z.newLines,time:Z.time});return Array.from(Q.entries()).map(([Z,$])=>({path:Z,...$}))}undo(){let Q=this.writes.pop();if(!Q)return null;if(Q.backup!==null)return i(Q.path,Q.backup,"utf8"),{path:Q.path,restored:!0};else{try{GQ("fs").unlinkSync(Q.path)}catch{}return{path:Q.path,restored:!1}}}get count(){return this.writes.length}clear(){this.writes=[]}}var M=new s,e=[{name:"read_file",description:"Read contents of a file. Returns the file text.",input_schema:{type:"object",properties:{path:{type:"string",description:"File path (relative to cwd or absolute)"},start_line:{type:"number",description:"Optional: start line (1-indexed)"},end_line:{type:"number",description:"Optional: end line (1-indexed, inclusive)"}},required:["path"]}},{name:"write_file",description:"Write content to a file. Creates parent directories if needed.",input_schema:{type:"object",properties:{path:{type:"string",description:"File path"},content:{type:"string",description:"Full file content to write"}},required:["path","content"]}},{name:"list_dir",description:"List files and directories in a path. Returns names with type indicators.",input_schema:{type:"object",properties:{path:{type:"string",description:"Directory path (default: cwd)"},depth:{type:"number",description:"Max depth (default: 1)"}},required:[]}},{name:"search_files",description:"Search for text in files using pattern matching (like grep). Returns matching lines with file paths.",input_schema:{type:"object",properties:{pattern:{type:"string",description:"Text or regex pattern to search for"},path:{type:"string",description:"Directory to search in (default: cwd)"},include:{type:"string",description:'Glob pattern to filter files, e.g. "*.ts"'}},required:["pattern"]}},{name:"exec_command",description:"Execute a shell command. Returns stdout and stderr.",input_schema:{type:"object",properties:{command:{type:"string",description:"Shell command to execute"},cwd:{type:"string",description:"Working directory (default: project cwd)"}},required:["command"]}}],UQ=30000;async function QQ(Q,Z,$){try{let z;switch(Q){case"read_file":z=AQ(Z,$),I("read_file",G.dim(k($,O(Z.path,$))));break;case"write_file":z=DQ(Z,$),I("write_file",G.dim(k($,O(Z.path,$))));break;case"list_dir":z=jQ(Z,$),I("list_dir",G.dim(Z.path||"."));break;case"search_files":z=await OQ(Z,$),I("search_files",G.dim(`"${Z.pattern}"`));break;case"exec_command":z=await EQ(Z,$),I("exec_command",G.dim(n(Z.command,60)));break;default:z=`Unknown tool: ${Q}`}return{type:"tool_result",content:n(z,UQ)}}catch(z){return{type:"tool_result",content:`Error: ${z.message}`,is_error:!0}}}function O(Q,Z){return RQ(Z,Q)}function AQ(Q,Z){let $=O(Q.path,Z);if(!S($))return`File not found: ${Q.path}`;let z=f($,"utf8"),V=z.split(`
23
+ `);if(Q.start_line||Q.end_line){let X=Math.max(1,Q.start_line||1)-1,Y=Math.min(V.length,Q.end_line||V.length);return V.slice(X,Y).map((K,q)=>`${X+q+1}: ${K}`).join(`
24
+ `)}if(V.length>50)return V.map((X,Y)=>`${Y+1}: ${X}`).join(`
25
+ `);return z}function DQ(Q,Z){let $=O(Q.path,Z),z=HQ($);if(!S(z))NQ(z,{recursive:!0});return M.recordWrite($,Q.content),i($,Q.content,"utf8"),`Written ${Q.content.split(`
26
+ `).length} lines to ${Q.path}`}function jQ(Q,Z){let $=O(Q.path||".",Z);if(!S($))return`Directory not found: ${Q.path||"."}`;let z=Q.depth||1,V=[];function X(Y,K){if(K>z)return;try{let q=r(Y);for(let W of q){if(W.startsWith(".")||W==="node_modules")continue;let N=t(Y,W),_=k($,N);try{let B=o(N),A=" ".repeat(K);if(B.isDirectory())V.push(`${A}${_}/`),X(N,K+1);else{let R=B.size>1024?`${(B.size/1024).toFixed(1)}KB`:`${B.size}B`;V.push(`${A}${_} (${R})`)}}catch{}}}catch{}}return X($,0),V.length>0?V.join(`
27
+ `):"(empty directory)"}async function OQ(Q,Z){let $=O(Q.path||".",Z),z=Q.pattern;try{let Y=["-n","--max-count=50","--no-heading"];if(Q.include)Y.push("--glob",Q.include);return Y.push(z,$),await new Promise((q,W)=>{let N=ZQ("rg",Y,{shell:!0}),_="";N.stdout.on("data",(B)=>_+=B.toString()),N.on("close",(B)=>{if(B===0||B===1)q(_.trim()||"No matches found.");else W(Error("rg failed"))}),N.on("error",W)})}catch{}let V=[];function X(Y){try{for(let K of r(Y)){if(K.startsWith(".")||K==="node_modules")continue;let q=t(Y,K);try{let W=o(q);if(W.isDirectory()){X(q);continue}if(W.size>500000)continue;if(Q.include&&!IQ(K,Q.include))continue;let _=f(q,"utf8").split(`
28
+ `);for(let B=0;B<_.length;B++)if(_[B].includes(z)){if(V.push(`${k(Z,q)}:${B+1}: ${_[B].trim()}`),V.length>=50)return}}catch{}}}catch{}}return X($),V.length>0?V.join(`
29
+ `):"No matches found."}async function EQ(Q,Z){let $=O(Q.cwd||".",Z),z=process.platform==="win32",V=z?"cmd":"sh",X=z?"/c":"-c";return new Promise((Y)=>{let K=ZQ(V,[X,Q.command],{cwd:$,env:{...process.env,PAGER:"cat"}}),q="",W="";K.stdout.on("data",(_)=>q+=_.toString()),K.stderr.on("data",(_)=>W+=_.toString());let N=setTimeout(()=>K.kill(),30000);K.on("close",(_)=>{clearTimeout(N);let B="";if(q.trim())B+=q.trim();if(W.trim())B+=(B?`
30
+ `:"")+`[stderr] ${W.trim()}`;B+=`
31
+ [exit code: ${_}]`,Y(B)}),K.on("error",(_)=>{clearTimeout(N),Y(`[error] Failed to start process: ${_.message}`)})})}function n(Q,Z){if(Q.length<=Z)return Q;return Q.slice(0,Z)+`
32
+ ... (truncated, ${Q.length-Z} chars omitted)`}function IQ(Q,Z){if(Z.startsWith("*."))return Q.endsWith(Z.slice(1));return Q.includes(Z.replace(/\*/g,""))}class v{messages=[];tokens={input:0,output:0};cfg;autoMode=!1;constructor(Q){this.cfg=Q}async send(Q){this.messages.push({role:"user",content:Q}),await this.agentLoop()}async agentLoop(){while(!0){let Q=await this.streamResponse();if(Q.stopReason==="tool_use"){let $=Q.content.filter((V)=>V.type==="tool_use"),z=[];for(let V of $){if(!this.autoMode&&(V.name==="write_file"||V.name==="exec_command")){let Y=V.name==="write_file"?V.input?.path:V.input?.command?.slice(0,80);console.log(`
33
+ ${J.warn} ${G.yellow(V.name)} ${G.gray(Y||"")}`);let q=(await U(` ${G.gray("allow?")} ${G.blue("[y/n/auto]")} `)).trim().toLowerCase();if(q==="auto")this.autoMode=!0;else if(q!=="y"&&q!=="yes"&&q!==""){z.push({type:"tool_result",tool_use_id:V.id,content:"User denied this action.",is_error:!0});continue}}let X=await QQ(V.name,V.input,this.cfg.cwd);z.push({type:"tool_result",tool_use_id:V.id,content:X.content,is_error:X.is_error})}this.messages.push({role:"assistant",content:Q.content}),this.messages.push({role:"user",content:z});continue}let Z=Q.content.filter(($)=>$.type==="text").map(($)=>$.text).join("");if(Z)this.messages.push({role:"assistant",content:Z});break}console.log(`
34
+ ${G.gray(`tokens: ${this.tokens.input}↑ ${this.tokens.output}↓`)}`)}async streamResponse(){let Q=new T("Thinking").start(),Z=[],$="",z="",V="",X="",Y="end_turn",K=!1;try{let q=x(this.cfg.provider,this.messages,this.cfg.systemPrompt,e,this.cfg.maxTokens);for await(let W of q)switch(W.type){case"text":if(!K)Q.stop(),K=!0,process.stdout.write(`
35
+ ${J.ai} `);if(W.text)process.stdout.write(W.text);$+=W.text;break;case"tool_start":if(!K)Q.stop(),K=!0;if($)Z.push({type:"text",text:$}),$="";z=W.id,V=W.name,X="";break;case"tool_input":X+=W.partial;break;case"tool_end":if(z){let N={};try{N=JSON.parse(X)}catch{}Z.push({type:"tool_use",id:z,name:V,input:N}),z="",X=""}break;case"usage":this.tokens.input+=W.input,this.tokens.output+=W.output;break;case"done":Y=W.stopReason;break}}catch(q){return Q.stop(),j(q.message),{content:[],stopReason:"error"}}if($)Z.push({type:"text",text:$}),process.stdout.write(`
36
+ `);if(!K)Q.stop();return{content:Z,stopReason:Y}}async compact(){if(this.messages.length<4){console.log(` ${J.warn} Not enough messages to compact.`);return}let Q=new T("Compacting").start();try{let Z="";for(let Y of this.messages)if(typeof Y.content==="string")Z+=`${Y.role}: ${Y.content.slice(0,500)}
37
+ `;else{let K=Y.content.filter((W)=>W.type==="text").map((W)=>W.text?.slice(0,300)).join(" ");if(K)Z+=`${Y.role}: ${K}
38
+ `;let q=Y.content.filter((W)=>W.type==="tool_use").map((W)=>`[tool: ${W.name}]`).join(", ");if(q)Z+=` tools: ${q}
39
+ `}let $=this.messages.length,z=`Summarize this conversation concisely. Keep key decisions, file changes, and current state. Be brief:
40
+
41
+ ${Z.slice(0,6000)}`;this.messages=[{role:"user",content:z},{role:"assistant",content:`[Conversation compacted from ${$} messages. Summary of what happened:]`}];let V=x(this.cfg.provider,[{role:"user",content:z}],"You are a conversation summarizer. Create a brief summary preserving key facts, decisions, and file changes.",[],this.cfg.maxTokens),X="";for await(let Y of V){if(Y.type==="text"&&Y.text)X+=Y.text;if(Y.type==="usage")this.tokens.input+=Y.input,this.tokens.output+=Y.output}this.messages=[{role:"user",content:`[Previous conversation summary]
42
+ ${X}`},{role:"assistant",content:"Understood. I have the context from our previous conversation. How can I continue helping you?"}],Q.stop(),console.log(` ${J.ok} Compacted ${$} messages → 2 ${G.gray(`(saved ~${Math.round(Z.length/4)} tokens)`)}`)}catch(Z){Q.stop(),j(`Compact failed: ${Z.message}`)}}get stats(){return this.tokens}get messageCount(){return this.messages.length}clear(){this.messages=[],this.tokens={input:0,output:0}}}import{writeFileSync as MQ,existsSync as CQ}from"fs";import{join as FQ,relative as $Q}from"path";var PQ="0.1.0";function LQ(Q){let Z={prompt:"",print:!1},$=0;while($<Q.length){switch(Q[$]){case"-p":case"--print":if(Z.print=!0,$+1<Q.length&&!Q[$+1].startsWith("-"))Z.prompt=Q[++$];break;case"-m":case"--model":Z.model=Q[++$];break;case"--base-url":Z.baseUrl=Q[++$];break;case"-v":case"--version":console.log(`teni v${PQ}`),process.exit(0);case"-h":case"--help":TQ(),process.exit(0);default:if(!Q[$].startsWith("-"))Z.prompt=Q.slice($).join(" "),$=Q.length}$++}return Z}function TQ(){console.log(`
43
+ ${G.bold(G.blue("TeniCLI"))} — Lightweight AI Coding Agent
44
+
45
+ ${G.bold("USAGE")}
46
+ teni Start chatting
47
+ teni "prompt" Start with a prompt
48
+ teni -p "prompt" Non-interactive mode
49
+
50
+ ${G.bold("OPTIONS")}
51
+ -p, --print <prompt> Print response and exit
52
+ -m, --model <model> Override model
53
+ --base-url <url> Override API base URL
54
+ -v, --version Show version
55
+ -h, --help Show help
56
+
57
+ ${G.bold("IN-CHAT")}
58
+ /model Select model /auth Set API key
59
+ /mode Ask/Auto toggle /compact Summarize chat
60
+ /diff Files changed /undo Revert last write
61
+ /init Create TENICLI.md /clear New conversation
62
+ /cost Token usage /exit Quit
63
+ \\\\ Multiline input
64
+ `)}async function kQ(Q,Z){switch(Q.toLowerCase().split(" ")[0]){case"/exit":case"/quit":case"/q":console.log(`
65
+ ${G.gray("Bye!")} \uD83D\uDC4B
66
+ `),process.exit(0);case"/clear":return Z.clear(),M.clear(),console.log(` ${J.ok} Conversation cleared`),!0;case"/compact":return await Z.compact(),!0;case"/diff":{let $=M.getChanges();if($.length===0)console.log(` ${G.gray("No files changed in this session.")}`);else{console.log(`
67
+ ${G.bold("Files changed this session:")}`);for(let z of $){let V=$Q(Z.cfg.cwd,z.path),X=z.isNew?G.green("[NEW]"):G.yellow("[MOD]");console.log(` ${X} ${G.cyan(V)} ${G.gray(`(${z.lines} lines)`)}`)}console.log(` ${G.gray(`total: ${$.length} files`)}`)}return!0}case"/undo":{let $=M.undo();if(!$)console.log(` ${G.gray("Nothing to undo.")}`);else{let z=$Q(Z.cfg.cwd,$.path);if($.restored)console.log(` ${J.ok} Restored: ${G.cyan(z)}`);else console.log(` ${J.ok} Deleted (was new): ${G.cyan(z)}`)}return!0}case"/init":{let $=FQ(Z.cfg.cwd,"TENICLI.md");if(CQ($))console.log(` ${J.warn} TENICLI.md already exists.`);else MQ($,wQ,"utf8"),console.log(` ${J.ok} Created ${G.cyan("TENICLI.md")}`);return!0}case"/mode":{Z.autoMode=!Z.autoMode;let $=Z.autoMode?G.yellow("auto"):G.green("ask");return console.log(` ${J.ok} Mode: ${$} ${G.gray(Z.autoMode?"(tools run without asking)":"(confirm write/exec)")}`),!0}case"/cost":{let $=Z.stats;return console.log(` ${J.ai} ${G.blue(String($.input))}↑ input ${G.blue(String($.output))}↓ output ${G.gray(`(${Z.messageCount} msgs)`)}`),!0}case"/model":{let $=D.map((V)=>({label:`${V.name} ${Z.cfg.provider.model===V.id?G.green("●"):""}`,desc:`${V.provider} • ${V.speed}`}));$.push({label:"Custom model...",desc:"type model ID"});let z=await y("Select model",$);if(z<D.length){let V=D[z];Z.cfg.provider.model=V.id,Z.cfg.provider.type=V.provider;let X=F(),Y=V.provider==="openai"?process.env.OPENAI_API_KEY||X.keys?.openai||"":process.env.ANTHROPIC_API_KEY||X.keys?.anthropic||"";if(Y)Z.cfg.provider.apiKey=Y;if(!X.baseUrls?.[V.provider])Z.cfg.provider.baseUrl=V.provider==="openai"?"https://api.openai.com":"https://api.anthropic.com";P({activeModel:V.id}),console.log(` ${J.ok} Model: ${G.blue(V.name)}`)}else{let V=await U(` ${G.gray("model ID")} ${G.blue("❯")} `);if(V.trim())Z.cfg.provider.model=V.trim(),P({activeModel:V.trim()}),console.log(` ${J.ok} Model: ${G.blue(V.trim())}`)}return!0}case"/auth":{let $=await y("Provider",[{label:"Anthropic",desc:"Claude models"},{label:"OpenAI",desc:"GPT models"},{label:"Custom",desc:"Anthropic-compatible proxy"}]),V=["anthropic","openai","anthropic"][$],X=await U(` ${G.gray("API Key")} ${G.blue("❯")} `);if(!X.trim())return console.log(` ${J.warn} Cancelled`),!0;let Y={[V]:X.trim()},K={};if($===2){let q=await U(` ${G.gray("Base URL")} ${G.blue("❯")} `);if(q.trim())K[V]=q.trim()}if(P({keys:Y,baseUrls:K}),Z.cfg.provider.apiKey=X.trim(),Z.cfg.provider.type=V,K[V])Z.cfg.provider.baseUrl=K[V];return console.log(` ${J.ok} ${V} key saved to ~/.tenicli/config.json`),!0}case"/help":return console.log(`
68
+ ${G.bold("Commands")}
69
+ ${G.blue("/model")} Select AI model
70
+ ${G.blue("/auth")} Set API key
71
+ ${G.blue("/mode")} Toggle ask/auto ${G.gray("(confirm before write/exec)")}
72
+ ${G.blue("/compact")} Summarize conversation ${G.gray("(save tokens)")}
73
+ ${G.blue("/diff")} List files changed this session
74
+ ${G.blue("/undo")} Revert last file write
75
+ ${G.blue("/init")} Create TENICLI.md template
76
+ ${G.blue("/clear")} New conversation
77
+ ${G.blue("/cost")} Show token usage
78
+ ${G.blue("/exit")} Quit
79
+ ${G.gray("\\\\")} Continue on next line`),!0;default:return console.log(` ${J.warn} Unknown: ${Q.split(" ")[0]} — try /help`),!0}}async function SQ(){let Q=LQ(process.argv.slice(2)),Z=m();if(Q.model)Z.provider.model=Q.model;if(Q.baseUrl)Z.provider.baseUrl=Q.baseUrl;let $=new v(Z);if(Q.print&&Q.prompt){if(!Z.provider.apiKey)j("No API key. Run: teni then /auth"),process.exit(1);await $.send(Q.prompt),process.exit(0)}a();let z=D.find((X)=>X.id===Z.provider.model)?.name||Z.provider.model,V=$.autoMode?G.yellow("auto"):G.green("ask");if(console.log(` ${G.gray("model")} ${G.blue(z)} ${G.gray("mode")} ${V} ${G.gray("cwd")} ${G.cyan(Z.cwd)}`),!Z.provider.apiKey)console.log(`
80
+ ${J.warn} ${G.yellow("No API key configured. Run /auth to set one.")}`);if(console.log(),Q.prompt){if(console.log(` ${J.prompt} ${Q.prompt}`),Z.provider.apiKey)await $.send(Q.prompt)}while(!0)try{let Y=(await c()).trim();if(!Y)continue;if(Y.startsWith("/")){await kQ(Y,$);continue}if(!$.cfg.provider.apiKey){console.log(` ${J.warn} ${G.yellow("No API key. Run /auth first.")}`);continue}await $.send(Y)}catch(X){if(X.message==="EOF")console.log(`
81
+ ${G.gray("Bye!")} \uD83D\uDC4B
82
+ `),process.exit(0);j(X.message)}}SQ().catch((Q)=>{j(Q.message),process.exit(1)});var wQ=`# Project Instructions
83
+
84
+ ## Overview
85
+ Describe your project here so the AI understands the context.
86
+
87
+ ## Tech Stack
88
+ - Language:
89
+ - Framework:
90
+ - Database:
91
+
92
+ ## Coding Rules
93
+ - Follow existing code style
94
+ - Write tests for new features
95
+ - Use descriptive variable names
96
+
97
+ ## File Structure
98
+ Describe important files and directories.
99
+
100
+ ## Notes
101
+ Any special instructions or constraints.
102
+ `;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "tenicli",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight AI coding CLI — fast, compact, multi-provider",
5
+ "type": "module",
6
+ "bin": {
7
+ "teni": "./dist/index.js"
8
+ },
9
+ "files": ["dist/", "README.md", "LICENSE"],
10
+ "scripts": {
11
+ "dev": "bun run src/index.ts",
12
+ "build:npm": "bun build src/index.ts --outfile dist/index.js --target node --minify",
13
+ "build": "bun build --compile --minify src/index.ts --outfile teni",
14
+ "build:win": "bun build --compile --minify --target=bun-windows-x64 src/index.ts --outfile teni.exe",
15
+ "build:linux": "bun build --compile --minify --target=bun-linux-x64 src/index.ts --outfile teni",
16
+ "build:mac": "bun build --compile --minify --target=bun-darwin-x64 src/index.ts --outfile teni",
17
+ "prepublishOnly": "bun run build:npm"
18
+ },
19
+ "keywords": ["ai", "cli", "coding", "agent", "anthropic", "openai", "terminal"],
20
+ "author": "Yan Tenica",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Nhqvu2005/TeniCli.git"
25
+ },
26
+ "homepage": "https://github.com/Nhqvu2005/TeniCli",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "dependencies": {},
31
+ "devDependencies": {
32
+ "@types/bun": "latest",
33
+ "@types/figlet": "^1.7.0",
34
+ "figlet": "^1.11.0"
35
+ }
36
+ }