@axplusb/kepler 1.0.5 → 1.0.10

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.
@@ -17,17 +17,17 @@
17
17
 
18
18
  import * as readline from 'node:readline';
19
19
  import * as path from 'node:path';
20
+ import * as fs from 'node:fs';
20
21
  import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
21
- import { calculateCost, formatCostValue, formatTokens } from '../core/pricing.mjs';
22
+ import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
22
23
  import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
23
24
  import { JsonlWriter } from '../core/jsonl-writer.mjs';
24
25
  import { createToolExecutor } from '../core/tool-executor.mjs';
26
+ import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
25
27
  import { TarangAuth } from '../auth/tarang-auth.mjs';
26
28
  import { ApprovalManager } from '../core/approval.mjs';
27
29
  import { resolveBackendUrl } from '../core/backend-url.mjs';
28
30
  import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
29
- import { ContextRetriever } from '../context/retriever.mjs';
30
- import { buildProjectSkeleton } from '../context/skeleton.mjs';
31
31
  import { SessionManager } from '../core/session-manager.mjs';
32
32
  import { parseArgs } from '../config/cli-args.mjs';
33
33
  import { formatShellCommand, toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
@@ -163,13 +163,12 @@ function printBanner(auth) {
163
163
  */
164
164
  function buildContextStrip() {
165
165
  const totalTokens = session.inputTokens + session.outputTokens;
166
- const cost = formatCostValue(session.totalCost);
166
+ const credits = formatCredits(costToCredits(session.totalCost));
167
167
  const elapsed = formatElapsed(session.startTime);
168
168
 
169
- // Right side — always shown
170
169
  const right = [
171
170
  c.dim(`${formatTokens(totalTokens)} tok`),
172
- c.dim(cost),
171
+ c.dim(credits),
173
172
  c.dim(elapsed),
174
173
  ].join(c.dim(' · '));
175
174
 
@@ -200,7 +199,7 @@ function printTurnSummary(toolCount, durationS, turnCost) {
200
199
  const parts = [];
201
200
  if (toolCount > 0) parts.push(`${toolCount} tools`);
202
201
  if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
203
- if (turnCost > 0) parts.push(formatCostValue(turnCost));
202
+ if (turnCost > 0) parts.push(formatCredits(costToCredits(turnCost)));
204
203
  if (parts.length > 0) {
205
204
  process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
206
205
  }
@@ -621,6 +620,16 @@ function renderEvent(event) {
621
620
  break;
622
621
  }
623
622
 
623
+ case 'plan_created': {
624
+ process.stderr.write(` ${c.dim('project plan prepared')}\n`);
625
+ break;
626
+ }
627
+
628
+ case 'goal_created': {
629
+ process.stderr.write(` ${c.dim('project goal prepared')}\n`);
630
+ break;
631
+ }
632
+
624
633
  case 'session_info': {
625
634
  if (data?.session_id) {
626
635
  session.id = data.session_id;
@@ -773,7 +782,7 @@ async function handleCommand(input, ctx) {
773
782
  process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
774
783
  process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total, ${session.toolCalls} last turn\n`);
775
784
  process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
776
- process.stderr.write(` ${c.dim('Cost')} ${formatCostValue(session.totalCost)}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
785
+ process.stderr.write(` ${c.dim('Credits')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
777
786
  process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
778
787
 
779
788
  // Permissions
@@ -841,29 +850,29 @@ async function handleCommand(input, ctx) {
841
850
  process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
842
851
  process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls}\n`);
843
852
  process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
844
- process.stderr.write(` ${c.gray('Cost:')} ${formatCostValue(session.totalCost)}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
853
+ process.stderr.write(` ${c.gray('Credits:')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
845
854
  process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
846
855
  return;
847
856
  }
848
857
 
849
858
  case '/cost': {
850
- process.stderr.write(`\n ${c.bold('Session Cost')}`);
859
+ process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(formatCredits(costToCredits(session.totalCost)))}`);
851
860
  if (!session.costAccurate) {
852
- process.stderr.write(` ${c.yellow('(estimated — backend not sending model breakdown)')}`);
861
+ process.stderr.write(` ${c.yellow('(estimated)')}`);
853
862
  }
854
863
  process.stderr.write('\n');
855
864
  process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
856
865
 
857
866
  if (session.costBreakdown.length > 0) {
858
867
  // Header
859
- process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('Cost'.padStart(10))}\n`);
868
+ process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('Credits'.padStart(10))}\n`);
860
869
  process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
861
870
 
862
871
  for (const b of session.costBreakdown) {
863
872
  const modelLabel = b.model === 'unknown' ? c.yellow('unknown model') : b.model;
864
873
  const roleTag = b.role && b.role !== 'unknown' ? ` ${c.dim(`(${b.role})`)}` : '';
865
874
  const cacheTokens = (b.cache_read_tokens || 0) + (b.cache_creation_tokens || 0);
866
- const costStr = b.free ? c.green('free') : formatCostValue(b.cost);
875
+ const costStr = b.free ? c.green('free') : formatCredits(costToCredits(b.cost));
867
876
 
868
877
  process.stderr.write(
869
878
  ` ${(modelLabel + roleTag).padEnd(36)}` +
@@ -882,9 +891,9 @@ async function handleCommand(input, ctx) {
882
891
  `${formatTokens(session.inputTokens).padStart(10)}` +
883
892
  `${formatTokens(session.outputTokens).padStart(10)}` +
884
893
  `${''.padStart(10)}` +
885
- `${formatCostValue(session.totalCost).padStart(10)}\n`
894
+ `${formatCredits(costToCredits(session.totalCost)).padStart(10)}\n`
886
895
  );
887
- process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
896
+ process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)} Provider: ${formatCostValue(session.totalCost)}`)}\n\n`);
888
897
  return;
889
898
  }
890
899
 
@@ -1122,9 +1131,8 @@ export async function startTerminalRepl() {
1122
1131
  const cliArgs = parseArgs(process.argv.slice(2));
1123
1132
  const auth = new TarangAuth();
1124
1133
 
1125
- // BM25 retriever indexes project files for search_code tool
1126
- const retriever = new ContextRetriever(safeCwd());
1127
- const toolExecutor = createToolExecutor({ retriever });
1134
+ // Projects are registered and indexed on demand through get_project_overview.
1135
+ const toolExecutor = createToolExecutor();
1128
1136
  const skipPerms = cliArgs.freeswim;
1129
1137
  const approval = new ApprovalManager({ autoApprove: skipPerms });
1130
1138
 
@@ -1142,51 +1150,13 @@ export async function startTerminalRepl() {
1142
1150
 
1143
1151
  printBanner(auth);
1144
1152
 
1145
- // ── Initialization with progress ──
1146
- // BM25 indexing is CPU-bound and blocks the event loop, so setInterval
1147
- // spinners won't tick during it. Instead, show a static "Initializing..."
1148
- // message, then yield to the event loop between phases so the spinner runs.
1149
- let projectSkeleton = '';
1150
-
1151
- // Phase 1: Show immediate feedback
1153
+ // ── Initialization ──
1152
1154
  process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
1153
-
1154
- // Fetch user in parallel (network I/O, won't block event loop)
1155
- const userPromise = fetchUser(ctx);
1156
-
1157
- // Phase 2: BM25 index — CPU-bound, blocks event loop.
1158
- // Wrap in a microtask break so the initial message renders first.
1159
- const indexResult = await new Promise((resolve) => {
1160
- // Let the event loop flush stderr before blocking
1161
- setImmediate(async () => {
1162
- try {
1163
- process.stderr.write(`\r ${c.brand('⠹')} ${c.dim('Indexing project files...')}${' '.repeat(20)}\r`);
1164
- const result = await retriever.buildIndex();
1165
- resolve(result);
1166
- } catch {
1167
- resolve({ fileCount: 0, chunkCount: 0 });
1168
- }
1169
- });
1170
- });
1171
-
1172
- // Phase 3: Build skeleton (fast, synchronous)
1173
- process.stderr.write(`\r ${c.brand('⠼')} ${c.dim('Building project skeleton...')}${' '.repeat(20)}\r`);
1174
- await new Promise(r => setImmediate(r)); // yield so message renders
1175
- projectSkeleton = buildProjectSkeleton(safeCwd());
1176
-
1177
- // Wait for user fetch
1178
- await userPromise;
1155
+ await fetchUser(ctx);
1179
1156
 
1180
1157
  // Clear the spinner line
1181
1158
  process.stderr.write(`\r${' '.repeat(60)}\r`);
1182
-
1183
- // Show init summary
1184
- if (indexResult.fileCount > 0) {
1185
- process.stderr.write(` ${c.green('✓')} ${c.dim(`Indexed ${indexResult.fileCount} files (${indexResult.chunkCount} chunks)`)}\n`);
1186
- }
1187
- if (projectSkeleton) {
1188
- process.stderr.write(` ${c.green('✓')} ${c.dim('Project skeleton ready')}\n`);
1189
- }
1159
+ process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
1190
1160
  if (session.user) {
1191
1161
  process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
1192
1162
  }
@@ -1367,9 +1337,17 @@ export async function startTerminalRepl() {
1367
1337
 
1368
1338
  const execContext = { cwd: safeCwd() };
1369
1339
  if (skipPerms) execContext.freeswim = true;
1370
- if (projectSkeleton) execContext.project_skeleton = projectSkeleton;
1340
+ execContext.project_resources = toolExecutor.getProjectResources();
1341
+ execContext.agent_context = toolExecutor.getAgentContext();
1371
1342
 
1372
1343
  for await (const event of client.execute(input, execContext, session.history)) {
1344
+ if (event.type === 'plan_created' || event.type === 'goal_created') {
1345
+ persistProjectArtifacts(
1346
+ event.data,
1347
+ toolExecutor.getProjectResources(),
1348
+ message => process.stderr.write(` ${c.dim(message)}\n`),
1349
+ );
1350
+ }
1373
1351
  renderEvent(event);
1374
1352
 
1375
1353
  if (event.type === 'content_partial') {
@@ -0,0 +1,54 @@
1
+ import { SkillInstaller } from '../skills/installer.mjs';
2
+ import { SkillsLoader } from '../skills/loader.mjs';
3
+
4
+ function has(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+
8
+ function scopeFrom(args) {
9
+ return has(args, '--project') ? 'project' : 'global';
10
+ }
11
+
12
+ function print(value) {
13
+ process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
14
+ }
15
+
16
+ export async function runSkillsCommand(args, { cwd = process.cwd() } = {}) {
17
+ const action = args[0] || 'list';
18
+ const rest = args.slice(1);
19
+ const scope = scopeFrom(rest);
20
+ const installer = new SkillInstaller({ cwd });
21
+ const loader = new SkillsLoader().load(cwd);
22
+
23
+ if (action === 'list') {
24
+ const rows = loader.list({ scope: has(rest, '--all') ? '' : scope });
25
+ if (has(rest, '--json')) print(rows);
26
+ else if (!rows.length) print('No skills found.');
27
+ else for (const row of rows) print(`${row.name}\t${row.scope}\t${row.source}\t${row.description}`);
28
+ return;
29
+ }
30
+ if (action === 'view') {
31
+ const name = rest.find(arg => !arg.startsWith('--'));
32
+ if (!name) throw new Error('Usage: kepler skills view <name> [resource-path]');
33
+ const nameIndex = rest.indexOf(name);
34
+ const resource = rest.slice(nameIndex + 1).find(arg => !arg.startsWith('--')) || null;
35
+ print(loader.view(name, resource));
36
+ return;
37
+ }
38
+ if (action === 'install') {
39
+ const source = rest.find(arg => !arg.startsWith('--'));
40
+ print(installer.install(source, { scope, force: has(rest, '--force') }));
41
+ return;
42
+ }
43
+ if (action === 'remove') {
44
+ const name = rest.find(arg => !arg.startsWith('--'));
45
+ print(installer.remove(name, { scope }));
46
+ return;
47
+ }
48
+ if (action === 'update') {
49
+ const name = rest.find(arg => !arg.startsWith('--'));
50
+ print(installer.update(name, { scope }));
51
+ return;
52
+ }
53
+ throw new Error(`Unknown skills command: ${action}`);
54
+ }
@@ -27,6 +27,7 @@ export const BashTool = {
27
27
  command: { type: 'string', description: 'The command to execute' },
28
28
  timeout: { type: 'number', description: 'Timeout in ms (max 600000)', default: 120000 },
29
29
  description: { type: 'string', description: 'Description of what this command does' },
30
+ cwd: { type: 'string', description: 'Working directory for the command' },
30
31
  run_in_background: { type: 'boolean', description: 'Run in background', default: false },
31
32
  },
32
33
  required: ['command'],
@@ -40,7 +41,7 @@ export const BashTool = {
40
41
  const timeout = Math.min(input.timeout || 120000, 600000);
41
42
 
42
43
  if (input.run_in_background) {
43
- return runBackground(input.command);
44
+ return runBackground(input.command, input.cwd);
44
45
  }
45
46
 
46
47
  return new Promise((resolve) => {
@@ -50,6 +51,7 @@ export const BashTool = {
50
51
  let exitCode = null;
51
52
 
52
53
  const proc = spawn('bash', ['-c', input.command], {
54
+ cwd: input.cwd,
53
55
  env: { ...process.env },
54
56
  stdio: ['pipe', 'pipe', 'pipe'],
55
57
  timeout: 0, // we handle timeout ourselves
@@ -120,9 +122,10 @@ export const BashTool = {
120
122
  const backgroundJobs = new Map();
121
123
  let bgJobId = 0;
122
124
 
123
- function runBackground(command) {
125
+ function runBackground(command, cwd) {
124
126
  const id = ++bgJobId;
125
127
  const proc = spawn('bash', ['-c', command], {
128
+ cwd,
126
129
  detached: true,
127
130
  stdio: ['ignore', 'pipe', 'pipe'],
128
131
  });