ikie-cli 0.1.41 → 0.1.43
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/dist/agent.d.ts +5 -1
- package/dist/agent.js +106 -12
- package/dist/context.d.ts +1 -1
- package/dist/context.js +2 -2
- package/dist/index.js +6 -5
- package/dist/memory.d.ts +5 -5
- package/dist/memory.js +24 -18
- package/dist/repl.js +39 -14
- package/dist/theme.d.ts +54 -0
- package/dist/theme.js +136 -3
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +77 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface AgentTurnStats {
|
|
|
20
20
|
toolCalls: number;
|
|
21
21
|
filesChanged: number;
|
|
22
22
|
}
|
|
23
|
-
export
|
|
23
|
+
export { estimateTokens } from './utils.js';
|
|
24
24
|
/**
|
|
25
25
|
* Extract the most useful upstream error message from an OpenAI SDK error.
|
|
26
26
|
* The SDK wraps provider responses; we prefer the nested error.message if it exists.
|
|
@@ -79,6 +79,8 @@ export declare class Agent {
|
|
|
79
79
|
private mode;
|
|
80
80
|
private planSteps;
|
|
81
81
|
private planStepIndex;
|
|
82
|
+
private thinkingStartTime;
|
|
83
|
+
private lastThinkingDuration;
|
|
82
84
|
constructor(client: OpenAI, config: IkieConfig, systemPrompt: string);
|
|
83
85
|
write(text: string): void;
|
|
84
86
|
clearConversation(): void;
|
|
@@ -119,4 +121,6 @@ export declare class Agent {
|
|
|
119
121
|
private checkPermission;
|
|
120
122
|
}
|
|
121
123
|
export declare const PLAN_MODE_ADDENDUM = "\n\n## PLAN MODE (read-only)\nYou are currently in **plan mode**. You have ONLY read-only tools (read_file,\nlist_dir, search_files, grep, fetch_url, web_search, use_skill, ask_user). You CANNOT write files, edit\nfiles, run shell commands, or change anything \u2014 those tools are unavailable and any\nattempt will be blocked.\n\nYour job is to investigate and produce a clear, actionable plan:\n- Explore the relevant files and understand the current state before proposing anything.\n- Do NOT describe changes as if you already made them. You haven't.\n- End your response with a concise plan: a short **## Plan** heading followed by\n numbered steps. Name the specific files to change and what to change in each. Call\n out risks, assumptions, or open questions.\n- Keep it tight \u2014 enough to execute from, not an essay.\n\nAfter you present the plan, the user will be asked whether to execute it. On approval,\nyou'll be switched to agent mode and asked to carry out exactly this plan.";
|
|
124
|
+
export declare function isLightweightModel(model: string): boolean;
|
|
125
|
+
export declare function buildLightweightSystemPrompt(prompt: string): string;
|
|
122
126
|
export declare function buildSystemPrompt(projectContext: string, memoryContext: string, skillsCatalog?: string): string;
|
package/dist/agent.js
CHANGED
|
@@ -2,16 +2,16 @@ import chalk from 'chalk';
|
|
|
2
2
|
import * as readline from 'node:readline';
|
|
3
3
|
import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool, isRestrictedPath, getMcpToolDefs } from './tools.js';
|
|
4
4
|
import { IKIE_PORT } from './config.js';
|
|
5
|
+
import { estimateTokens } from './utils.js';
|
|
5
6
|
import { renderMarkdown, extractThinkTags } from './renderer.js';
|
|
6
7
|
import { getSkill, renderSkill, mapAllowedTools } from './skills.js';
|
|
7
|
-
import { c, toolLine, toolSuccessLine, toolErrorLine, toolOutputBlock, toolDiffBlock, InlineSpinner, CH, toolMeta } from './theme.js';
|
|
8
|
+
import { c, toolLine, toolSuccessLine, toolErrorLine, toolOutputBlock, toolDiffBlock, InlineSpinner, CH, toolMeta, thinkingTime, actionLine, planStep, planTitle } from './theme.js';
|
|
8
9
|
/** Default per-turn step budget — guards against runaway tool loops. */
|
|
9
10
|
export const DEFAULT_MAX_STEPS = 60;
|
|
10
11
|
/** Result synthesized for a tool call that never produced one (cancel/crash). */
|
|
11
12
|
export const INTERRUPTED_TOOL_RESULT = 'Interrupted: this tool did not run to completion (the turn was cancelled).';
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
}
|
|
13
|
+
// Re-export so existing callers that import from agent.ts still work.
|
|
14
|
+
export { estimateTokens } from './utils.js';
|
|
15
15
|
/**
|
|
16
16
|
* Extract the most useful upstream error message from an OpenAI SDK error.
|
|
17
17
|
* The SDK wraps provider responses; we prefer the nested error.message if it exists.
|
|
@@ -251,6 +251,8 @@ export class Agent {
|
|
|
251
251
|
mode = 'agent';
|
|
252
252
|
planSteps = [];
|
|
253
253
|
planStepIndex = 0;
|
|
254
|
+
thinkingStartTime = 0;
|
|
255
|
+
lastThinkingDuration = 0;
|
|
254
256
|
constructor(client, config, systemPrompt) {
|
|
255
257
|
this.client = client;
|
|
256
258
|
this.config = config;
|
|
@@ -337,9 +339,9 @@ export class Agent {
|
|
|
337
339
|
this.planSteps = steps;
|
|
338
340
|
this.planStepIndex = 0;
|
|
339
341
|
if (steps.length > 0) {
|
|
340
|
-
this.write(`\n
|
|
342
|
+
this.write(`\n${this.indent}${planTitle(steps.length)}\n\n`);
|
|
341
343
|
for (let i = 0; i < steps.length; i++) {
|
|
342
|
-
this.write(
|
|
344
|
+
this.write(`${this.indent}${planStep(steps[i], false)}\n`);
|
|
343
345
|
}
|
|
344
346
|
this.write('\n');
|
|
345
347
|
}
|
|
@@ -347,7 +349,7 @@ export class Agent {
|
|
|
347
349
|
advancePlanStep() {
|
|
348
350
|
if (this.planStepIndex < this.planSteps.length) {
|
|
349
351
|
const step = this.planSteps[this.planStepIndex];
|
|
350
|
-
this.write(
|
|
352
|
+
this.write(`${this.indent}${planStep(step, true)}\n`);
|
|
351
353
|
this.planStepIndex++;
|
|
352
354
|
}
|
|
353
355
|
}
|
|
@@ -476,7 +478,15 @@ export class Agent {
|
|
|
476
478
|
if (remaining.length === 1) {
|
|
477
479
|
if (this.activeTurnStats)
|
|
478
480
|
this.activeTurnStats.toolCalls++;
|
|
479
|
-
|
|
481
|
+
// Use clean action line for file edits
|
|
482
|
+
if (remaining[0].name === 'edit_file' || remaining[0].name === 'write_file') {
|
|
483
|
+
const filePath = String(inputs[0].path || '');
|
|
484
|
+
const action = remaining[0].name === 'edit_file' ? 'Edit' : 'Write';
|
|
485
|
+
this.write(`\n${this.indent}${actionLine(action, filePath)}\n`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.write(`\n${this.indent}${toolLine(remaining[0].name, formatToolArgs(remaining[0].name, inputs[0])).trimStart()}\n`);
|
|
489
|
+
}
|
|
480
490
|
const result = await this.handleToolCall(remaining[0].name, remaining[0].id, inputs[0], opts);
|
|
481
491
|
pushResult(remaining[0].id, result);
|
|
482
492
|
}
|
|
@@ -499,9 +509,19 @@ export class Agent {
|
|
|
499
509
|
const groupSpinner = new InlineSpinner(`${toolPhaseLabel(remaining[0].name)} (${remaining.length} operations)`, t0, opts.signal);
|
|
500
510
|
groupSpinner.start();
|
|
501
511
|
const results = new Map();
|
|
512
|
+
// Fail-fast flag for mutating tool batches: if a write/edit fails
|
|
513
|
+
// with a critical error, skip subsequent tools in this batch to
|
|
514
|
+
// prevent writing broken/incomplete code from a half-applied change.
|
|
515
|
+
const isMutatingBatch = remaining[0].name === 'edit_file' || remaining[0].name === 'write_file';
|
|
516
|
+
let batchAborted = false;
|
|
502
517
|
for (let i = 0; i < remaining.length; i++) {
|
|
503
518
|
if (opts.signal?.aborted)
|
|
504
519
|
break;
|
|
520
|
+
// Fail-fast: skip remaining mutating ops after a critical failure.
|
|
521
|
+
if (batchAborted) {
|
|
522
|
+
results.set(i, 'Skipped: a previous tool in this batch failed — batch aborted to prevent inconsistent state.');
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
505
525
|
const tc = remaining[i];
|
|
506
526
|
try {
|
|
507
527
|
if (tc.name === 'read_file' && isRestrictedPath(String(inputs[i].path ?? ''))) {
|
|
@@ -514,13 +534,19 @@ export class Agent {
|
|
|
514
534
|
const result = tc.name === 'use_skill'
|
|
515
535
|
? await this.handleToolCall(tc.name, tc.id, inputs[i], opts)
|
|
516
536
|
: await executeTool(tc.name, inputs[i], opts.signal);
|
|
517
|
-
if (result.startsWith('Error'))
|
|
537
|
+
if (result.startsWith('Error')) {
|
|
518
538
|
errors++;
|
|
539
|
+
// Trigger fail-fast only for mutating batches on critical errors.
|
|
540
|
+
if (isMutatingBatch)
|
|
541
|
+
batchAborted = true;
|
|
542
|
+
}
|
|
519
543
|
this.recordChangedFile(tc.name, inputs[i], result);
|
|
520
544
|
results.set(i, result);
|
|
521
545
|
}
|
|
522
546
|
catch (err) {
|
|
523
547
|
errors++;
|
|
548
|
+
if (isMutatingBatch)
|
|
549
|
+
batchAborted = true;
|
|
524
550
|
results.set(i, `Tool error: ${err}`);
|
|
525
551
|
}
|
|
526
552
|
}
|
|
@@ -597,9 +623,20 @@ export class Agent {
|
|
|
597
623
|
if (this.activeTurnStats)
|
|
598
624
|
this.activeTurnStats.modelCalls++;
|
|
599
625
|
await this.throttleModelRequest();
|
|
626
|
+
// Start thinking timer
|
|
627
|
+
this.thinkingStartTime = Date.now();
|
|
600
628
|
const state = { emitted: false };
|
|
601
629
|
try {
|
|
602
|
-
|
|
630
|
+
const result = await this.callModelStreaming(opts, state);
|
|
631
|
+
// Record thinking duration
|
|
632
|
+
this.lastThinkingDuration = (Date.now() - this.thinkingStartTime) / 1000;
|
|
633
|
+
// Display thinking time if it was significant (>0.5s)
|
|
634
|
+
if (this.lastThinkingDuration > 0.5) {
|
|
635
|
+
this.write(`
|
|
636
|
+
${this.indent}${thinkingTime(this.lastThinkingDuration)}
|
|
637
|
+
`);
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
603
640
|
}
|
|
604
641
|
catch (err) {
|
|
605
642
|
if (opts.signal?.aborted)
|
|
@@ -609,7 +646,14 @@ export class Agent {
|
|
|
609
646
|
// and double-bill — surface the error instead.
|
|
610
647
|
if (state.emitted)
|
|
611
648
|
throw err;
|
|
612
|
-
|
|
649
|
+
const result = await this.callModelNonStreaming(opts);
|
|
650
|
+
this.lastThinkingDuration = (Date.now() - this.thinkingStartTime) / 1000;
|
|
651
|
+
if (this.lastThinkingDuration > 0.5) {
|
|
652
|
+
this.write(`
|
|
653
|
+
${this.indent}${thinkingTime(this.lastThinkingDuration)}
|
|
654
|
+
`);
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
613
657
|
}
|
|
614
658
|
}
|
|
615
659
|
buildParams() {
|
|
@@ -622,7 +666,11 @@ export class Agent {
|
|
|
622
666
|
// Always include switch_mode so the agent can request a mode change.
|
|
623
667
|
const switchModeTool = TOOL_DEFS.find(t => t.function.name === 'switch_mode');
|
|
624
668
|
// Plan mode: only offer read-only tools, and steer toward proposing a plan.
|
|
625
|
-
|
|
669
|
+
// For lightweight/local models, use a condensed system prompt to preserve
|
|
670
|
+
// context window budget; full prompt is used for large capable models.
|
|
671
|
+
let systemContent = isLightweightModel(this.config.model)
|
|
672
|
+
? buildLightweightSystemPrompt(this.systemPrompt)
|
|
673
|
+
: this.systemPrompt;
|
|
626
674
|
if (this.mode === 'plan') {
|
|
627
675
|
tools = tools.filter(t => PLAN_TOOLS.has(t.function.name) || t.function.name === 'switch_mode');
|
|
628
676
|
if (switchModeTool && !tools.includes(switchModeTool))
|
|
@@ -1090,6 +1138,52 @@ Your job is to investigate and produce a clear, actionable plan:
|
|
|
1090
1138
|
|
|
1091
1139
|
After you present the plan, the user will be asked whether to execute it. On approval,
|
|
1092
1140
|
you'll be switched to agent mode and asked to carry out exactly this plan.`;
|
|
1141
|
+
export function isLightweightModel(model) {
|
|
1142
|
+
const m = model.toLowerCase();
|
|
1143
|
+
return (m.includes('haiku') ||
|
|
1144
|
+
m.includes('flash') ||
|
|
1145
|
+
m.includes('mini') ||
|
|
1146
|
+
m.includes('llama') ||
|
|
1147
|
+
m.includes('gemma') ||
|
|
1148
|
+
m.includes('phi') ||
|
|
1149
|
+
m.includes('qwen') ||
|
|
1150
|
+
m.includes('local'));
|
|
1151
|
+
}
|
|
1152
|
+
export function buildLightweightSystemPrompt(prompt) {
|
|
1153
|
+
const lines = prompt.split('\n');
|
|
1154
|
+
const result = [];
|
|
1155
|
+
let skipping = false;
|
|
1156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1157
|
+
const line = lines[i];
|
|
1158
|
+
// Check if we should start/stop skipping a verbose section
|
|
1159
|
+
if (line.startsWith('## Response Formatting') ||
|
|
1160
|
+
line.startsWith('## Modern Design') ||
|
|
1161
|
+
line.startsWith('## Tool-Use Discipline') ||
|
|
1162
|
+
line.startsWith('## Tools Available') ||
|
|
1163
|
+
line.startsWith('## MCP (Model Context Protocol) System')) {
|
|
1164
|
+
skipping = true;
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
if (line.startsWith('## Identity') ||
|
|
1168
|
+
line.startsWith('## Confidentiality') ||
|
|
1169
|
+
line.startsWith('## Working Style') ||
|
|
1170
|
+
line.startsWith('## Engineering Principles') ||
|
|
1171
|
+
line.startsWith('## Approach') ||
|
|
1172
|
+
line.startsWith('## Project Context') ||
|
|
1173
|
+
line.startsWith('## Memory') ||
|
|
1174
|
+
line.startsWith('## Available Skills')) {
|
|
1175
|
+
skipping = false;
|
|
1176
|
+
}
|
|
1177
|
+
if (!skipping) {
|
|
1178
|
+
result.push(line);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// Inject a condensed response formatting instruction at the top
|
|
1182
|
+
const corePrompt = result.join('\n');
|
|
1183
|
+
return `You are Ikie, an elite terminal coding assistant. Be direct, concise, and pragmatic. Use markdown in responses.
|
|
1184
|
+
|
|
1185
|
+
${corePrompt}`;
|
|
1186
|
+
}
|
|
1093
1187
|
export function buildSystemPrompt(projectContext, memoryContext, skillsCatalog = '') {
|
|
1094
1188
|
const parts = [
|
|
1095
1189
|
`You are Ikie, an elite agentic software engineer running in the terminal.
|
package/dist/context.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export interface ProjectContext {
|
|
|
9
9
|
manifest?: string;
|
|
10
10
|
instructions?: string;
|
|
11
11
|
}
|
|
12
|
-
export declare function detectProjectContext(): ProjectContext;
|
|
12
|
+
export declare function detectProjectContext(projectRoot?: string): ProjectContext;
|
|
13
13
|
export declare function formatContextForPrompt(ctx: ProjectContext): string;
|
package/dist/context.js
CHANGED
|
@@ -82,8 +82,8 @@ function readManifest(cwd) {
|
|
|
82
82
|
}
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
|
-
export function detectProjectContext() {
|
|
86
|
-
const cwd = process.cwd();
|
|
85
|
+
export function detectProjectContext(projectRoot) {
|
|
86
|
+
const cwd = projectRoot || process.cwd();
|
|
87
87
|
const name = basename(cwd);
|
|
88
88
|
const types = detectTypes(cwd);
|
|
89
89
|
let readme;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import minimist from 'minimist';
|
|
|
5
5
|
import { loadConfig, getApiKey, IKIE_API_BASE, IKIE_HOST, DEFAULT_MODEL, hasExplicitModel } from './config.js';
|
|
6
6
|
import { detectProjectContext, formatContextForPrompt } from './context.js';
|
|
7
7
|
import { loadAllMemory, formatMemoryForPrompt } from './memory.js';
|
|
8
|
+
import { findProjectRoot } from './utils.js';
|
|
8
9
|
import { discoverSkills, formatSkillsForPrompt } from './skills.js';
|
|
9
10
|
import { buildSystemPrompt, Agent } from './agent.js';
|
|
10
11
|
import { startREPL } from './repl.js';
|
|
@@ -203,11 +204,12 @@ ${errorLine('Not signed in.')}
|
|
|
203
204
|
baseURL: config.baseURL ?? IKIE_API_BASE,
|
|
204
205
|
timeout: 60000, // 60 seconds timeout to prevent indefinite hangs
|
|
205
206
|
});
|
|
206
|
-
const
|
|
207
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
208
|
+
const projectCtx = detectProjectContext(projectRoot);
|
|
207
209
|
const projectContextStr = formatContextForPrompt(projectCtx);
|
|
208
|
-
const memory = loadAllMemory();
|
|
210
|
+
const memory = loadAllMemory(projectRoot);
|
|
209
211
|
const memoryStr = formatMemoryForPrompt(memory);
|
|
210
|
-
const skills = discoverSkills();
|
|
212
|
+
const skills = discoverSkills(projectRoot);
|
|
211
213
|
const skillsStr = formatSkillsForPrompt(skills);
|
|
212
214
|
const systemPrompt = buildSystemPrompt(projectContextStr, memoryStr, skillsStr);
|
|
213
215
|
if (argv.verbose) {
|
|
@@ -219,9 +221,8 @@ ${errorLine('Not signed in.')}
|
|
|
219
221
|
}
|
|
220
222
|
const agent = new Agent(client, config, systemPrompt);
|
|
221
223
|
// Connect configured MCP servers before the first turn.
|
|
222
|
-
const cwd = process.cwd();
|
|
223
224
|
try {
|
|
224
|
-
await getMcpManager().connectAll(
|
|
225
|
+
await getMcpManager().connectAll(projectRoot);
|
|
225
226
|
}
|
|
226
227
|
catch {
|
|
227
228
|
// Failures are recorded per-server; don't crash the REPL.
|
package/dist/memory.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export declare function loadGlobalMemory(): string;
|
|
2
|
-
export declare function loadProjectMemory(): string;
|
|
2
|
+
export declare function loadProjectMemory(projectRoot?: string): string;
|
|
3
3
|
export declare function saveGlobalMemory(content: string): void;
|
|
4
|
-
export declare function saveProjectMemory(content: string): void;
|
|
4
|
+
export declare function saveProjectMemory(content: string, projectRoot?: string): void;
|
|
5
5
|
export declare function appendGlobalMemory(note: string): void;
|
|
6
|
-
export declare function appendProjectMemory(note: string): void;
|
|
6
|
+
export declare function appendProjectMemory(note: string, projectRoot?: string): void;
|
|
7
7
|
export interface MemoryContext {
|
|
8
8
|
global: string;
|
|
9
9
|
project: string;
|
|
10
10
|
}
|
|
11
|
-
export declare function loadAllMemory(): MemoryContext;
|
|
11
|
+
export declare function loadAllMemory(projectRoot?: string): MemoryContext;
|
|
12
12
|
export declare function formatMemoryForPrompt(mem: MemoryContext): string;
|
|
13
|
-
export declare function getProjectMemoryPath(): string;
|
|
13
|
+
export declare function getProjectMemoryPath(projectRoot?: string): string;
|
|
14
14
|
export declare function getGlobalMemoryPath(): string;
|
package/dist/memory.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
import { GLOBAL_MEMORY_FILE } from './config.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { findProjectRoot } from './utils.js';
|
|
5
|
+
function getProjectMemoryFile(projectRoot) {
|
|
6
|
+
const root = projectRoot || findProjectRoot(process.cwd());
|
|
7
|
+
return join(root, '.ikie', 'memory.md');
|
|
8
|
+
}
|
|
9
|
+
function ensureProjectDir(projectRoot) {
|
|
10
|
+
const root = projectRoot || findProjectRoot(process.cwd());
|
|
11
|
+
const dir = join(root, '.ikie');
|
|
12
|
+
if (!existsSync(dir)) {
|
|
13
|
+
mkdirSync(dir, { recursive: true });
|
|
9
14
|
}
|
|
10
15
|
}
|
|
11
16
|
export function loadGlobalMemory() {
|
|
@@ -17,10 +22,11 @@ export function loadGlobalMemory() {
|
|
|
17
22
|
catch { }
|
|
18
23
|
return '';
|
|
19
24
|
}
|
|
20
|
-
export function loadProjectMemory() {
|
|
25
|
+
export function loadProjectMemory(projectRoot) {
|
|
21
26
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
const file = getProjectMemoryFile(projectRoot);
|
|
28
|
+
if (existsSync(file)) {
|
|
29
|
+
return readFileSync(file, 'utf-8').trim();
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
catch { }
|
|
@@ -29,9 +35,9 @@ export function loadProjectMemory() {
|
|
|
29
35
|
export function saveGlobalMemory(content) {
|
|
30
36
|
writeFileSync(GLOBAL_MEMORY_FILE, content.trim() + '\n');
|
|
31
37
|
}
|
|
32
|
-
export function saveProjectMemory(content) {
|
|
33
|
-
ensureProjectDir();
|
|
34
|
-
writeFileSync(
|
|
38
|
+
export function saveProjectMemory(content, projectRoot) {
|
|
39
|
+
ensureProjectDir(projectRoot);
|
|
40
|
+
writeFileSync(getProjectMemoryFile(projectRoot), content.trim() + '\n');
|
|
35
41
|
}
|
|
36
42
|
export function appendGlobalMemory(note) {
|
|
37
43
|
const existing = loadGlobalMemory();
|
|
@@ -40,17 +46,17 @@ export function appendGlobalMemory(note) {
|
|
|
40
46
|
: note.trim();
|
|
41
47
|
saveGlobalMemory(updated);
|
|
42
48
|
}
|
|
43
|
-
export function appendProjectMemory(note) {
|
|
44
|
-
const existing = loadProjectMemory();
|
|
49
|
+
export function appendProjectMemory(note, projectRoot) {
|
|
50
|
+
const existing = loadProjectMemory(projectRoot);
|
|
45
51
|
const updated = existing
|
|
46
52
|
? `${existing}\n\n${note.trim()}`
|
|
47
53
|
: note.trim();
|
|
48
|
-
saveProjectMemory(updated);
|
|
54
|
+
saveProjectMemory(updated, projectRoot);
|
|
49
55
|
}
|
|
50
|
-
export function loadAllMemory() {
|
|
56
|
+
export function loadAllMemory(projectRoot) {
|
|
51
57
|
return {
|
|
52
58
|
global: loadGlobalMemory(),
|
|
53
|
-
project: loadProjectMemory(),
|
|
59
|
+
project: loadProjectMemory(projectRoot),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
export function formatMemoryForPrompt(mem) {
|
|
@@ -63,8 +69,8 @@ export function formatMemoryForPrompt(mem) {
|
|
|
63
69
|
}
|
|
64
70
|
return parts.join('\n\n');
|
|
65
71
|
}
|
|
66
|
-
export function getProjectMemoryPath() {
|
|
67
|
-
return resolve(
|
|
72
|
+
export function getProjectMemoryPath(projectRoot) {
|
|
73
|
+
return resolve(getProjectMemoryFile(projectRoot));
|
|
68
74
|
}
|
|
69
75
|
export function getGlobalMemoryPath() {
|
|
70
76
|
return GLOBAL_MEMORY_FILE;
|
package/dist/repl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as readline from 'node:readline';
|
|
2
2
|
import { execSync, exec } from 'child_process';
|
|
3
3
|
import { restoreStdinListeners, extractUpstreamError } from './agent.js';
|
|
4
|
-
import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, promptHeaderText, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, renderSlashMenu, supportsVT, CH, } from './theme.js';
|
|
4
|
+
import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, promptHeaderText, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, renderSlashMenu, supportsVT, CH, planApprovalPrompt, planHeader, } from './theme.js';
|
|
5
5
|
import { renderMarkdown } from './renderer.js';
|
|
6
6
|
import { loadAllMemory } from './memory.js';
|
|
7
7
|
import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey, loadConfig } from './config.js';
|
|
@@ -285,13 +285,15 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
287
|
const { appendProjectMemory } = await import('./memory.js');
|
|
288
|
-
|
|
288
|
+
const { findProjectRoot } = await import('./utils.js');
|
|
289
|
+
appendProjectMemory(note, findProjectRoot(process.cwd()));
|
|
289
290
|
}
|
|
290
291
|
console.log(successLine(`Saved to ${scope} memory.`));
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
else {
|
|
294
|
-
const
|
|
295
|
+
const { findProjectRoot } = await import('./utils.js');
|
|
296
|
+
const mem = loadAllMemory(findProjectRoot(process.cwd()));
|
|
295
297
|
if (mem.global || mem.project) {
|
|
296
298
|
if (mem.global) {
|
|
297
299
|
console.log(`\n${c.primary.bold('Global Memory')} ${c.muted('(~/.ikie/memory.md)')}`);
|
|
@@ -469,6 +471,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
469
471
|
}
|
|
470
472
|
case 'skills': {
|
|
471
473
|
const sub = (args[0] ?? 'list').toLowerCase();
|
|
474
|
+
const { findProjectRoot } = await import('./utils.js');
|
|
475
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
472
476
|
const { discoverSkills, getSkill, listSkillFiles, installSkill, removeSkill } = await import('./skills.js');
|
|
473
477
|
if (sub === 'install' || sub === 'add') {
|
|
474
478
|
const rest = args.slice(1).filter(a => a !== '--force');
|
|
@@ -517,7 +521,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
517
521
|
console.log(errorLine('Usage: /skills show <name>'));
|
|
518
522
|
return true;
|
|
519
523
|
}
|
|
520
|
-
const skill = getSkill(nameArg);
|
|
524
|
+
const skill = getSkill(nameArg, projectRoot);
|
|
521
525
|
if (!skill) {
|
|
522
526
|
console.log(errorLine(`No skill named "${nameArg}".`));
|
|
523
527
|
return true;
|
|
@@ -529,7 +533,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
529
533
|
console.log(renderMarkdown(skill.body || '(no instructions)'));
|
|
530
534
|
return true;
|
|
531
535
|
}
|
|
532
|
-
const skills = discoverSkills();
|
|
536
|
+
const skills = discoverSkills(projectRoot);
|
|
533
537
|
if (!skills.length) {
|
|
534
538
|
console.log(infoLine('No skills installed.'));
|
|
535
539
|
console.log(`\n ${c.muted('Install one with')} ${c.warning('/skills install <git-url|path>')}`);
|
|
@@ -943,14 +947,12 @@ function confirmExecutePlan() {
|
|
|
943
947
|
resolve(false);
|
|
944
948
|
return;
|
|
945
949
|
}
|
|
946
|
-
process.stdout.write(
|
|
947
|
-
`${c.success.bold('y')} ${c.muted('yes')} ${c.error.bold('n')} ${c.muted('keep planning')}\n` +
|
|
948
|
-
` ${c.muted('❯')} `);
|
|
950
|
+
process.stdout.write(planApprovalPrompt());
|
|
949
951
|
const onKey = (d) => {
|
|
950
952
|
process.stdin.removeListener('data', onKey);
|
|
951
953
|
const k = d.toString().toLowerCase();
|
|
952
954
|
const yes = k === 'y' || k === '\r' || k === '\n';
|
|
953
|
-
process.stdout.write(yes ? c.success('y\n') : c.
|
|
955
|
+
process.stdout.write(yes ? c.success('y\n') : c.error('N\n'));
|
|
954
956
|
resolve(yes);
|
|
955
957
|
};
|
|
956
958
|
process.stdin.on('data', onKey);
|
|
@@ -1130,6 +1132,30 @@ function parsePlanSteps(conversation) {
|
|
|
1130
1132
|
}
|
|
1131
1133
|
export async function startREPL(agent, config, projectContext, oneShot) {
|
|
1132
1134
|
void HISTORY_FILE;
|
|
1135
|
+
const setBracketedPaste = (on) => {
|
|
1136
|
+
if (process.stdout.isTTY)
|
|
1137
|
+
process.stdout.write(on ? '\x1b[?2004h' : '\x1b[?2004l');
|
|
1138
|
+
};
|
|
1139
|
+
const restoreTerminal = () => {
|
|
1140
|
+
setBracketedPaste(false);
|
|
1141
|
+
if (process.stdin.isTTY) {
|
|
1142
|
+
process.stdin.setRawMode(false);
|
|
1143
|
+
process.stdin.pause();
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
const onFatalError = (err) => {
|
|
1147
|
+
restoreTerminal();
|
|
1148
|
+
console.error('\n' + errorLine(`Uncaught exception: ${err instanceof Error ? err.stack || err.message : String(err)}`));
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
};
|
|
1151
|
+
const onUnhandledRejection = (reason) => {
|
|
1152
|
+
restoreTerminal();
|
|
1153
|
+
console.error('\n' + errorLine(`Unhandled rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`));
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
};
|
|
1156
|
+
process.on('uncaughtException', onFatalError);
|
|
1157
|
+
process.on('unhandledRejection', onUnhandledRejection);
|
|
1158
|
+
process.on('exit', restoreTerminal);
|
|
1133
1159
|
// Context window size — default 131072 (128k), updated silently from model info.
|
|
1134
1160
|
let contextWindow = 131072;
|
|
1135
1161
|
fetchModelsFromServer(config).then(models => {
|
|
@@ -1183,12 +1209,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1183
1209
|
// \x1b[200~ … \x1b[201~ markers. This lets a multi-chunk paste (a long
|
|
1184
1210
|
// paragraph the TTY delivers across several reads) be coalesced into ONE
|
|
1185
1211
|
// paste instead of being split into several. Disabled again on exit.
|
|
1186
|
-
const setBracketedPaste = (on) => {
|
|
1187
|
-
if (process.stdout.isTTY)
|
|
1188
|
-
process.stdout.write(on ? '\x1b[?2004h' : '\x1b[?2004l');
|
|
1189
|
-
};
|
|
1190
1212
|
setBracketedPaste(true);
|
|
1191
|
-
process.on('exit', () => setBracketedPaste(false));
|
|
1192
1213
|
let multilineBuffer = '';
|
|
1193
1214
|
let busy = false;
|
|
1194
1215
|
let ctrlCCount = 0;
|
|
@@ -1648,6 +1669,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1648
1669
|
};
|
|
1649
1670
|
process.stdin.on('data', cancelHandler);
|
|
1650
1671
|
try {
|
|
1672
|
+
// Display mode status for plan mode
|
|
1673
|
+
if (agent.getMode() === 'plan') {
|
|
1674
|
+
process.stdout.write(`\n${planHeader('plan mode', agent.getLastTurnStats().filesChanged)}\n\n`);
|
|
1675
|
+
}
|
|
1651
1676
|
await agent.send(userContent, {
|
|
1652
1677
|
autoApprove: config.autoApprove,
|
|
1653
1678
|
signal: abortController.signal,
|
package/dist/theme.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ declare const CH: {
|
|
|
50
50
|
cont: string;
|
|
51
51
|
arrow: string;
|
|
52
52
|
dot: string;
|
|
53
|
+
sparkle: string;
|
|
54
|
+
check: string;
|
|
55
|
+
box: string;
|
|
53
56
|
};
|
|
54
57
|
export { CH };
|
|
55
58
|
/**
|
|
@@ -109,3 +112,54 @@ export declare function errorLine(msg: string): string;
|
|
|
109
112
|
export declare function warnLine(msg: string): string;
|
|
110
113
|
export declare function infoLine(msg: string): string;
|
|
111
114
|
export declare function permissionPrompt(toolName: string, preview: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* Draw a rounded box around text (like command input in images)
|
|
117
|
+
*/
|
|
118
|
+
export declare function roundedBox(content: string, opts?: {
|
|
119
|
+
width?: number;
|
|
120
|
+
padding?: number;
|
|
121
|
+
}): string;
|
|
122
|
+
/**
|
|
123
|
+
* Status indicator with sparkle icon
|
|
124
|
+
*/
|
|
125
|
+
export declare function sparkleStatus(message: string, color?: keyof typeof c): string;
|
|
126
|
+
/**
|
|
127
|
+
* Thinking time display
|
|
128
|
+
*/
|
|
129
|
+
export declare function thinkingTime(seconds: number): string;
|
|
130
|
+
/**
|
|
131
|
+
* Action indicator (e.g., "✦ Edit src/api/checkout.ts")
|
|
132
|
+
*/
|
|
133
|
+
export declare function actionLine(action: string, target: string): string;
|
|
134
|
+
/**
|
|
135
|
+
* Plan step display
|
|
136
|
+
*/
|
|
137
|
+
export declare function planStep(text: string, completed?: boolean): string;
|
|
138
|
+
/**
|
|
139
|
+
* Plan header
|
|
140
|
+
*/
|
|
141
|
+
export declare function planHeader(mode: string, filesChanged?: number): string;
|
|
142
|
+
/**
|
|
143
|
+
* Proposed plan title
|
|
144
|
+
*/
|
|
145
|
+
export declare function planTitle(stepCount: number): string;
|
|
146
|
+
/**
|
|
147
|
+
* Bottom status bar (full width)
|
|
148
|
+
*/
|
|
149
|
+
export declare function bottomBar(leftText: string, rightText?: string): string;
|
|
150
|
+
/**
|
|
151
|
+
* Code line with number and optional highlight
|
|
152
|
+
*/
|
|
153
|
+
export declare function codeLine(lineNum: number, code: string, highlight?: boolean): string;
|
|
154
|
+
/**
|
|
155
|
+
* File edit header
|
|
156
|
+
*/
|
|
157
|
+
export declare function fileEditHeader(filePath: string): string;
|
|
158
|
+
/**
|
|
159
|
+
* Approval prompt for plan execution
|
|
160
|
+
*/
|
|
161
|
+
export declare function planApprovalPrompt(): string;
|
|
162
|
+
/**
|
|
163
|
+
* Command echo in rounded box
|
|
164
|
+
*/
|
|
165
|
+
export declare function commandBox(command: string): string;
|
package/dist/theme.js
CHANGED
|
@@ -284,8 +284,8 @@ const IS_WIN = process.platform === 'win32';
|
|
|
284
284
|
/** True when the terminal supports VT100 escape sequences (cursor save/restore, etc). */
|
|
285
285
|
export const supportsVT = !IS_WIN;
|
|
286
286
|
const CH = IS_WIN
|
|
287
|
-
? { tl: '+-', prompt: '-> ', cont: '| ', arrow: '>', dot: 'o' }
|
|
288
|
-
: { tl: '╭─', prompt: '╰─❯', cont: '│ ', arrow: '❯', dot: '●' };
|
|
287
|
+
? { tl: '+-', prompt: '-> ', cont: '| ', arrow: '>', dot: 'o', sparkle: '*', check: 'v', box: '[ ]' }
|
|
288
|
+
: { tl: '╭─', prompt: '╰─❯', cont: '│ ', arrow: '❯', dot: '●', sparkle: '✦', check: '✓', box: '☐' };
|
|
289
289
|
export { CH };
|
|
290
290
|
/**
|
|
291
291
|
* Builds the prompt header line (e.g. `╭─ ikie · agent theme aurora in <cwd>`)
|
|
@@ -297,7 +297,16 @@ export function promptHeaderText(mode = 'agent') {
|
|
|
297
297
|
const branch = getGitBranchFast();
|
|
298
298
|
const gitSegment = branch ? ` ${c.muted('on')} ${c.secondary(branch)}` : '';
|
|
299
299
|
const themeSegment = ` ${c.muted('theme')} ${c.secondary(activeTheme.name)}`;
|
|
300
|
-
|
|
300
|
+
const leftStr = `${c.primary(CH.tl)} ${c.primary.bold('ikie')} ${c.muted('·')} ${modeTag(mode)}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}`;
|
|
301
|
+
const rightStr = `${mode} · shift+tab to toggle`;
|
|
302
|
+
const cols = process.stdout.columns ?? 80;
|
|
303
|
+
const leftLen = stripAnsi(leftStr).length;
|
|
304
|
+
const rightLen = rightStr.length;
|
|
305
|
+
const space = cols - leftLen - rightLen - 1; // -1 safety to prevent soft-wrap
|
|
306
|
+
if (space > 0) {
|
|
307
|
+
return `${leftStr}${' '.repeat(space)}${c.dim(rightStr)}`;
|
|
308
|
+
}
|
|
309
|
+
return leftStr;
|
|
301
310
|
}
|
|
302
311
|
export function printPromptHeader(mode = 'agent') {
|
|
303
312
|
process.stdout.write(`\n${promptHeaderText(mode)}\n`);
|
|
@@ -667,3 +676,127 @@ export function permissionPrompt(toolName, preview) {
|
|
|
667
676
|
`${c.muted.bold('!')} ${c.muted('never')}\n` +
|
|
668
677
|
` ${c.muted(CH.arrow)} `);
|
|
669
678
|
}
|
|
679
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
680
|
+
// Modern UX Components
|
|
681
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
682
|
+
/**
|
|
683
|
+
* Draw a rounded box around text (like command input in images)
|
|
684
|
+
*/
|
|
685
|
+
export function roundedBox(content, opts) {
|
|
686
|
+
const cols = opts?.width ?? Math.min(process.stdout.columns ?? 80, 120);
|
|
687
|
+
const padding = opts?.padding ?? 1;
|
|
688
|
+
const innerWidth = cols - 4 - (padding * 2);
|
|
689
|
+
const lines = content.split('\n').flatMap(line => {
|
|
690
|
+
if (stripAnsi(line).length <= innerWidth)
|
|
691
|
+
return [line];
|
|
692
|
+
// Simple wrapping for long lines
|
|
693
|
+
const chunks = [];
|
|
694
|
+
let current = '';
|
|
695
|
+
for (const word of line.split(' ')) {
|
|
696
|
+
if (stripAnsi(current + ' ' + word).length > innerWidth) {
|
|
697
|
+
if (current)
|
|
698
|
+
chunks.push(current);
|
|
699
|
+
current = word;
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
current = current ? current + ' ' + word : word;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (current)
|
|
706
|
+
chunks.push(current);
|
|
707
|
+
return chunks;
|
|
708
|
+
});
|
|
709
|
+
const tl = '╭';
|
|
710
|
+
const tr = '╮';
|
|
711
|
+
const bl = '╰';
|
|
712
|
+
const br = '╯';
|
|
713
|
+
const h = '─';
|
|
714
|
+
const v = '│';
|
|
715
|
+
const pad = ' '.repeat(padding);
|
|
716
|
+
const top = c.muted(`${tl}${h.repeat(cols - 2)}${tr}`);
|
|
717
|
+
const bottom = c.muted(`${bl}${h.repeat(cols - 2)}${br}`);
|
|
718
|
+
const contentLines = lines.map(line => {
|
|
719
|
+
const visible = stripAnsi(line).length;
|
|
720
|
+
const spaces = innerWidth - visible;
|
|
721
|
+
return `${c.muted(v)}${pad}${line}${' '.repeat(spaces)}${pad}${c.muted(v)}`;
|
|
722
|
+
});
|
|
723
|
+
return [top, ...contentLines, bottom].join('\n');
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Status indicator with sparkle icon
|
|
727
|
+
*/
|
|
728
|
+
export function sparkleStatus(message, color) {
|
|
729
|
+
const sparkleColor = color ? c[color] : c.warning;
|
|
730
|
+
return `${sparkleColor(CH.sparkle)} ${c.muted(message)}`;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Thinking time display
|
|
734
|
+
*/
|
|
735
|
+
export function thinkingTime(seconds) {
|
|
736
|
+
return sparkleStatus(`Thought for ${seconds.toFixed(1)}s`, 'warning');
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Action indicator (e.g., "✦ Edit src/api/checkout.ts")
|
|
740
|
+
*/
|
|
741
|
+
export function actionLine(action, target) {
|
|
742
|
+
return `${c.warning(CH.sparkle)} ${c.white(action)} ${c.accent(target)}`;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Plan step display
|
|
746
|
+
*/
|
|
747
|
+
export function planStep(text, completed = false) {
|
|
748
|
+
const icon = completed ? c.success(CH.check) : c.muted(CH.box);
|
|
749
|
+
return `${icon} ${completed ? c.white(text) : c.dim(text)}`;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Plan header
|
|
753
|
+
*/
|
|
754
|
+
export function planHeader(mode, filesChanged = 0) {
|
|
755
|
+
return sparkleStatus(`${mode} · read-only · ${filesChanged} files changed`, 'warning');
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Proposed plan title
|
|
759
|
+
*/
|
|
760
|
+
export function planTitle(stepCount) {
|
|
761
|
+
return `${c.white('Proposed plan')} ${c.muted('·')} ${c.secondary(`${stepCount} steps`)}`;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Bottom status bar (full width)
|
|
765
|
+
*/
|
|
766
|
+
export function bottomBar(leftText, rightText) {
|
|
767
|
+
const cols = process.stdout.columns ?? 80;
|
|
768
|
+
const left = stripAnsi(leftText);
|
|
769
|
+
const right = rightText ? stripAnsi(rightText) : '';
|
|
770
|
+
const spacing = cols - left.length - right.length;
|
|
771
|
+
return (c.muted(leftText) +
|
|
772
|
+
' '.repeat(Math.max(0, spacing)) +
|
|
773
|
+
(rightText ? c.dim(rightText) : ''));
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Code line with number and optional highlight
|
|
777
|
+
*/
|
|
778
|
+
export function codeLine(lineNum, code, highlight = false) {
|
|
779
|
+
const num = c.dim(String(lineNum).padStart(4) + ' ');
|
|
780
|
+
const content = highlight ? chalk.bgHex('#1a3a2e')(code) : code;
|
|
781
|
+
return `${num} ${content}`;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* File edit header
|
|
785
|
+
*/
|
|
786
|
+
export function fileEditHeader(filePath) {
|
|
787
|
+
return actionLine('Edit', filePath);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Approval prompt for plan execution
|
|
791
|
+
*/
|
|
792
|
+
export function planApprovalPrompt() {
|
|
793
|
+
return `
|
|
794
|
+
${c.muted('Approve to run in agent mode?')} ${c.success.bold('[y')}/${c.error('N]')}
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Command echo in rounded box
|
|
799
|
+
*/
|
|
800
|
+
export function commandBox(command) {
|
|
801
|
+
return roundedBox(`${c.primary('>')} ${c.white(command)}`);
|
|
802
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimate the number of tokens in a string.
|
|
3
|
+
*
|
|
4
|
+
* Heuristic rationale:
|
|
5
|
+
* - Prose English: ~4 chars / token (GPT BPE baseline)
|
|
6
|
+
* - Code/symbols: ~3 chars / token (more tokens per char due to operators,
|
|
7
|
+
* punctuation, identifiers broken at sub-word
|
|
8
|
+
* boundaries by BPE tokenisers)
|
|
9
|
+
*
|
|
10
|
+
* We classify a string as "code-heavy" when the density of typical code
|
|
11
|
+
* characters (braces, semicolons, brackets, operators, indentation runs) is
|
|
12
|
+
* above a threshold. This gives a noticeably better estimate for tool outputs
|
|
13
|
+
* and file contents while remaining O(n) and zero-dependency.
|
|
14
|
+
*
|
|
15
|
+
* The function is designed as a drop-in for a proper BPE tokeniser:
|
|
16
|
+
* swap the body for `tiktoken.encode(text).length` when you want exactness.
|
|
17
|
+
*/
|
|
18
|
+
export declare function estimateTokens(chars: number, text?: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Walk up the directory tree from `startDir`, looking for a `.git` directory
|
|
21
|
+
* or a `package.json` file. Returns the absolute path of the detected root,
|
|
22
|
+
* or `startDir` itself if nothing is found (graceful fallback).
|
|
23
|
+
*
|
|
24
|
+
* This lets `ikie` be invoked from a deeply nested subdirectory and still
|
|
25
|
+
* resolve project-scoped memory/config files correctly.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findProjectRoot(startDir: string): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
// ── Token estimation ──────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Estimate the number of tokens in a string.
|
|
6
|
+
*
|
|
7
|
+
* Heuristic rationale:
|
|
8
|
+
* - Prose English: ~4 chars / token (GPT BPE baseline)
|
|
9
|
+
* - Code/symbols: ~3 chars / token (more tokens per char due to operators,
|
|
10
|
+
* punctuation, identifiers broken at sub-word
|
|
11
|
+
* boundaries by BPE tokenisers)
|
|
12
|
+
*
|
|
13
|
+
* We classify a string as "code-heavy" when the density of typical code
|
|
14
|
+
* characters (braces, semicolons, brackets, operators, indentation runs) is
|
|
15
|
+
* above a threshold. This gives a noticeably better estimate for tool outputs
|
|
16
|
+
* and file contents while remaining O(n) and zero-dependency.
|
|
17
|
+
*
|
|
18
|
+
* The function is designed as a drop-in for a proper BPE tokeniser:
|
|
19
|
+
* swap the body for `tiktoken.encode(text).length` when you want exactness.
|
|
20
|
+
*/
|
|
21
|
+
export function estimateTokens(chars, text) {
|
|
22
|
+
if (chars <= 0)
|
|
23
|
+
return 1;
|
|
24
|
+
let charsPerToken = 4; // prose default
|
|
25
|
+
if (text && text.length > 0) {
|
|
26
|
+
// Count code-characteristic characters
|
|
27
|
+
let codeChars = 0;
|
|
28
|
+
for (let i = 0; i < text.length; i++) {
|
|
29
|
+
const c = text.charCodeAt(i);
|
|
30
|
+
// { } ( ) [ ] ; : = < > ! & | ^ ~ + - * / \ . , @ # % ` and tab
|
|
31
|
+
if (c === 123 || c === 125 || // { }
|
|
32
|
+
c === 40 || c === 41 || // ( )
|
|
33
|
+
c === 91 || c === 93 || // [ ]
|
|
34
|
+
c === 59 || c === 58 || // ; :
|
|
35
|
+
c === 61 || c === 60 || c === 62 || // = < >
|
|
36
|
+
c === 33 || c === 38 || c === 124 || // ! & |
|
|
37
|
+
c === 94 || c === 126 || c === 43 || // ^ ~ +
|
|
38
|
+
c === 45 || c === 42 || c === 47 || // - * /
|
|
39
|
+
c === 92 || c === 46 || c === 44 || // \ . ,
|
|
40
|
+
c === 64 || c === 35 || c === 37 || // @ # %
|
|
41
|
+
c === 96 || c === 9 // ` tab
|
|
42
|
+
) {
|
|
43
|
+
codeChars++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const codeDensity = codeChars / text.length;
|
|
47
|
+
if (codeDensity > 0.12) {
|
|
48
|
+
charsPerToken = 3; // code-heavy — more tokens per char
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Math.max(1, Math.round(chars / charsPerToken));
|
|
52
|
+
}
|
|
53
|
+
// ── Project root detection ────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Walk up the directory tree from `startDir`, looking for a `.git` directory
|
|
56
|
+
* or a `package.json` file. Returns the absolute path of the detected root,
|
|
57
|
+
* or `startDir` itself if nothing is found (graceful fallback).
|
|
58
|
+
*
|
|
59
|
+
* This lets `ikie` be invoked from a deeply nested subdirectory and still
|
|
60
|
+
* resolve project-scoped memory/config files correctly.
|
|
61
|
+
*/
|
|
62
|
+
export function findProjectRoot(startDir) {
|
|
63
|
+
let current = startDir;
|
|
64
|
+
// Safety cap: don't walk more than 20 levels to avoid infinite loops on
|
|
65
|
+
// pathological setups or file systems where the root is never reached.
|
|
66
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
67
|
+
if (existsSync(join(current, '.git')) ||
|
|
68
|
+
existsSync(join(current, 'package.json'))) {
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
const parent = dirname(current);
|
|
72
|
+
if (parent === current)
|
|
73
|
+
break; // reached filesystem root
|
|
74
|
+
current = parent;
|
|
75
|
+
}
|
|
76
|
+
return startDir; // fallback — treat cwd as the project root
|
|
77
|
+
}
|