@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.
- package/KEPLER-README.md +157 -142
- package/README.md +37 -65
- package/package.json +2 -2
- package/src/config/cli-args.mjs +14 -0
- package/src/core/agent-loop.mjs +8 -2
- package/src/core/cache-control.mjs +92 -0
- package/src/core/headless.mjs +85 -9
- package/src/core/local-agent.mjs +121 -14
- package/src/core/tasks.mjs +67 -1
- package/src/core/work-scope.mjs +217 -0
- package/src/terminal/main.mjs +2 -0
- package/src/terminal/repl.mjs +91 -4
- package/src/ui/commands.mjs +5 -4
|
@@ -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
|
+
}
|
package/src/terminal/main.mjs
CHANGED
package/src/terminal/repl.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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`);
|
package/src/ui/commands.mjs
CHANGED
|
@@ -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:
|
|
407
|
+
`Cache: read=${cacheRead}, write=${cacheWrite}, hit-rate=${rate}%`,
|
|
407
408
|
`Telemetry: ${telemetryStats.totalEvents} events`,
|
|
408
409
|
].join('\n');
|
|
409
410
|
},
|