@axplusb/kepler 2.2.0 → 2.3.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,217 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+
6
+ const SCHEMA = 'kepler.work_scope/1';
7
+ const ROOT_MARKERS = [
8
+ '.kepler',
9
+ '.git',
10
+ 'package.json',
11
+ 'pyproject.toml',
12
+ 'setup.py',
13
+ 'go.mod',
14
+ 'Cargo.toml',
15
+ ];
16
+
17
+ function stable(value) {
18
+ if (Array.isArray(value)) return value.map(stable);
19
+ if (value && typeof value === 'object') {
20
+ return Object.fromEntries(
21
+ Object.entries(value)
22
+ .filter(([, v]) => v !== undefined)
23
+ .sort(([a], [b]) => a.localeCompare(b))
24
+ .map(([k, v]) => [k, stable(v)]),
25
+ );
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function sha(payload) {
31
+ return crypto
32
+ .createHash('sha256')
33
+ .update(JSON.stringify(stable(payload)))
34
+ .digest('hex')
35
+ .slice(0, 16);
36
+ }
37
+
38
+ function normalizePathInput(value) {
39
+ let s = String(value || '').trim();
40
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
41
+ s = s.slice(1, -1);
42
+ }
43
+ if (s === '~' || s.startsWith('~/')) {
44
+ s = path.join(os.homedir(), s.slice(1));
45
+ }
46
+ return s.replace(/\\([ \t()&$;'"])/g, '$1');
47
+ }
48
+
49
+ function nearestProjectRoot(candidate) {
50
+ let resolved = normalizePathInput(candidate);
51
+ if (!resolved || !path.isAbsolute(resolved)) return null;
52
+
53
+ try {
54
+ resolved = fs.realpathSync(resolved);
55
+ } catch {
56
+ return null;
57
+ }
58
+
59
+ let dir = resolved;
60
+ try {
61
+ if (fs.statSync(resolved).isFile()) dir = path.dirname(resolved);
62
+ } catch {
63
+ return null;
64
+ }
65
+
66
+ const root = path.parse(dir).root;
67
+ let current = dir;
68
+ while (current && current !== root) {
69
+ if (ROOT_MARKERS.some(marker => fs.existsSync(path.join(current, marker)))) {
70
+ return fs.realpathSync(current);
71
+ }
72
+ current = path.dirname(current);
73
+ }
74
+ return fs.realpathSync(dir);
75
+ }
76
+
77
+ function extractQuotedPaths(text) {
78
+ const paths = [];
79
+ const re = /(['"])(\/[^'"\n]+)\1/g;
80
+ let match;
81
+ while ((match = re.exec(String(text || ''))) !== null) {
82
+ paths.push(match[2]);
83
+ }
84
+ return paths;
85
+ }
86
+
87
+ function extractPastedPaths(text) {
88
+ const source = String(text || '');
89
+ const paths = [];
90
+ for (let i = 0; i < source.length; i++) {
91
+ if (source[i] !== '/') continue;
92
+ const line = source.slice(i).split(/\r?\n/, 1)[0].replace(/[),.;:]+$/g, '');
93
+ const parts = line.split(/\s+/).filter(Boolean);
94
+ let candidate = '';
95
+ let lastRoot = null;
96
+ for (const part of parts.slice(0, 12)) {
97
+ candidate = candidate ? `${candidate} ${part}` : part;
98
+ const root = nearestProjectRoot(candidate.replace(/[),.;:]+$/g, ''));
99
+ if (root) lastRoot = root;
100
+ }
101
+ if (lastRoot) paths.push(lastRoot);
102
+ }
103
+ return paths;
104
+ }
105
+
106
+ function uniqueRoots(entries) {
107
+ const seen = new Set();
108
+ const result = [];
109
+ for (const entry of entries) {
110
+ if (!entry?.path || seen.has(entry.path)) continue;
111
+ seen.add(entry.path);
112
+ result.push(entry);
113
+ }
114
+ return result;
115
+ }
116
+
117
+ function roleForRoot(root, cwd) {
118
+ const base = path.basename(root).toLowerCase();
119
+ if (base.includes('backend')) return 'backend';
120
+ if (base.includes('frontend') || base.includes('web')) return 'frontend';
121
+ if (base.includes('deploy')) return 'deploy';
122
+ if (base.includes('docs') || base.includes('prd')) return 'docs';
123
+ if (base.includes('npm') || base.includes('cli')) return 'cli';
124
+ if (root === cwd) return 'primary';
125
+ return 'workspace';
126
+ }
127
+
128
+ function truncate(value, max = 280) {
129
+ const s = String(value || '').replace(/\s+/g, ' ').trim();
130
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
131
+ }
132
+
133
+ function sortedResources(resources) {
134
+ return (Array.isArray(resources) ? resources : [])
135
+ .filter(resource => resource && resource.root)
136
+ .map(resource => ({
137
+ project_id: String(resource.project_id || ''),
138
+ root: String(resource.root || ''),
139
+ name: String(resource.name || path.basename(String(resource.root || ''))),
140
+ index_version: String(resource.index_version || ''),
141
+ }))
142
+ .sort((a, b) => a.root.localeCompare(b.root));
143
+ }
144
+
145
+ export function buildWorkScope({
146
+ instruction = '',
147
+ cwd = process.cwd(),
148
+ projectResources = [],
149
+ } = {}) {
150
+ const cwdRoot = nearestProjectRoot(cwd) || path.resolve(cwd);
151
+ const roots = [{
152
+ path: cwdRoot,
153
+ role: roleForRoot(cwdRoot, cwdRoot),
154
+ source: 'cwd',
155
+ status: 'active',
156
+ }];
157
+
158
+ for (const raw of [...extractQuotedPaths(instruction), ...extractPastedPaths(instruction)]) {
159
+ const root = nearestProjectRoot(raw);
160
+ if (!root) continue;
161
+ roots.push({
162
+ path: root,
163
+ role: roleForRoot(root, cwdRoot),
164
+ source: 'prompt',
165
+ status: 'active',
166
+ });
167
+ }
168
+
169
+ for (const resource of sortedResources(projectResources)) {
170
+ roots.push({
171
+ path: resource.root,
172
+ role: roleForRoot(resource.root, cwdRoot),
173
+ source: 'registered',
174
+ status: 'active',
175
+ project_id: resource.project_id || undefined,
176
+ });
177
+ }
178
+
179
+ const activeRoots = uniqueRoots(roots);
180
+ const resources = sortedResources(projectResources);
181
+ const scope = {
182
+ schema: SCHEMA,
183
+ primary_root: cwdRoot,
184
+ intent: truncate(instruction),
185
+ active_roots: activeRoots,
186
+ candidate_roots: [],
187
+ workspace_resources: resources,
188
+ cache_policy: {
189
+ stable_system: false,
190
+ placement: 'pinned_context',
191
+ reason: 'scope changes with user intent and discovered roots',
192
+ },
193
+ };
194
+ scope.version = sha({
195
+ schema: scope.schema,
196
+ primary_root: scope.primary_root,
197
+ intent: scope.intent,
198
+ active_roots: scope.active_roots,
199
+ workspace_resources: scope.workspace_resources,
200
+ });
201
+ return scope;
202
+ }
203
+
204
+ export function summarizeWorkScope(scope) {
205
+ if (!scope || typeof scope !== 'object') return '';
206
+ const roots = Array.isArray(scope.active_roots) ? scope.active_roots : [];
207
+ const lines = [
208
+ `Work scope ${scope.version || 'unknown'}`,
209
+ `Primary: ${scope.primary_root || '(unknown)'}`,
210
+ ];
211
+ if (scope.intent) lines.push(`Intent: ${scope.intent}`);
212
+ for (const root of roots) {
213
+ if (!root?.path) continue;
214
+ lines.push(`- ${root.path} [${root.role || 'workspace'}; ${root.source || 'unknown'}]`);
215
+ }
216
+ return lines.join('\n');
217
+ }
@@ -162,6 +162,8 @@ async function main() {
162
162
  model: args.model,
163
163
  timeout: args.timeout || (args.maxTurns ? args.maxTurns * 60 : 600),
164
164
  verbose: args.verbose,
165
+ cacheReport: args.cacheReport,
166
+ local: args.local,
165
167
  });
166
168
  return;
167
169
  }
@@ -23,6 +23,7 @@ import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCred
23
23
  import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
24
24
  import { JsonlWriter } from '../core/jsonl-writer.mjs';
25
25
  import { createToolExecutor } from '../core/tool-executor.mjs';
26
+ import { buildWorkScope } from '../core/work-scope.mjs';
26
27
  import { CheckpointManager } from '../core/checkpoints.mjs';
27
28
  import { HookRunner } from '../config/hook-runner.mjs';
28
29
  import { runPreflight } from '../onboarding/preflight.mjs';
@@ -47,7 +48,7 @@ import { loadProjectContext } from '../core/project-context-loader.mjs';
47
48
  import { buildContextEnvelope } from '../core/context-envelope.mjs';
48
49
  import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
49
50
  import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
50
- import { appendTask, ensureTaskFiles, loadTaskBoard, taskCounts, TASK_FILES } from '../core/tasks.mjs';
51
+ import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
51
52
  import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
52
53
  import { createOrbit } from '../state/orbit.mjs';
53
54
  import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
@@ -1121,12 +1122,26 @@ function printBanner(auth) {
1121
1122
  * Left side: last-turn summary (tools, time, cost)
1122
1123
  * Right side: session totals (ctx%, tokens)
1123
1124
  */
1125
+ function computeCacheTotals() {
1126
+ let read = 0;
1127
+ let write = 0;
1128
+ for (const b of session.costBreakdown) {
1129
+ read += b.cache_read_tokens || 0;
1130
+ write += b.cache_creation_tokens || 0;
1131
+ }
1132
+ const denom = session.inputTokens + read;
1133
+ const hitRate = denom > 0 ? Math.round((read / denom) * 100) : 0;
1134
+ return { read, write, hitRate };
1135
+ }
1136
+
1124
1137
  function buildContextStrip() {
1125
1138
  const totalTokens = session.inputTokens + session.outputTokens;
1126
1139
  const elapsed = formatElapsed(session.startTime);
1140
+ const cache = computeCacheTotals();
1127
1141
 
1128
1142
  const right = [
1129
1143
  c.dim(`${formatTokens(totalTokens)} tok`),
1144
+ ...(cache.read > 0 ? [c.dim(`cache ${cache.hitRate}%`)] : []),
1130
1145
  c.dim(elapsed),
1131
1146
  ].join(c.dim(' · '));
1132
1147
 
@@ -1968,7 +1983,7 @@ function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
1968
1983
  }
1969
1984
 
1970
1985
  renderTaskBoard(board, { showDone: mode === 'status' });
1971
- process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|blocked|done <text>')}\n\n`);
1986
+ process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
1972
1987
  }
1973
1988
 
1974
1989
  function refreshTaskContext(ctx) {
@@ -1987,7 +2002,7 @@ function handleTasksCommand(rest, ctx) {
1987
2002
  process.stderr.write(`\n ${c.bold('Tasks')}\n`);
1988
2003
  process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
1989
2004
  renderTaskBoard(board, { showDone: true });
1990
- process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|backlog|blocked|done <text>')}\n\n`);
2005
+ process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
1991
2006
  return;
1992
2007
  }
1993
2008
 
@@ -1998,6 +2013,60 @@ function handleTasksCommand(rest, ctx) {
1998
2013
 
1999
2014
  const parts = raw.split(/\s+/);
2000
2015
  let verb = (parts.shift() || '').toLowerCase();
2016
+
2017
+ if (verb === 'move') {
2018
+ try {
2019
+ const [from, index, to, ...textParts] = parts;
2020
+ const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
2021
+ refreshTaskContext(ctx);
2022
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
2023
+ } catch (err) {
2024
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2025
+ process.stderr.write(` ${c.gray('Usage: /tasks move <active|backlog|blocked|done> <number> <active|backlog|blocked|done> [new text]')}\n`);
2026
+ }
2027
+ return;
2028
+ }
2029
+
2030
+ if (verb === 'edit' || verb === 'rename') {
2031
+ try {
2032
+ const [list, index, ...textParts] = parts;
2033
+ const result = updateTask({ cwd: safeCwd(), list, index, text: textParts.join(' ') });
2034
+ refreshTaskContext(ctx);
2035
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`updated ${result.list} #${result.index}`)} ${result.text}\n`);
2036
+ } catch (err) {
2037
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2038
+ process.stderr.write(` ${c.gray('Usage: /tasks edit <active|backlog|blocked|done> <number> <new text>')}\n`);
2039
+ }
2040
+ return;
2041
+ }
2042
+
2043
+ if (verb === 'remove' || verb === 'rm' || verb === 'delete') {
2044
+ try {
2045
+ const [list, index] = parts;
2046
+ const result = removeTask({ cwd: safeCwd(), list, index });
2047
+ refreshTaskContext(ctx);
2048
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`removed ${result.list} #${result.index}`)} ${result.task.text}\n`);
2049
+ } catch (err) {
2050
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2051
+ process.stderr.write(` ${c.gray('Usage: /tasks remove <active|backlog|blocked|done> <number>')}\n`);
2052
+ }
2053
+ return;
2054
+ }
2055
+
2056
+ if (verb === 'finish' || verb === 'complete' || verb === 'block' || verb === 'unblock') {
2057
+ try {
2058
+ const [from, index, ...textParts] = parts;
2059
+ const to = verb === 'block' ? 'blocked' : verb === 'unblock' ? 'active' : 'done';
2060
+ const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
2061
+ refreshTaskContext(ctx);
2062
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
2063
+ } catch (err) {
2064
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2065
+ process.stderr.write(` ${c.gray(`Usage: /tasks ${verb} <active|backlog|blocked|done> <number> [new text]`)}\n`);
2066
+ }
2067
+ return;
2068
+ }
2069
+
2001
2070
  let list = 'backlog';
2002
2071
  if (verb === 'add' || verb === 'new') {
2003
2072
  const maybeList = (parts[0] || '').toLowerCase();
@@ -2192,6 +2261,15 @@ async function handleCommand(input, ctx) {
2192
2261
  }
2193
2262
  process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
2194
2263
 
2264
+ // Cache — PRD-071 §1.2. Only surface when we have data; a fresh session
2265
+ // shows nothing rather than a misleading "0%".
2266
+ const cache = computeCacheTotals();
2267
+ if (cache.read > 0 || cache.write > 0) {
2268
+ const readLabel = formatTokens(cache.read);
2269
+ const writeLabel = formatTokens(cache.write);
2270
+ process.stderr.write(` ${c.dim('Cache')} ${cache.hitRate}% hit ${c.dim('·')} ${readLabel} read ${c.dim('·')} ${writeLabel} write\n`);
2271
+ }
2272
+
2195
2273
  // Permissions
2196
2274
  process.stderr.write(`\n ${c.bold('Permissions')}\n`);
2197
2275
  process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
@@ -3358,6 +3436,7 @@ export async function startTerminalRepl() {
3358
3436
  if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
3359
3437
  hookRunner.reload();
3360
3438
  latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
3439
+ const projectResources = toolExecutor.getProjectResources();
3361
3440
  const promptHook = await hookRunner.run('UserPromptSubmit', {
3362
3441
  input: { prompt: input },
3363
3442
  turnId: String(session.turns),
@@ -3379,7 +3458,7 @@ export async function startTerminalRepl() {
3379
3458
  effectivePolicy,
3380
3459
  projectContext: latestProjectContext,
3381
3460
  activeHints: [...hookHints, ...rejectionHints],
3382
- projectResources: toolExecutor.getProjectResources(),
3461
+ projectResources,
3383
3462
  agentContext: toolExecutor.getAgentContext(),
3384
3463
  });
3385
3464
  ctx.effectivePolicy = effectivePolicy;
@@ -3387,6 +3466,14 @@ export async function startTerminalRepl() {
3387
3466
  ctx.latestEnvelope = latestEnvelope;
3388
3467
  Object.assign(execContext, latestEnvelope);
3389
3468
  if (skipPerms) execContext.freeswim = true;
3469
+ // PRD-071: seed work_scope from CLI so the backend has a byte-stable
3470
+ // scope block from turn 1. Uses projectResources already gathered by
3471
+ // the envelope above.
3472
+ execContext.work_scope = buildWorkScope({
3473
+ instruction: input,
3474
+ cwd: safeCwd(),
3475
+ projectResources,
3476
+ });
3390
3477
  for (const file of latestProjectContext.changed || []) {
3391
3478
  if (effectivePolicy.policy.context?.showReloadNotice) {
3392
3479
  process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
@@ -7,12 +7,10 @@
7
7
 
8
8
  import { SessionManager } from '../core/session.mjs';
9
9
  import { CheckpointManager } from '../core/checkpoints.mjs';
10
- import { PromptCache } from '../core/cache.mjs';
11
10
  import { readEnv, listEnvVars } from '../config/env.mjs';
12
11
  import * as telemetry from '../telemetry/index.mjs';
13
12
 
14
13
  const checkpoints = new CheckpointManager();
15
- const promptCache = new PromptCache();
16
14
  let sessionManager = null;
17
15
 
18
16
  function getSession() {
@@ -399,11 +397,14 @@ export const COMMANDS = {
399
397
  '/extra-usage': {
400
398
  description: 'Show detailed usage stats',
401
399
  handler(args, state) {
402
- const cacheStats = promptCache.getStats();
403
400
  const telemetryStats = telemetry.getStats();
401
+ const cacheRead = state.tokenUsage.cache_read || 0;
402
+ const cacheWrite = state.tokenUsage.cache_creation || 0;
403
+ const denom = state.tokenUsage.input + cacheRead;
404
+ const rate = denom > 0 ? Math.round((cacheRead / denom) * 100) : 0;
404
405
  return [
405
406
  `Tokens: in=${state.tokenUsage.input}, out=${state.tokenUsage.output}`,
406
- `Cache: hits=${cacheStats.cacheHits}, misses=${cacheStats.cacheMisses}, rate=${cacheStats.hitRate}`,
407
+ `Cache: read=${cacheRead}, write=${cacheWrite}, hit-rate=${rate}%`,
407
408
  `Telemetry: ${telemetryStats.totalEvents} events`,
408
409
  ].join('\n');
409
410
  },