@fullkkk/codiq 0.0.3

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.
Files changed (3) hide show
  1. package/README.md +98 -0
  2. package/dist/index.js +10 -0
  3. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # `Codiq` — 터미널에서 바로 활용하는 프로젝트 분석 & 코딩 도우미
2
+
3
+ ```
4
+ npm i -g @fullkkk/codiq
5
+ ```
6
+
7
+ `codiq`는 현재 디렉터리 안에 있는 프로젝트를 자동으로 파악하고, Ollama 기반 LLM(언어 모델)에게 질문하면 바로 답변을 받아옵니다.
8
+
9
+ - **핵심 기능**
10
+ 1. 현재 프로젝트 구조를 자동 분석
11
+ 2. 실시간 스트리밍 결과 제공
12
+ 3. Ink 기반 TUI(터미널 UI)에서 대화형 사용 가능
13
+ 4. 모델·API 키·시스템 프롬프트를 옵션으로 손쉽게 교체
14
+
15
+ ---
16
+
17
+ ## 📚 목차
18
+
19
+ - [소개](#소개)
20
+ - [필요 요건](#필요-요건)
21
+ - [설치](#설치)
22
+ - [빠른 시작](#빠른-시작)
23
+ - [사용법](#사용법)
24
+ - [CLI 모드](#cli-모드)
25
+ - [TUI 모드](#tui-모드)
26
+ - [옵션 설명](#옵션-설명)
27
+ - [참고 설정](#참고-설정)
28
+ - [기여](#기여)
29
+ - [라이선스](#라이선스)
30
+
31
+ ---
32
+
33
+ ## 🚀 소개
34
+
35
+ `codiq`는 명령줄에서 바로 **프로젝트를 이해해주는** AI 도우미입니다.
36
+
37
+ - 현재 디렉터리의 파일을 분석해 3단계 깊이, 60파일 이하, 32KB 이하 파일 8개 정도를 샘플링해 컨텍스트를 구성합니다.
38
+ - Ollama 모델에게 전달하면 교정·설명·수정 제안 등을 받아볼 수 있습니다.
39
+ - `codiq "질문"` 으로 한 번에 답변을 얻거나, `codiq` 단독 실행으로 AI가 띄우는 TUI에서 대화형으로 질문/답변을 주고받을 수 있습니다.
40
+
41
+ > **주의**
42
+ > `OLLAMA_API_KEY` 환경 변수에 인증 토큰을 설정해야 합니다. Ollama 서버가 아닌 다른 API를 사용하려면 `-u, --base-url` 옵션으로 지정하세요.
43
+
44
+ ---
45
+
46
+ ## 🔧 필요 요건
47
+
48
+ - Node.js 20+ (ESM 모듈 지원)
49
+ - npm / yarn
50
+ - Ollama 등 LLM 서버
51
+ - **(선택)** `curl` 혹은 `httpie` (테스트용)
52
+
53
+ ---
54
+
55
+ ## 📦 설치
56
+
57
+ ```bash
58
+ npm i -g @fullkkk/codiq
59
+ ```
60
+
61
+ ---
62
+
63
+ ## ⚡ 빠른 시작
64
+
65
+ ```bash
66
+ # 단일 질문에 대한 답변
67
+ codiq "프로젝트에서 src/lib/project-context.ts 가 쓴 이유를 설명해줘"
68
+
69
+ # 대화형 TUI 실행
70
+ codiq
71
+
72
+ # TUI에서 입력 → Enter, 종료 → Ctrl‑C, 초기화 → Ctrl‑L
73
+ ```
74
+
75
+ > `codiq "질문"` 을 실행하면
76
+ >
77
+ > 1. 현재 디렉터리 스캔 → 프로젝트 컨텍스트가 시스템 프롬프트에 삽입
78
+ > 2. Ollama에 전송 → 스트리밍 토큰이 바로 화면에 표시
79
+
80
+ ---
81
+
82
+ ## 🛠 옵션 설명
83
+
84
+ | 옵션 | 설명 | 기본값 |
85
+ | ---------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `-m, --model <model>` | 사용하려는 모델 이름 | `gpt-oss:20b` |
87
+ | `-u, --base-url <url>` | API URL | `https://ollama.com/api/chat` |
88
+ | `-s, --system <text>` | 시스템 프롬프트 | `"당신은 shell에서 동작하는 프로젝트 분석가 및 코딩 도우미입니다. 현재 작업 디렉터리를 기준으로 프로젝트를 이해하고, 사용자의 질문에 맞게 설명, 분석, 수정 제안을 |
89
+ | 제공합니다."` |
90
+ | `--stream` | 토큰 단위 스트리밍 출력 | `true` |
91
+
92
+ > **예시**
93
+ >
94
+ > ```bash
95
+ > codiq "파일 탐색 로직은 왜 이렇게 구현했어?" -m ollama/llama3.2 -u https://api.my-ollama.com
96
+ > ```
97
+
98
+ ---
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import{Command as Ft}from"commander";import{render as It}from"ink";import jt from"react";import"dotenv/config";async function M(e,t,o){let s=[...o,{role:"user",content:e}];return t.stream?await tt({apiKey:t.apiKey,baseUrl:t.baseUrl,model:t.model,messages:s,onToken:t.onToken}):await Z({apiKey:t.apiKey,baseUrl:t.baseUrl,model:t.model,messages:s})}async function Z(e){let t=await fetch(e.baseUrl,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},body:JSON.stringify({model:e.model,messages:e.messages,stream:!1})});if(!t.ok)throw new Error(`HTTP ${t.status}: ${await t.text()}`);return(await t.json()).message?.content??""}async function tt(e){let t=await fetch(e.baseUrl,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},body:JSON.stringify({model:e.model,messages:e.messages,stream:!0})});if(!t.ok)throw new Error(`HTTP ${t.status}: ${await t.text()}`);if(!t.body)throw new Error("\uC751\uB2F5 \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");let o=new TextDecoder,s="",r="";for await(let a of t.body){s+=o.decode(a,{stream:!0});let p=s.split(`
3
+ `);s=p.pop()??"";for(let x of p){let m=x.trim();if(!m)continue;let y;try{y=JSON.parse(m)}catch{throw new Error(`\uC2A4\uD2B8\uB9BC \uD30C\uC2F1 \uC2E4\uD328: ${m}`)}let w=y.message?.content??"";w&&(r+=w,e.onToken?.(w))}}s+=o.decode();let n=s.trim();if(!n)return r;let l=JSON.parse(n).message?.content??"";return l&&(r+=l,e.onToken?.(l)),r}import{mkdir as et,readFile as st,writeFile as nt}from"node:fs/promises";import ot from"node:os";import I from"node:path";var j=I.join(ot.homedir(),".codiq"),O=I.join(j,"config.json");async function K(){let e=process.env.OLLAMA_API_KEY?.trim();if(e)return e;let t=await rt();if(t)return t;let o=await at();return await it(o),console.log(`OLLAMA_API_KEY\uB97C ${O} \uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`),o}async function rt(){try{let e=await st(O,"utf8");return JSON.parse(e).ollamaApiKey?.trim()||null}catch{return null}}async function it(e){await et(j,{recursive:!0}),await nt(O,JSON.stringify({ollamaApiKey:e},null,2),"utf8")}async function at(){if(!process.stdin.isTTY||!process.stdout.isTTY)throw new Error("OLLAMA_API_KEY\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uACE0 \uC785\uB825\uC744 \uBC1B\uC744 \uC218 \uC788\uB294 \uD130\uBBF8\uB110\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");console.log("OLLAMA_API_KEY\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."),console.log("\uCD5C\uCD08 1\uD68C\uB9CC \uC785\uB825\uD558\uBA74 \uC0AC\uC6A9\uC790 \uC124\uC815\uC5D0 \uC800\uC7A5\uD574 \uC7AC\uC0AC\uC6A9\uD569\uB2C8\uB2E4.");let e=await ct("OLLAMA_API_KEY: ");if(!e.trim())throw new Error("OLLAMA_API_KEY\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");return e.trim()}async function ct(e){return await new Promise((t,o)=>{let s=process.stdin,r=process.stdout,n=s.isRaw,c="",l=()=>{s.off("data",a),s.isTTY&&s.setRawMode(n??!1),s.pause(),r.write(`
4
+ `)},a=p=>{if(p===""){l(),o(new Error("\uC785\uB825\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));return}if(p==="\r"||p===`
5
+ `){l(),t(c);return}if(p==="\x7F"){c.length>0&&(c=c.slice(0,-1),r.write("\b \b"));return}c+=p,r.write("*")};r.write(e),s.setEncoding("utf8"),s.isTTY&&s.setRawMode(!0),s.resume(),s.on("data",a)})}import{readdir as H,readFile as lt,stat as pt}from"node:fs/promises";import F from"node:path";var mt=new Set([".git","node_modules","dist","build",".next",".turbo","coverage",".idea",".vscode"]),ut=[".env"],k=new Set(["package.json","package-lock.json","tsconfig.json","README.md","README","pyproject.toml","requirements.txt","Cargo.toml","go.mod","Dockerfile","docker-compose.yml"]),dt=3,v=60,ft=32e3,gt=8,yt=1200,$=`\uC544\uB798\uB294 \uD604\uC7AC \uC791\uC5C5 \uB514\uB809\uD130\uB9AC\uB97C \uAE30\uC900\uC73C\uB85C \uC218\uC9D1\uD55C \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8\uC785\uB2C8\uB2E4.
6
+ `,U=`\uC751\uB2F5 \uD615\uC2DD \uC9C0\uCE68:
7
+ `;async function _(e,t,o){let s=e.filter(n=>n.role!=="system"||!n.content.startsWith($)&&!n.content.startsWith(U));if(s.push({role:"system",content:U+wt(t)}),!ht(t))return{history:s,projectContextUsed:!1};let r=await Et(o);return s.push({role:"system",content:$+r}),{history:s,projectContextUsed:!0}}function P(e,t){return[{role:"system",content:e},{role:"system",content:`\uD604\uC7AC \uC0AC\uC6A9\uC790\uAC00 CLI\uB97C \uC2E4\uD589\uD55C \uC791\uC5C5 \uB514\uB809\uD130\uB9AC\uB294 ${t} \uC785\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 \uD604\uC7AC \uD504\uB85C\uC81D\uD2B8, \uD30C\uC77C \uAD6C\uC870, \uCF54\uB4DC \uC218\uC815, \uB9AC\uD329\uD1A0\uB9C1, \uD2B9\uC815 \uD30C\uC77C \uB3D9\uC791\uC744 \uBB3B\uB294 \uACBD\uC6B0 \uC774 \uB514\uB809\uD130\uB9AC\uB97C \uAE30\uC900\uC73C\uB85C \uB2F5\uBCC0\uD558\uC138\uC694.`}]}function ht(e){let t=e.toLowerCase();return[/프로젝트/,/코드베이스/,/저장소/,/리포지토리/,/구조/,/분석/,/설명/,/파악/,/파일/,/수정/,/리팩토링/,/어디/,/엔트리/,/architecture/,/project/,/codebase/,/repository/,/repo/,/file/,/refactor/,/edit/,/fix/].some(s=>s.test(t))}function wt(e){return Ct(e)?"\uC0AC\uC6A9\uC790\uAC00 README.md, \uBB38\uC11C, markdown \uD30C\uC77C, md \uBB38\uC11C \uC791\uC131\uC744 \uC694\uCCAD\uD55C \uC0C1\uD669\uC785\uB2C8\uB2E4. \uC774 \uACBD\uC6B0\uC5D0\uB294 Markdown \uD615\uC2DD\uC744 \uC0AC\uC6A9\uD574\uB3C4 \uB429\uB2C8\uB2E4. \uB300\uC2E0 \uBB38\uC11C \uBCF8\uBB38 \uC790\uCCB4\uB97C \uBC14\uB85C \uC4F8 \uC218 \uC788\uB294 \uD615\uD0DC\uB85C \uCD9C\uB825\uD558\uC138\uC694.":"\uC0AC\uC6A9\uC790\uB294 shell\uC5D0\uC11C \uC751\uB2F5\uC744 \uC77D\uC2B5\uB2C8\uB2E4. Markdown\uC73C\uB85C \uB2F5\uBCC0\uD558\uC9C0 \uB9C8\uC138\uC694. \uCF54\uB4DC \uD39C\uC2A4(```), \uC81C\uBAA9(#), \uAD75\uAC8C(**), \uBAA9\uB85D \uB9C8\uCEE4(-, *, 1.)\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694. \uD56D\uC0C1 plain text\uB85C\uB9CC \uB2F5\uBCC0\uD558\uACE0, \uC9E7\uC740 \uBB38\uB2E8\uC774\uB098 \uC904\uBC14\uAFC8\uB41C \uC77C\uBC18 \uD14D\uC2A4\uD2B8\uB85C \uC124\uBA85\uD558\uC138\uC694."}function Ct(e){let t=e.toLowerCase();return[/readme/,/markdown/,/\bmd\b/,/문서/,/README\.md/i,/사용법/,/guide/,/docs?/].some(s=>s.test(t))}async function Et(e){let t=await N(e,0,[]),o=await At(e),s=[];return s.push(`root: ${e}`),s.push(`top-level entries: ${o.slice(0,20).join(", ")||"(empty)"}`),t.keyFiles.length>0&&s.push(`key files: ${t.keyFiles.join(", ")}`),t.sourceFiles.length>0&&s.push(`source files(sample): ${t.sourceFiles.slice(0,20).join(", ")}`),t.fileSnippets.length>0&&(s.push("file snippets:"),s.push(...t.fileSnippets.map(r=>`- ${r}`))),s.join(`
8
+ `)}async function N(e,t,o){let s={keyFiles:[],sourceFiles:[],fileSnippets:[]};if(t>dt||o.length>=v)return s;let r=await H(e,{withFileTypes:!0});for(let n of r){if(o.length>=v)break;if(n.isDirectory()){if(mt.has(n.name))continue;let a=await N(F.join(e,n.name),t+1,o);s.keyFiles.push(...a.keyFiles),s.sourceFiles.push(...a.sourceFiles),s.fileSnippets.push(...a.fileSnippets);continue}if(!n.isFile()||ut.some(a=>n.name.startsWith(a)))continue;let c=F.join(e,n.name),l=F.relative(process.cwd(),c)||n.name;if(o.push(l),k.has(n.name)&&s.keyFiles.push(l),D(n.name)&&s.sourceFiles.push(l),s.fileSnippets.length<gt&&(k.has(n.name)||D(n.name))){let a=await xt(c,l);a&&s.fileSnippets.push(a)}}return s}async function xt(e,t){try{if((await pt(e)).size>ft)return`${t}: skipped, too large`;let r=(await lt(e,"utf8")).replace(/\s+/g," ").trim().slice(0,yt);return r?`${t}: ${r}`:null}catch{return null}}function D(e){return/\.(ts|tsx|js|jsx|json|md|py|rs|go|java|kt|rb|php|toml|yaml|yml|sh)$/.test(e)}async function At(e){try{return(await H(e)).sort()}catch{return[]}}import{useEffect as X,useRef as Tt,useState as C}from"react";import{Box as E,Text as f,useApp as Pt,useInput as bt}from"ink";import St from"ink-text-input";import{jsx as h,jsxs as g}from"react/jsx-runtime";var Mt="Enter \uC804\uC1A1 | Ctrl+C \uC885\uB8CC | Ctrl+L \uCD08\uAE30\uD654",B="v0.0.3";function Y({apiKey:e,options:t}){let{exit:o}=Pt(),[s,r]=C([{role:"assistant",content:"\uC9C8\uBB38\uC744 \uC785\uB825\uD558\uC138\uC694."}]),[n,c]=C([...P(t.system,process.cwd())]),l=Tt(n),[a,p]=C(""),[x,m]=C("Ready"),[y,w]=C(!1),[z,J]=C(process.stdout.columns??80);X(()=>{l.current=n},[n]),X(()=>{let i=()=>{J(process.stdout.columns??80)};return process.stdout.on("resize",i),()=>{process.stdout.off("resize",i)}},[]),bt((i,u)=>{if(u.ctrl&&i==="c"){o();return}if(u.ctrl&&i==="l"&&!y){r([{role:"assistant",content:"\uB300\uD654 \uAE30\uB85D\uC744 \uCD08\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4."}]),c(P(t.system,process.cwd())),m("Cleared");return}});let W=async()=>{let i=a.trim();if(!i||y)return;p(""),w(!0),m("Streaming...");let u=-1;r(d=>(u=d.length+1,[...d,{role:"user",content:i},{role:"assistant",content:""}]));try{m("Inspecting project...");let{history:d,projectContextUsed:L}=await _(l.current,i,process.cwd()),R=[...d,{role:"user",content:i}];m(L?"Streaming with project context...":"Streaming...");let b=await M(i,{apiKey:e,model:t.model,baseUrl:t.baseUrl,system:t.system,stream:t.stream,onToken:A=>{r(S=>S.map((T,Q)=>Q===u?{...T,content:T.content+A}:T))}},d);r(A=>A.map((S,T)=>T===u?{...S,content:b}:S)),c([...R,{role:"assistant",content:b}]),m("Ready")}catch(d){let L=d instanceof Error?d.message:String(d);r(R=>R.map((b,A)=>A===u?{role:"assistant",content:`\uC694\uCCAD \uC2E4\uD328: ${L}`}:b)),m("Error")}finally{w(!1)}},G=Math.max(z-4,20),V=_t(s,G,y);return g(E,{flexDirection:"column",children:[g(E,{borderStyle:"round",borderColor:"cyan",paddingX:1,children:[h(f,{color:"cyan",children:"Codiq"}),g(f,{dimColor:!0,children:[" ",`(${B})`]})]}),h(E,{flexDirection:"column",paddingX:1,children:V.map((i,u)=>g(f,{children:[h(f,{color:Ot(i.role),children:i.prefix}),i.text]},`${u}-${i.role}-${i.prefix}-${i.text}`))}),g(E,{borderStyle:"round",borderColor:"green",paddingX:1,children:[h(f,{color:"green",children:"> "}),h(St,{value:a,onChange:p,onSubmit:()=>{W()},placeholder:y?"\uC751\uB2F5 \uC0DD\uC131 \uC911...":"\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694",showCursor:!0})]}),g(E,{justifyContent:"space-between",children:[g(f,{dimColor:!0,children:["Codiq (",B,") | model=",t.model," | status=",x]}),g(f,{dimColor:!0,children:["messages=",s.length]})]}),h(E,{children:h(f,{dimColor:!0,children:Mt})})]})}function _t(e,t,o){return e.flatMap((s,r)=>{let n=`${Rt(s.role)}: `,c=" ".repeat(n.length),l=s.content||(o&&r===e.length-1?"...":"");return Lt(l,Math.max(t-n.length,8)).map((p,x)=>({role:s.role,prefix:x===0?n:c,text:p}))})}function Lt(e,t){let o=e.split(`
9
+ `),s=[];for(let r of o){if(r.length===0){s.push("");continue}let n=r;for(;n.length>t;)s.push(n.slice(0,t)),n=n.slice(t);s.push(n)}return s.length>0?s:[""]}function Rt(e){switch(e){case"user":return"You";case"assistant":return"AI";default:return"System"}}function Ot(e){switch(e){case"user":return"yellow";case"assistant":return"green";default:return"cyan"}}var q=new Ft,Kt="https://ollama.com/api/chat",kt="gpt-oss:20b",vt="\uB2F9\uC2E0\uC740 shell\uC5D0\uC11C \uB3D9\uC791\uD558\uB294 \uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D\uAC00 \uBC0F \uCF54\uB529 \uB3C4\uC6B0\uBBF8\uC785\uB2C8\uB2E4. \uD604\uC7AC \uC791\uC5C5 \uB514\uB809\uD130\uB9AC\uB97C \uAE30\uC900\uC73C\uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uC774\uD574\uD558\uACE0, \uC0AC\uC6A9\uC790\uC758 \uC9C8\uBB38\uC5D0 \uB9DE\uAC8C \uC124\uBA85, \uBD84\uC11D, \uC218\uC815 \uC81C\uC548\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.";q.name("codiq").description("A simple Ollama Cloud CLI tool").version("1.0.0").argument("[prompt...]","\uC9C8\uBB38 \uB0B4\uC6A9").option("-m, --model <model>","\uBAA8\uB378\uBA85",kt).option("-u, --base-url <url>","API URL",Kt).option("-s, --system <text>","System \uD504\uB86C\uD504\uD2B8",vt).option("--stream","\uC2A4\uD2B8\uB9AC\uBC0D \uCD9C\uB825",!0).action(async(e,t)=>{let o=e.join(" ").trim(),s=await K();if(o){let r=P(t.system,process.cwd()),{history:n}=await _(r,o,process.cwd());await M(o,{...t,apiKey:s,onToken:c=>{process.stdout.write(c)}},n),process.stdout.write(`
10
+ `);return}$t(s,t)});await q.parseAsync();function $t(e,t){It(jt.createElement(Y,{apiKey:e,options:t}),{exitOnCtrlC:!0})}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@fullkkk/codiq",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "bin": {
7
+ "codiq": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit",
11
+ "build": "npm run typecheck && node scripts/build.mjs",
12
+ "prepublishOnly": "npm run build",
13
+ "dev": "node --loader ts-node/esm src/index.ts",
14
+ "format": "prettier --write .",
15
+ "codiq": "node dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "keywords": [],
22
+ "author": "",
23
+ "license": "ISC",
24
+ "description": "",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^13.1.0",
30
+ "dotenv": "^17.4.0",
31
+ "ink": "^5.2.1",
32
+ "ink-text-input": "^6.0.0",
33
+ "react": "^18.3.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.5.0",
37
+ "@types/react": "^19.2.14",
38
+ "esbuild": "^0.27.5",
39
+ "prettier": "^3.8.1",
40
+ "ts-node": "^10.9.2",
41
+ "typescript": "^6.0.2"
42
+ }
43
+ }