icopilot 2.2.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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { config } from '../config.js';
|
|
2
|
+
import { renderGitContextBlock } from '../context/git-context.js';
|
|
3
|
+
import { PinnedContext } from '../context/pinned.js';
|
|
4
|
+
import { PLAN_SYSTEM, getAskSystemPrompt } from './prompts.js';
|
|
5
|
+
import { loadMemoryBlock } from '../context/memory.js';
|
|
6
|
+
import { loadConventionPromptContext } from '../knowledge/conventions.js';
|
|
7
|
+
import { loadStylePromptContext } from '../knowledge/style-learner.js';
|
|
8
|
+
import { theme } from '../ui/theme.js';
|
|
9
|
+
import { countTokensSync } from '../util/tokens.js';
|
|
10
|
+
import { showContextUsage } from './context-viz-cmd.js';
|
|
11
|
+
const FILE_REF_HEADER = '### Referenced files';
|
|
12
|
+
export function buildContextBreakdown(session) {
|
|
13
|
+
const systemPrompt = session.state.systemPrompt ??
|
|
14
|
+
(session.state.mode === 'plan' ? PLAN_SYSTEM : getAskSystemPrompt());
|
|
15
|
+
const memoryBlock = loadMemoryBlock(session.state.cwd) ?? '';
|
|
16
|
+
const styleBlock = loadStylePromptContext(session.state.cwd) ?? '';
|
|
17
|
+
const conventionBlock = loadConventionPromptContext(session.state.cwd) ?? '';
|
|
18
|
+
const pinnedBlock = PinnedContext.fromJSON(session.state.pinned).render();
|
|
19
|
+
const gitBlock = renderGitContextBlock(session.state.gitContext ?? []);
|
|
20
|
+
let historyTokens = 0;
|
|
21
|
+
let fileTokens = 0;
|
|
22
|
+
let toolTokens = 0;
|
|
23
|
+
for (const message of session.state.messages) {
|
|
24
|
+
const role = String(message.role || 'message');
|
|
25
|
+
const content = contentToText(message.content);
|
|
26
|
+
if (role === 'user') {
|
|
27
|
+
const { mainText, refText } = splitFileRefs(content);
|
|
28
|
+
historyTokens += safeCountTokens(mainText);
|
|
29
|
+
fileTokens += safeCountTokens(refText);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (role === 'assistant') {
|
|
33
|
+
historyTokens += safeCountTokens(content);
|
|
34
|
+
toolTokens += safeCountTokens(toolCallsToText(message.tool_calls));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (role === 'tool') {
|
|
38
|
+
toolTokens += safeCountTokens(content);
|
|
39
|
+
toolTokens += safeCountTokens(typeof message.tool_call_id === 'string'
|
|
40
|
+
? message.tool_call_id
|
|
41
|
+
: '');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
historyTokens += safeCountTokens(content);
|
|
45
|
+
}
|
|
46
|
+
const sources = [
|
|
47
|
+
source('System prompt', 'system', safeCountTokens(systemPrompt)),
|
|
48
|
+
source('Style profile', 'system', safeCountTokens(styleBlock)),
|
|
49
|
+
source('Conventions', 'system', safeCountTokens(conventionBlock)),
|
|
50
|
+
source('File references', 'file', fileTokens),
|
|
51
|
+
source('Memory', 'memory', safeCountTokens(memoryBlock)),
|
|
52
|
+
source('Pinned files', 'pinned', safeCountTokens(pinnedBlock)),
|
|
53
|
+
source('Git context', 'git', safeCountTokens(gitBlock)),
|
|
54
|
+
source('Conversation history', 'history', historyTokens),
|
|
55
|
+
source('Tool results', 'history', toolTokens),
|
|
56
|
+
];
|
|
57
|
+
const total = sources.reduce((sum, entry) => sum + entry.tokens, 0);
|
|
58
|
+
for (const entry of sources) {
|
|
59
|
+
entry.percentage = total > 0 ? (entry.tokens / total) * 100 : 0;
|
|
60
|
+
}
|
|
61
|
+
const budget = config.contextWindow;
|
|
62
|
+
return {
|
|
63
|
+
sources,
|
|
64
|
+
total,
|
|
65
|
+
budget,
|
|
66
|
+
remaining: Math.max(0, budget - total),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function renderContextBreakdown(breakdown) {
|
|
70
|
+
const usedPct = breakdown.budget > 0 ? (breakdown.total / breakdown.budget) * 100 : 0;
|
|
71
|
+
const status = usedPct >= 100
|
|
72
|
+
? theme.err('over budget')
|
|
73
|
+
: usedPct >= config.contextWarn * 100
|
|
74
|
+
? theme.warn('approaching budget')
|
|
75
|
+
: theme.ok('healthy');
|
|
76
|
+
const lines = [
|
|
77
|
+
theme.brand('Context hub'),
|
|
78
|
+
` used: ${theme.hl(String(breakdown.total))} tk / ${theme.hl(String(breakdown.budget))} tk`,
|
|
79
|
+
` budget: ${renderBudgetBar(breakdown.total, breakdown.budget, 28)} ${theme.dim(`(${usedPct.toFixed(1)}%)`)}`,
|
|
80
|
+
` remaining: ${breakdown.remaining > 0 ? theme.ok(String(breakdown.remaining)) : theme.err(String(breakdown.remaining))} tk`,
|
|
81
|
+
` status: ${status}`,
|
|
82
|
+
'',
|
|
83
|
+
theme.brand('Sources'),
|
|
84
|
+
...breakdown.sources.map((entry) => renderSourceLine(entry, breakdown.total)),
|
|
85
|
+
'',
|
|
86
|
+
];
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
export function contextCommand(args, session) {
|
|
90
|
+
const breakdown = buildContextBreakdown(session);
|
|
91
|
+
const subcommand = args[0]?.toLowerCase();
|
|
92
|
+
switch (subcommand) {
|
|
93
|
+
case undefined:
|
|
94
|
+
case '':
|
|
95
|
+
return showContextUsage(session);
|
|
96
|
+
case 'sources':
|
|
97
|
+
return renderSources(breakdown);
|
|
98
|
+
case 'budget':
|
|
99
|
+
return renderBudget(breakdown);
|
|
100
|
+
case 'trim':
|
|
101
|
+
return renderTrimSuggestions(breakdown);
|
|
102
|
+
default:
|
|
103
|
+
return `${theme.warn(`unknown /context subcommand: ${subcommand}`)}\n${theme.dim('usage: /context [sources|budget|trim]')}\n`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function renderSources(breakdown) {
|
|
107
|
+
return [
|
|
108
|
+
theme.brand('Context sources'),
|
|
109
|
+
...breakdown.sources.map((entry) => ` ${colorForType(entry.type)(entry.name.padEnd(22))} ${theme.hl(String(entry.tokens).padStart(6))} tk ${theme.dim(`${entry.percentage.toFixed(1).padStart(5)}%`)} ${theme.dim(`(${entry.type})`)}`),
|
|
110
|
+
'',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
function renderBudget(breakdown) {
|
|
114
|
+
const usedPct = breakdown.budget > 0 ? (breakdown.total / breakdown.budget) * 100 : 0;
|
|
115
|
+
return [
|
|
116
|
+
theme.brand('Context budget'),
|
|
117
|
+
` ${renderBudgetBar(breakdown.total, breakdown.budget, 36)} ${theme.dim(`(${usedPct.toFixed(1)}% used)`)}`,
|
|
118
|
+
` remaining: ${breakdown.remaining > 0 ? theme.ok(String(breakdown.remaining)) : theme.err(String(breakdown.remaining))} tk`,
|
|
119
|
+
'',
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
function renderTrimSuggestions(breakdown) {
|
|
123
|
+
const candidates = breakdown.sources
|
|
124
|
+
.filter((entry) => entry.tokens > 0 && entry.name !== 'System prompt')
|
|
125
|
+
.sort((left, right) => right.tokens - left.tokens || left.name.localeCompare(right.name));
|
|
126
|
+
if (!candidates.length) {
|
|
127
|
+
return [theme.brand('Trim suggestions'), ` ${theme.dim('No context loaded yet.')}`, ''].join('\n');
|
|
128
|
+
}
|
|
129
|
+
return [
|
|
130
|
+
theme.brand('Trim suggestions'),
|
|
131
|
+
...candidates.map((entry, index) => {
|
|
132
|
+
const suggestion = trimSuggestion(entry);
|
|
133
|
+
return ` ${index + 1}. ${colorForType(entry.type)(entry.name)} ${theme.hl(`(${entry.tokens} tk)`)} — ${suggestion}`;
|
|
134
|
+
}),
|
|
135
|
+
'',
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
function renderSourceLine(entry, total) {
|
|
139
|
+
const label = colorForType(entry.type)(entry.name.padEnd(22));
|
|
140
|
+
return ` ${label} ${theme.hl(String(entry.tokens).padStart(6))} tk ${renderShareBar(entry.tokens, total, 18)} ${theme.dim(`${entry.percentage.toFixed(1).padStart(5)}%`)}`;
|
|
141
|
+
}
|
|
142
|
+
function renderBudgetBar(used, total, width) {
|
|
143
|
+
const ratio = total <= 0 ? 0 : Math.max(0, Math.min(1, used / total));
|
|
144
|
+
const fill = Math.round(ratio * width);
|
|
145
|
+
const rawBar = '█'.repeat(fill) + '░'.repeat(Math.max(0, width - fill));
|
|
146
|
+
const colored = ratio >= 1
|
|
147
|
+
? theme.err(rawBar)
|
|
148
|
+
: ratio >= config.contextWarn
|
|
149
|
+
? theme.warn(rawBar)
|
|
150
|
+
: theme.ok(rawBar);
|
|
151
|
+
return `[${colored}]`;
|
|
152
|
+
}
|
|
153
|
+
function renderShareBar(used, total, width) {
|
|
154
|
+
const ratio = total <= 0 ? 0 : Math.max(0, Math.min(1, used / total));
|
|
155
|
+
const fill = Math.round(ratio * width);
|
|
156
|
+
return `[${'█'.repeat(fill)}${'░'.repeat(Math.max(0, width - fill))}]`;
|
|
157
|
+
}
|
|
158
|
+
function trimSuggestion(entry) {
|
|
159
|
+
switch (entry.name) {
|
|
160
|
+
case 'Conversation history':
|
|
161
|
+
return 'run /compact or clear older turns';
|
|
162
|
+
case 'File references':
|
|
163
|
+
return 'remove large @file injections from the next prompt';
|
|
164
|
+
case 'Pinned files':
|
|
165
|
+
return 'unpin files you no longer need';
|
|
166
|
+
case 'Git context':
|
|
167
|
+
return 'trim the auto-injected git diff context';
|
|
168
|
+
case 'Tool results':
|
|
169
|
+
return 'compact the session to collapse verbose tool output';
|
|
170
|
+
case 'Memory':
|
|
171
|
+
return 'trim .icopilot/memory.md or .icopilot/team-memory.md to the essentials';
|
|
172
|
+
case 'System prompt':
|
|
173
|
+
return 'fixed overhead; optimize other sources first';
|
|
174
|
+
default:
|
|
175
|
+
return 'trim this source first';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function colorForType(type) {
|
|
179
|
+
switch (type) {
|
|
180
|
+
case 'system':
|
|
181
|
+
return theme.brand;
|
|
182
|
+
case 'file':
|
|
183
|
+
return theme.hl;
|
|
184
|
+
case 'memory':
|
|
185
|
+
return theme.ok;
|
|
186
|
+
case 'pinned':
|
|
187
|
+
return theme.warn;
|
|
188
|
+
case 'git':
|
|
189
|
+
return theme.assistant;
|
|
190
|
+
case 'skill':
|
|
191
|
+
return theme.assistant;
|
|
192
|
+
case 'history':
|
|
193
|
+
default:
|
|
194
|
+
return theme.dim;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function source(name, type, tokens) {
|
|
198
|
+
return { name, type, tokens, percentage: 0 };
|
|
199
|
+
}
|
|
200
|
+
function splitFileRefs(text) {
|
|
201
|
+
if (!text)
|
|
202
|
+
return { mainText: '', refText: '' };
|
|
203
|
+
const markerIndex = text.indexOf(FILE_REF_HEADER);
|
|
204
|
+
if (markerIndex === -1)
|
|
205
|
+
return { mainText: text, refText: '' };
|
|
206
|
+
return {
|
|
207
|
+
mainText: text.slice(0, markerIndex).trimEnd(),
|
|
208
|
+
refText: text.slice(markerIndex),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function toolCallsToText(toolCalls) {
|
|
212
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0)
|
|
213
|
+
return '';
|
|
214
|
+
return JSON.stringify(toolCalls);
|
|
215
|
+
}
|
|
216
|
+
function contentToText(content) {
|
|
217
|
+
if (typeof content === 'string')
|
|
218
|
+
return content;
|
|
219
|
+
if (Array.isArray(content)) {
|
|
220
|
+
return content
|
|
221
|
+
.map((part) => {
|
|
222
|
+
if (!part || typeof part !== 'object')
|
|
223
|
+
return '';
|
|
224
|
+
const record = part;
|
|
225
|
+
if (typeof record.text === 'string')
|
|
226
|
+
return record.text;
|
|
227
|
+
if (typeof record.type === 'string')
|
|
228
|
+
return JSON.stringify(record);
|
|
229
|
+
return '';
|
|
230
|
+
})
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join('\n');
|
|
233
|
+
}
|
|
234
|
+
if (content == null)
|
|
235
|
+
return '';
|
|
236
|
+
return JSON.stringify(content, null, 2);
|
|
237
|
+
}
|
|
238
|
+
function safeCountTokens(text) {
|
|
239
|
+
if (!text)
|
|
240
|
+
return 0;
|
|
241
|
+
try {
|
|
242
|
+
return countTokensSync(text);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return Math.ceil(text.length / 4);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { theme } from '../ui/theme.js';
|
|
2
|
+
import { buildContextBreakdown } from './context-cmd.js';
|
|
3
|
+
export function showContextUsage(session) {
|
|
4
|
+
const breakdown = buildContextBreakdown(session);
|
|
5
|
+
const buckets = summarizeContextUsage(breakdown);
|
|
6
|
+
const usedPct = breakdown.budget > 0 ? Math.round((breakdown.total / breakdown.budget) * 100) : 0;
|
|
7
|
+
const lines = [
|
|
8
|
+
theme.brand('Context usage'),
|
|
9
|
+
` ${renderProgressBar(breakdown.total, breakdown.budget, 16)} ${usedPct}% (${formatTokenAmount(breakdown.total)} / ${formatTokenAmount(breakdown.budget)} tokens)`,
|
|
10
|
+
` ${buckets.map((bucket) => `${bucket.label}: ${formatTokenAmount(bucket.tokens)}`).join(' | ')}`,
|
|
11
|
+
'',
|
|
12
|
+
theme.brand('Breakdown'),
|
|
13
|
+
...buckets.map((bucket) => ` ${bucket.label.padEnd(7)} ${formatTokenAmount(bucket.tokens).padStart(6)} ${theme.dim(bucket.details)}`),
|
|
14
|
+
'',
|
|
15
|
+
];
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
export function summarizeContextUsage(breakdown) {
|
|
19
|
+
const systemTokens = sumByType(breakdown.sources, ['system', 'memory']);
|
|
20
|
+
const historyTokens = sumByType(breakdown.sources, ['history']);
|
|
21
|
+
const fileTokens = sumByType(breakdown.sources, ['file', 'pinned', 'git', 'skill']);
|
|
22
|
+
return [
|
|
23
|
+
{ label: 'System', tokens: systemTokens, details: 'prompt, style, conventions, memory' },
|
|
24
|
+
{ label: 'History', tokens: historyTokens, details: 'conversation turns and tool output' },
|
|
25
|
+
{ label: 'Files', tokens: fileTokens, details: 'referenced, pinned, and git context' },
|
|
26
|
+
{ label: 'Free', tokens: breakdown.remaining, details: 'remaining context budget' },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function sumByType(sources, types) {
|
|
30
|
+
return sources.reduce((sum, source) => (types.includes(source.type) ? sum + source.tokens : sum), 0);
|
|
31
|
+
}
|
|
32
|
+
function renderProgressBar(used, total, width) {
|
|
33
|
+
const ratio = total <= 0 ? 0 : Math.max(0, Math.min(1, used / total));
|
|
34
|
+
const fill = Math.round(ratio * width);
|
|
35
|
+
return `[${'█'.repeat(fill)}${'░'.repeat(Math.max(0, width - fill))}]`;
|
|
36
|
+
}
|
|
37
|
+
function formatTokenAmount(tokens) {
|
|
38
|
+
if (tokens >= 1000) {
|
|
39
|
+
const compact = (tokens / 1000).toFixed(tokens >= 10_000 ? 0 : 1);
|
|
40
|
+
return `${compact.replace(/\.0$/, '')}k`;
|
|
41
|
+
}
|
|
42
|
+
return `${tokens}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ConventionManager, listConventionFiles, resolveConventionPath, } from '../knowledge/conventions.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
export function conventionsCommand(args, cwd) {
|
|
6
|
+
const manager = new ConventionManager();
|
|
7
|
+
const [rawSubcommand = 'list', ...rest] = args;
|
|
8
|
+
const subcommand = rawSubcommand.toLowerCase();
|
|
9
|
+
try {
|
|
10
|
+
switch (subcommand) {
|
|
11
|
+
case 'list':
|
|
12
|
+
case 'show':
|
|
13
|
+
case 'current':
|
|
14
|
+
return listConventions(manager, cwd);
|
|
15
|
+
case 'detect':
|
|
16
|
+
return detectConventions(manager, cwd);
|
|
17
|
+
case 'check':
|
|
18
|
+
return checkConventions(manager, cwd, rest);
|
|
19
|
+
case 'add':
|
|
20
|
+
return addConvention(manager, cwd, rest);
|
|
21
|
+
default:
|
|
22
|
+
return usage();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return theme.err(`conventions: ${error.message}\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function listConventions(manager, cwd) {
|
|
30
|
+
const set = manager.load(cwd);
|
|
31
|
+
if (set.conventions.length === 0) {
|
|
32
|
+
return `${theme.brand('Conventions')}\n ${theme.dim('No saved conventions. Run /conventions detect or /conventions add first.')}\n`;
|
|
33
|
+
}
|
|
34
|
+
return `${theme.brand(`Conventions ${theme.dim(`(${set.name})`)}`)}\n${set.conventions
|
|
35
|
+
.map((convention) => ` ${theme.hl(convention.name)} ${theme.dim(`[${convention.severity}]`)}\n ${convention.rule}${convention.example ? ` ${theme.dim(`e.g. ${convention.example}`)}` : ''}`)
|
|
36
|
+
.join('\n')}\n`;
|
|
37
|
+
}
|
|
38
|
+
function detectConventions(manager, cwd) {
|
|
39
|
+
manager.load(cwd);
|
|
40
|
+
const detected = manager.detect(cwd);
|
|
41
|
+
if (detected.length === 0) {
|
|
42
|
+
return `${theme.brand('Conventions')}\n ${theme.dim('No source patterns detected.')}\n`;
|
|
43
|
+
}
|
|
44
|
+
for (const convention of detected) {
|
|
45
|
+
manager.add(convention);
|
|
46
|
+
}
|
|
47
|
+
manager.save(cwd);
|
|
48
|
+
return [
|
|
49
|
+
`${theme.ok('✔ detected conventions')} ${theme.dim(`(${detected.length})`)} ${theme.hl(resolveConventionPath(cwd))}`,
|
|
50
|
+
...detected.map((convention) => ` - ${theme.hl(convention.name)} ${theme.dim(`[${convention.severity}]`)} — ${convention.rule}`),
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
function checkConventions(manager, cwd, args) {
|
|
55
|
+
const set = manager.load(cwd);
|
|
56
|
+
if (set.conventions.length === 0) {
|
|
57
|
+
const detected = manager.detect(cwd);
|
|
58
|
+
for (const convention of detected) {
|
|
59
|
+
manager.add(convention);
|
|
60
|
+
}
|
|
61
|
+
if (manager.getConventionSet().conventions.length === 0) {
|
|
62
|
+
return `${theme.brand('Convention check')}\n ${theme.dim('No conventions available. Run /conventions detect or /conventions add first.')}\n`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const target = args.join(' ').trim();
|
|
66
|
+
const files = target ? [resolveTargetFile(cwd, target)] : listConventionFiles(cwd);
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
return `${theme.brand('Convention check')}\n ${theme.dim('No files found to check.')}\n`;
|
|
69
|
+
}
|
|
70
|
+
const violations = files.flatMap((file) => {
|
|
71
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
72
|
+
return manager.check(code).map((violation) => ({
|
|
73
|
+
...violation,
|
|
74
|
+
file,
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
if (violations.length === 0) {
|
|
78
|
+
return `${theme.ok('✔ no convention violations found')}${target ? ` ${theme.dim(path.relative(cwd, files[0] ?? target))}` : ''}\n`;
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
`${theme.warn(`Found ${violations.length} convention violation${violations.length === 1 ? '' : 's'}.`)}`,
|
|
82
|
+
...violations.map((violation) => formatViolation(cwd, violation)),
|
|
83
|
+
'',
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
function addConvention(manager, cwd, args) {
|
|
87
|
+
const [name, ...ruleParts] = args;
|
|
88
|
+
const rule = ruleParts.join(' ').trim();
|
|
89
|
+
if (!name || !rule)
|
|
90
|
+
return theme.warn('usage: /conventions add <name> <rule>\n');
|
|
91
|
+
manager.load(cwd);
|
|
92
|
+
manager.add({
|
|
93
|
+
id: name,
|
|
94
|
+
name,
|
|
95
|
+
description: `User-defined convention: ${name}`,
|
|
96
|
+
rule,
|
|
97
|
+
severity: 'recommended',
|
|
98
|
+
});
|
|
99
|
+
manager.save(cwd);
|
|
100
|
+
return `${theme.ok('✔ saved convention')} ${theme.hl(name)} ${theme.dim('→')} ${rule}\n`;
|
|
101
|
+
}
|
|
102
|
+
function resolveTargetFile(cwd, target) {
|
|
103
|
+
const resolved = path.resolve(cwd, target);
|
|
104
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
105
|
+
throw new Error(`file not found: ${resolved}`);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
function formatViolation(cwd, violation) {
|
|
110
|
+
const relativePath = violation.file ? path.relative(cwd, violation.file) : '(input)';
|
|
111
|
+
const location = violation.line ? `${relativePath}:${violation.line}` : relativePath;
|
|
112
|
+
return ` ${theme.hl(location)} ${theme.dim(`(${violation.convention.name})`)} ${violation.description}`;
|
|
113
|
+
}
|
|
114
|
+
function usage() {
|
|
115
|
+
return theme.warn('usage: /conventions [list|detect|check [file]|add <name> <rule>]\n');
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { theme } from '../ui/theme.js';
|
|
2
|
+
import { countTokensSync } from '../util/tokens.js';
|
|
3
|
+
import { estimateCost, formatUsd, getRate } from '../util/cost.js';
|
|
4
|
+
export function costCommand(session) {
|
|
5
|
+
let inputTokens = 0;
|
|
6
|
+
let outputTokens = 0;
|
|
7
|
+
for (const message of session.state.messages) {
|
|
8
|
+
const text = contentToText(message.content);
|
|
9
|
+
if (!text)
|
|
10
|
+
continue;
|
|
11
|
+
const count = countTokensSync(text);
|
|
12
|
+
if (message.role === 'assistant') {
|
|
13
|
+
outputTokens += count;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
inputTokens += count;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const model = session.state.model;
|
|
20
|
+
const rate = getRate(model);
|
|
21
|
+
const cost = estimateCost(model, inputTokens, outputTokens);
|
|
22
|
+
return ([
|
|
23
|
+
theme.brand('Cost estimate'),
|
|
24
|
+
` model: ${theme.hl(model)}`,
|
|
25
|
+
` input tokens: ${theme.hl(String(inputTokens))}`,
|
|
26
|
+
` output tokens: ${theme.hl(String(outputTokens))}`,
|
|
27
|
+
` estimated USD: ${theme.ok(formatUsd(cost))}`,
|
|
28
|
+
` rate used: ${formatUsd(rate.input)} / ${formatUsd(rate.output)} per 1K input/output tokens`,
|
|
29
|
+
].join('\n') + '\n');
|
|
30
|
+
}
|
|
31
|
+
function contentToText(content) {
|
|
32
|
+
if (typeof content === 'string')
|
|
33
|
+
return content;
|
|
34
|
+
if (Array.isArray(content)) {
|
|
35
|
+
return content
|
|
36
|
+
.map((part) => {
|
|
37
|
+
if (typeof part !== 'object' || part === null)
|
|
38
|
+
return '';
|
|
39
|
+
if ('text' in part && typeof part.text === 'string')
|
|
40
|
+
return part.text;
|
|
41
|
+
if ('type' in part && typeof part.type === 'string')
|
|
42
|
+
return JSON.stringify(part);
|
|
43
|
+
return '';
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join('\n');
|
|
47
|
+
}
|
|
48
|
+
if (content == null)
|
|
49
|
+
return '';
|
|
50
|
+
return JSON.stringify(content);
|
|
51
|
+
}
|