ikie-cli 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.
@@ -0,0 +1,239 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
3
+ import { basename, extname, join, resolve } from 'path';
4
+ import { HOME_DIR, ensureHome } from './config.js';
5
+ const MIME_BY_EXT = {
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.webp': 'image/webp',
10
+ '.gif': 'image/gif',
11
+ };
12
+ export function isImagePath(path) {
13
+ return Boolean(MIME_BY_EXT[extname(path).toLowerCase()]);
14
+ }
15
+ export function loadImageAttachment(path, id) {
16
+ const abs = resolve(path.replace(/^["']|["']$/g, ''));
17
+ if (!existsSync(abs))
18
+ throw new Error(`Image not found: ${path}`);
19
+ const st = statSync(abs);
20
+ if (!st.isFile())
21
+ throw new Error(`Not a file: ${path}`);
22
+ const mime = MIME_BY_EXT[extname(abs).toLowerCase()];
23
+ if (!mime)
24
+ throw new Error('Supported image types: png, jpg, jpeg, webp, gif');
25
+ const maxBytes = 20 * 1024 * 1024;
26
+ if (st.size > maxBytes)
27
+ throw new Error(`Image is too large (${formatBytes(st.size)}). Max is ${formatBytes(maxBytes)}.`);
28
+ return {
29
+ id,
30
+ path: abs,
31
+ name: basename(abs),
32
+ mime,
33
+ bytes: st.size,
34
+ };
35
+ }
36
+ /**
37
+ * Load image from clipboard (cross-platform)
38
+ * Windows: PowerShell
39
+ * macOS: osascript (AppleScript)
40
+ * Linux: xclip (X11) or wl-paste (Wayland)
41
+ */
42
+ export function loadClipboardImageAttachment(id) {
43
+ ensureHome();
44
+ const dir = join(HOME_DIR, 'clipboard-images');
45
+ if (!existsSync(dir))
46
+ mkdirSync(dir, { recursive: true });
47
+ const outPath = join(dir, `clipboard-${Date.now()}.png`);
48
+ const platform = process.platform;
49
+ if (platform === 'win32') {
50
+ return loadClipboardImageWindows(outPath, id);
51
+ }
52
+ else if (platform === 'darwin') {
53
+ return loadClipboardImageMacOS(outPath, id);
54
+ }
55
+ else if (platform === 'linux') {
56
+ return loadClipboardImageLinux(outPath, id);
57
+ }
58
+ else {
59
+ throw new Error(`Clipboard image paste is not supported on platform: ${platform}`);
60
+ }
61
+ }
62
+ function loadClipboardImageWindows(outPath, id) {
63
+ const script = `
64
+ $ProgressPreference = 'SilentlyContinue'
65
+ Add-Type -AssemblyName System.Windows.Forms
66
+ Add-Type -AssemblyName System.Drawing
67
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
68
+ if ($null -eq $img) {
69
+ [Console]::Error.WriteLine('Clipboard does not contain an image.')
70
+ exit 2
71
+ }
72
+ $path = @'
73
+ ${outPath}
74
+ '@
75
+ $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
76
+ $img.Dispose()
77
+ Write-Output $path
78
+ `;
79
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
80
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Sta', '-NonInteractive', '-OutputFormat', 'Text', '-EncodedCommand', encoded], {
81
+ encoding: 'utf8',
82
+ windowsHide: true,
83
+ });
84
+ if (result.error)
85
+ throw new Error(`Could not read clipboard image: ${result.error.message}`);
86
+ if (result.status !== 0) {
87
+ const msg = cleanPowerShellMessage(result.stderr || result.stdout || 'Clipboard does not contain an image.');
88
+ throw new Error(msg);
89
+ }
90
+ return loadImageAttachment(outPath, id);
91
+ }
92
+ function loadClipboardImageMacOS(outPath, id) {
93
+ // Try using osascript (built-in, no dependencies)
94
+ const script = `
95
+ set theFile to POSIX file \"${outPath}\"
96
+ try
97
+ set imageData to the clipboard as «class PNGf»
98
+ set fileRef to open for access theFile with write permission
99
+ set eof fileRef to 0
100
+ write imageData to fileRef
101
+ close access fileRef
102
+ return \"${outPath}\"
103
+ on error errMsg
104
+ try
105
+ close access theFile
106
+ end try
107
+ error \"Clipboard does not contain an image.\"
108
+ end try
109
+ `;
110
+ let result = spawnSync('osascript', ['-e', script], {
111
+ encoding: 'utf8',
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ });
114
+ if (result.status === 0 && existsSync(outPath)) {
115
+ return loadImageAttachment(outPath, id);
116
+ }
117
+ // Fallback: try pngpaste if available
118
+ result = spawnSync('pngpaste', [outPath], {
119
+ encoding: 'utf8',
120
+ stdio: ['ignore', 'pipe', 'pipe'],
121
+ });
122
+ if (result.status === 0 && existsSync(outPath)) {
123
+ return loadImageAttachment(outPath, id);
124
+ }
125
+ throw new Error('Clipboard does not contain an image. (Tip: Install pngpaste via: brew install pngpaste)');
126
+ }
127
+ function loadClipboardImageLinux(outPath, id) {
128
+ // Try xclip first (X11)
129
+ let result = spawnSync('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], {
130
+ encoding: 'utf8',
131
+ stdio: ['ignore', 'pipe', 'pipe'],
132
+ });
133
+ if (result.status === 0 && result.stdout) {
134
+ try {
135
+ writeFileSync(outPath, result.stdout, 'binary');
136
+ if (existsSync(outPath) && statSync(outPath).size > 0) {
137
+ return loadImageAttachment(outPath, id);
138
+ }
139
+ }
140
+ catch { }
141
+ }
142
+ // Try wl-paste for Wayland
143
+ result = spawnSync('wl-paste', ['--type', 'image/png'], {
144
+ encoding: 'utf8',
145
+ stdio: ['ignore', 'pipe', 'pipe'],
146
+ });
147
+ if (result.status === 0 && result.stdout) {
148
+ try {
149
+ writeFileSync(outPath, result.stdout, 'binary');
150
+ if (existsSync(outPath) && statSync(outPath).size > 0) {
151
+ return loadImageAttachment(outPath, id);
152
+ }
153
+ }
154
+ catch { }
155
+ }
156
+ throw new Error('Clipboard does not contain an image. (Tip: Install xclip or wl-clipboard)');
157
+ }
158
+ function cleanPowerShellMessage(message) {
159
+ const text = message.trim();
160
+ if (text.includes('Clipboard does not contain an image.'))
161
+ return 'Clipboard does not contain an image.';
162
+ return text.replace(/#< CLIXML[\s\S]*/g, '').trim() || 'Could not read clipboard image.';
163
+ }
164
+ /**
165
+ * Check if clipboard contains an image (non-throwing)
166
+ * Returns true if clipboard has an image, false otherwise
167
+ */
168
+ export function hasClipboardImage() {
169
+ try {
170
+ const platform = process.platform;
171
+ if (platform === 'win32') {
172
+ const script = `
173
+ $ProgressPreference = 'SilentlyContinue'
174
+ Add-Type -AssemblyName System.Windows.Forms
175
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
176
+ if ($null -eq $img) { exit 1 }
177
+ exit 0
178
+ `;
179
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
180
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Sta', '-NonInteractive', '-EncodedCommand', encoded], {
181
+ windowsHide: true,
182
+ timeout: 1000,
183
+ });
184
+ return result.status === 0;
185
+ }
186
+ else if (platform === 'darwin') {
187
+ // Quick check using osascript
188
+ const script = 'try\nthe clipboard as «class PNGf»\nreturn true\non error\nreturn false\nend try';
189
+ const result = spawnSync('osascript', ['-e', script], {
190
+ encoding: 'utf8',
191
+ timeout: 1000,
192
+ });
193
+ return result.stdout?.trim() === 'true';
194
+ }
195
+ else if (platform === 'linux') {
196
+ // Check xclip
197
+ let result = spawnSync('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], {
198
+ encoding: 'utf8',
199
+ timeout: 1000,
200
+ });
201
+ if (result.status === 0 && result.stdout?.includes('image/png')) {
202
+ return true;
203
+ }
204
+ // Check wl-paste
205
+ result = spawnSync('wl-paste', ['--list-types'], {
206
+ encoding: 'utf8',
207
+ timeout: 1000,
208
+ });
209
+ return result.status === 0 && result.stdout?.includes('image/png');
210
+ }
211
+ }
212
+ catch { }
213
+ return false;
214
+ }
215
+ export function imageToContentPart(image) {
216
+ const data = readFileSync(image.path).toString('base64');
217
+ return {
218
+ type: 'image_url',
219
+ image_url: {
220
+ url: `data:${image.mime};base64,${data}`,
221
+ },
222
+ };
223
+ }
224
+ export function buildUserContent(text, images) {
225
+ if (!images.length)
226
+ return text;
227
+ return [
228
+ { type: 'text', text },
229
+ ...images.map(imageToContentPart),
230
+ ];
231
+ }
232
+ export function formatBytes(bytes) {
233
+ if (bytes < 1024)
234
+ return `${bytes} B`;
235
+ const kb = bytes / 1024;
236
+ if (kb < 1024)
237
+ return `${kb.toFixed(1)} KB`;
238
+ return `${(kb / 1024).toFixed(1)} MB`;
239
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Device-code login against the hosted ikie API.
3
+ * 1. POST /api/cli/device → { device_code, user_code, verification_uri }
4
+ * 2. user approves in browser
5
+ * 3. poll POST /api/cli/token → { api_key }
6
+ */
7
+ export declare function login(): Promise<void>;
8
+ export declare function logout(): void;
package/dist/auth.js ADDED
@@ -0,0 +1,89 @@
1
+ import { IKIE_HOST, saveLogin, clearLogin } from './config.js';
2
+ import { c, successLine, errorLine, infoLine } from './theme.js';
3
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
+ /** Try to open a URL in the user's default browser (best-effort). */
5
+ async function openBrowser(url) {
6
+ try {
7
+ const { exec } = await import('child_process');
8
+ const platform = process.platform;
9
+ const cmd = platform === 'darwin' ? `open "${url}"`
10
+ : platform === 'win32' ? `start "" "${url}"`
11
+ : `xdg-open "${url}"`;
12
+ exec(cmd, () => { });
13
+ }
14
+ catch {
15
+ /* ignore — we print the URL anyway */
16
+ }
17
+ }
18
+ /**
19
+ * Device-code login against the hosted ikie API.
20
+ * 1. POST /api/cli/device → { device_code, user_code, verification_uri }
21
+ * 2. user approves in browser
22
+ * 3. poll POST /api/cli/token → { api_key }
23
+ */
24
+ export async function login() {
25
+ let start;
26
+ try {
27
+ const res = await fetch(`${IKIE_HOST}/api/cli/device`, { method: 'POST' });
28
+ if (!res.ok)
29
+ throw new Error(`HTTP ${res.status}`);
30
+ start = (await res.json());
31
+ }
32
+ catch (e) {
33
+ console.error(errorLine(`Couldn't reach ikie at ${IKIE_HOST}`));
34
+ console.error(c.muted(` ${e instanceof Error ? e.message : String(e)}`));
35
+ console.error(c.muted(' Set IKIE_HOST if your server is elsewhere.'));
36
+ process.exit(1);
37
+ }
38
+ console.log();
39
+ console.log(infoLine('Sign in to ikie'));
40
+ console.log();
41
+ console.log(` ${c.muted('1.')} Open this URL in your browser:`);
42
+ console.log(` ${c.info(start.verification_uri_complete)}`);
43
+ console.log();
44
+ console.log(` ${c.muted('2.')} Confirm this code:`);
45
+ console.log(` ${c.primary.bold(start.user_code)}`);
46
+ console.log();
47
+ console.log(c.muted(' Waiting for approval…'));
48
+ await openBrowser(start.verification_uri_complete);
49
+ const intervalMs = Math.max(2, start.interval ?? 3) * 1000;
50
+ const deadline = Date.now() + (start.expires_in ?? 600) * 1000;
51
+ while (Date.now() < deadline) {
52
+ await sleep(intervalMs);
53
+ try {
54
+ const res = await fetch(`${IKIE_HOST}/api/cli/token`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({ device_code: start.device_code }),
58
+ });
59
+ if (res.status === 202)
60
+ continue; // still pending
61
+ const data = (await res.json());
62
+ if (res.ok && data.api_key) {
63
+ saveLogin(data.api_key);
64
+ console.log();
65
+ console.log(successLine('Logged in. ikie is now connected to your account.'));
66
+ console.log(c.muted(' Your usage and credit are tracked in the dashboard.'));
67
+ console.log();
68
+ return;
69
+ }
70
+ if (data.status === 'denied') {
71
+ console.error(errorLine('Login was denied.'));
72
+ process.exit(1);
73
+ }
74
+ if (data.status === 'expired') {
75
+ console.error(errorLine('This login request expired. Run `ikie login` again.'));
76
+ process.exit(1);
77
+ }
78
+ }
79
+ catch {
80
+ /* transient network hiccup — keep polling */
81
+ }
82
+ }
83
+ console.error(errorLine('Login timed out. Run `ikie login` again.'));
84
+ process.exit(1);
85
+ }
86
+ export function logout() {
87
+ clearLogin();
88
+ console.log(successLine('Logged out. ikie will use your local API key / Fireworks again.'));
89
+ }
@@ -0,0 +1,41 @@
1
+ export declare const HOME_DIR: string;
2
+ export declare const CONFIG_FILE: string;
3
+ export declare const GLOBAL_MEMORY_FILE: string;
4
+ export declare const SESSIONS_DIR: string;
5
+ export declare const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
6
+ export declare const DEFAULT_MODEL = "accounts/fireworks/models/kimi-k2p7-code";
7
+ /**
8
+ * The hosted ikie API (masks the upstream provider behind ik_live_ keys).
9
+ * Override with IKIE_HOST env var, e.g. for local dev or a custom domain.
10
+ */
11
+ export declare const IKIE_HOST: string;
12
+ export declare const IKIE_API_BASE: string;
13
+ export interface IkieConfig {
14
+ model: string;
15
+ maxTokens: number;
16
+ autoApprove: boolean;
17
+ apiKey?: string;
18
+ baseURL?: string;
19
+ /** Set when logged into the hosted ikie account via `ikie login`. */
20
+ account?: {
21
+ apiKey: string;
22
+ loggedInAt: number;
23
+ };
24
+ temperature: number;
25
+ topP: number;
26
+ topK: number;
27
+ presencePenalty: number;
28
+ frequencyPenalty: number;
29
+ theme: string;
30
+ requestsPerMinute: number;
31
+ }
32
+ export declare function ensureHome(): void;
33
+ export declare function loadConfig(): IkieConfig;
34
+ export declare function saveConfig(patch: Partial<IkieConfig>): void;
35
+ export declare function getApiKey(cfg: IkieConfig): string | undefined;
36
+ /** True when signed into a hosted ikie account. */
37
+ export declare function isLoggedIn(cfg: IkieConfig): boolean;
38
+ /** Persist the hosted account login (ik_live_ key from `ikie login`). */
39
+ export declare function saveLogin(apiKey: string): void;
40
+ /** Clear the hosted account login and revert to the default base URL. */
41
+ export declare function clearLogin(): void;
package/dist/config.js ADDED
@@ -0,0 +1,69 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
4
+ export const HOME_DIR = join(homedir(), '.ikie');
5
+ export const CONFIG_FILE = join(HOME_DIR, 'config.json');
6
+ export const GLOBAL_MEMORY_FILE = join(HOME_DIR, 'memory.md');
7
+ export const SESSIONS_DIR = join(HOME_DIR, 'sessions');
8
+ export const FIREWORKS_BASE_URL = 'https://api.fireworks.ai/inference/v1';
9
+ export const DEFAULT_MODEL = 'accounts/fireworks/models/kimi-k2p7-code';
10
+ /**
11
+ * The hosted ikie API (masks the upstream provider behind ik_live_ keys).
12
+ * Override with IKIE_HOST env var, e.g. for local dev or a custom domain.
13
+ */
14
+ export const IKIE_HOST = process.env.IKIE_HOST ?? 'http://140.245.26.210:3000';
15
+ export const IKIE_API_BASE = `${IKIE_HOST}/api/v1`;
16
+ const DEFAULTS = {
17
+ model: DEFAULT_MODEL,
18
+ maxTokens: 32768,
19
+ autoApprove: false,
20
+ baseURL: FIREWORKS_BASE_URL,
21
+ temperature: 0.6,
22
+ topP: 0.95,
23
+ topK: 40,
24
+ presencePenalty: 0,
25
+ frequencyPenalty: 0,
26
+ theme: 'nebula',
27
+ requestsPerMinute: 10,
28
+ };
29
+ export function ensureHome() {
30
+ for (const dir of [HOME_DIR, SESSIONS_DIR]) {
31
+ if (!existsSync(dir))
32
+ mkdirSync(dir, { recursive: true });
33
+ }
34
+ }
35
+ export function loadConfig() {
36
+ ensureHome();
37
+ try {
38
+ if (existsSync(CONFIG_FILE)) {
39
+ return { ...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) };
40
+ }
41
+ }
42
+ catch { }
43
+ return { ...DEFAULTS };
44
+ }
45
+ export function saveConfig(patch) {
46
+ const current = loadConfig();
47
+ writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...patch }, null, 2));
48
+ }
49
+ export function getApiKey(cfg) {
50
+ return (process.env.FIREWORKS_API_KEY ??
51
+ process.env.IKIE_API_KEY ??
52
+ cfg.account?.apiKey ??
53
+ cfg.apiKey);
54
+ }
55
+ /** True when signed into a hosted ikie account. */
56
+ export function isLoggedIn(cfg) {
57
+ return Boolean(cfg.account?.apiKey);
58
+ }
59
+ /** Persist the hosted account login (ik_live_ key from `ikie login`). */
60
+ export function saveLogin(apiKey) {
61
+ saveConfig({ account: { apiKey, loggedInAt: Date.now() }, baseURL: IKIE_API_BASE });
62
+ }
63
+ /** Clear the hosted account login and revert to the default base URL. */
64
+ export function clearLogin() {
65
+ const current = loadConfig();
66
+ delete current.account;
67
+ current.baseURL = FIREWORKS_BASE_URL;
68
+ writeFileSync(CONFIG_FILE, JSON.stringify(current, null, 2));
69
+ }
@@ -0,0 +1,13 @@
1
+ export interface ProjectContext {
2
+ cwd: string;
3
+ name: string;
4
+ type: string[];
5
+ description: string;
6
+ gitBranch?: string;
7
+ recentCommits?: string;
8
+ readme?: string;
9
+ manifest?: string;
10
+ instructions?: string;
11
+ }
12
+ export declare function detectProjectContext(): ProjectContext;
13
+ export declare function formatContextForPrompt(ctx: ProjectContext): string;
@@ -0,0 +1,126 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join, basename } from 'path';
3
+ import { execSync } from 'child_process';
4
+ const MANIFEST_FILES = {
5
+ 'package.json': 'Node.js',
6
+ 'pyproject.toml': 'Python',
7
+ 'setup.py': 'Python',
8
+ 'requirements.txt': 'Python',
9
+ 'Cargo.toml': 'Rust',
10
+ 'go.mod': 'Go',
11
+ 'pom.xml': 'Java/Maven',
12
+ 'build.gradle': 'Java/Gradle',
13
+ 'Gemfile': 'Ruby',
14
+ 'composer.json': 'PHP',
15
+ 'CMakeLists.txt': 'C/C++',
16
+ };
17
+ const INSTRUCTION_FILES = ['CLAUDE.md', 'IKIE.md', '.cursorrules', 'AGENTS.md'];
18
+ const README_FILES = ['README.md', 'README.txt', 'README.rst', 'readme.md'];
19
+ function tryRead(path, maxChars = 4000) {
20
+ try {
21
+ if (existsSync(path)) {
22
+ const content = readFileSync(path, 'utf-8');
23
+ return content.length > maxChars ? content.slice(0, maxChars) + '\n...(truncated)' : content;
24
+ }
25
+ }
26
+ catch { }
27
+ return undefined;
28
+ }
29
+ function gitBranch(cwd) {
30
+ try {
31
+ return execSync('git branch --show-current', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
32
+ .toString()
33
+ .trim();
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function gitLog(cwd) {
40
+ try {
41
+ return execSync('git log --oneline -8 2>/dev/null', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
42
+ .toString()
43
+ .trim();
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ function detectTypes(cwd) {
50
+ const types = [];
51
+ for (const [file, type] of Object.entries(MANIFEST_FILES)) {
52
+ if (existsSync(join(cwd, file)) && !types.includes(type)) {
53
+ types.push(type);
54
+ }
55
+ }
56
+ // Detect TypeScript
57
+ if (existsSync(join(cwd, 'tsconfig.json')))
58
+ types.push('TypeScript');
59
+ // Detect Docker
60
+ if (existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'docker-compose.yml'))) {
61
+ types.push('Docker');
62
+ }
63
+ return types.length ? types : ['Unknown'];
64
+ }
65
+ function readManifest(cwd) {
66
+ // Try package.json first (most common), trim it
67
+ const pkgPath = join(cwd, 'package.json');
68
+ if (existsSync(pkgPath)) {
69
+ try {
70
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
71
+ const { name, version, description, scripts, dependencies, devDependencies } = pkg;
72
+ const depCount = Object.keys(dependencies ?? {}).length;
73
+ const devDepCount = Object.keys(devDependencies ?? {}).length;
74
+ return JSON.stringify({ name, version, description, scripts, depCount, devDepCount }, null, 2);
75
+ }
76
+ catch { }
77
+ }
78
+ for (const file of ['pyproject.toml', 'Cargo.toml', 'go.mod']) {
79
+ const content = tryRead(join(cwd, file), 2000);
80
+ if (content)
81
+ return content;
82
+ }
83
+ return undefined;
84
+ }
85
+ export function detectProjectContext() {
86
+ const cwd = process.cwd();
87
+ const name = basename(cwd);
88
+ const types = detectTypes(cwd);
89
+ let readme;
90
+ for (const f of README_FILES) {
91
+ readme = tryRead(join(cwd, f), 3000);
92
+ if (readme)
93
+ break;
94
+ }
95
+ let instructions;
96
+ for (const f of INSTRUCTION_FILES) {
97
+ instructions = tryRead(join(cwd, f), 5000);
98
+ if (instructions)
99
+ break;
100
+ }
101
+ const branch = gitBranch(cwd);
102
+ const recentCommits = gitLog(cwd);
103
+ const manifest = readManifest(cwd);
104
+ const description = readme
105
+ ? readme.split('\n').filter(l => l.trim() && !l.startsWith('#')).slice(0, 3).join(' ').slice(0, 300)
106
+ : `A ${types.join('/')} project`;
107
+ return { cwd, name, type: types, description, gitBranch: branch, recentCommits, readme: readme?.slice(0, 1500), manifest, instructions };
108
+ }
109
+ export function formatContextForPrompt(ctx) {
110
+ const parts = [
111
+ `**Project:** ${ctx.name}`,
112
+ `**Type:** ${ctx.type.join(', ')}`,
113
+ `**Directory:** ${ctx.cwd}`,
114
+ ];
115
+ if (ctx.description)
116
+ parts.push(`**Description:** ${ctx.description}`);
117
+ if (ctx.gitBranch)
118
+ parts.push(`**Git branch:** ${ctx.gitBranch}`);
119
+ if (ctx.recentCommits)
120
+ parts.push(`**Recent commits:**\n${ctx.recentCommits}`);
121
+ if (ctx.manifest)
122
+ parts.push(`**Manifest:**\n\`\`\`\n${ctx.manifest}\n\`\`\``);
123
+ if (ctx.instructions)
124
+ parts.push(`**Project instructions (CLAUDE.md / IKIE.md):**\n${ctx.instructions}`);
125
+ return parts.join('\n');
126
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};