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.
Files changed (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { theme } from '../ui/theme.js';
4
+ function readText(filePath) {
5
+ try {
6
+ return fs.readFileSync(filePath, 'utf8');
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ function readPackageJson(cwd) {
13
+ const text = readText(path.join(cwd, 'package.json'));
14
+ if (!text)
15
+ return undefined;
16
+ try {
17
+ return JSON.parse(text);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function fileExists(cwd, fileName) {
24
+ return fs.existsSync(path.join(cwd, fileName));
25
+ }
26
+ function hasAnyFile(cwd, fileNames) {
27
+ return fileNames.some((fileName) => fileExists(cwd, fileName));
28
+ }
29
+ function directoryHasFilePrefix(cwd, prefix) {
30
+ try {
31
+ return fs.readdirSync(cwd).some((entry) => entry.startsWith(prefix));
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function fileContains(cwd, fileName, needle) {
38
+ return readText(path.join(cwd, fileName))?.includes(needle) ?? false;
39
+ }
40
+ function hasGoTestFiles(dir) {
41
+ try {
42
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
43
+ const fullPath = path.join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ if (entry.name === '.git' || entry.name === 'node_modules')
46
+ continue;
47
+ if (hasGoTestFiles(fullPath))
48
+ return true;
49
+ continue;
50
+ }
51
+ if (entry.isFile() && entry.name.endsWith('_test.go'))
52
+ return true;
53
+ }
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ return false;
59
+ }
60
+ export function detectTestFrameworks(cwd) {
61
+ const matches = [];
62
+ const pkg = readPackageJson(cwd);
63
+ if (typeof pkg?.scripts?.test === 'string') {
64
+ matches.push({
65
+ name: 'npm-test',
66
+ command: 'npm test',
67
+ reason: 'package.json has "scripts.test"',
68
+ });
69
+ }
70
+ if (directoryHasFilePrefix(cwd, 'vitest.config.')) {
71
+ matches.push({
72
+ name: 'vitest',
73
+ command: 'npx vitest run',
74
+ reason: 'vitest.config.* is present',
75
+ });
76
+ }
77
+ if (directoryHasFilePrefix(cwd, 'jest.config.')) {
78
+ matches.push({
79
+ name: 'jest',
80
+ command: 'npx jest',
81
+ reason: 'jest.config.* is present',
82
+ });
83
+ }
84
+ if (pkg?.devDependencies?.mocha) {
85
+ matches.push({
86
+ name: 'mocha',
87
+ command: 'npx mocha',
88
+ reason: 'package.json has mocha in devDependencies',
89
+ });
90
+ }
91
+ if (fileExists(cwd, 'pytest.ini') || fileContains(cwd, 'pyproject.toml', '[tool.pytest]')) {
92
+ matches.push({
93
+ name: 'pytest',
94
+ command: 'pytest',
95
+ reason: fileExists(cwd, 'pytest.ini')
96
+ ? 'pytest.ini is present'
97
+ : 'pyproject.toml contains [tool.pytest]',
98
+ });
99
+ }
100
+ if (fileExists(cwd, 'Cargo.toml')) {
101
+ matches.push({
102
+ name: 'cargo-test',
103
+ command: 'cargo test',
104
+ reason: 'Cargo.toml is present',
105
+ });
106
+ }
107
+ if (fileExists(cwd, 'go.mod')) {
108
+ matches.push({
109
+ name: 'go-test',
110
+ command: 'go test ./...',
111
+ reason: 'go.mod is present',
112
+ });
113
+ }
114
+ else if (hasGoTestFiles(cwd)) {
115
+ matches.push({
116
+ name: 'go-test-files',
117
+ command: 'go test ./...',
118
+ reason: 'found *_test.go files',
119
+ });
120
+ }
121
+ if (hasAnyFile(cwd, ['phpunit.xml', 'phpunit.xml.dist'])) {
122
+ matches.push({
123
+ name: 'phpunit',
124
+ command: 'vendor/bin/phpunit',
125
+ reason: fileExists(cwd, 'phpunit.xml')
126
+ ? 'phpunit.xml is present'
127
+ : 'phpunit.xml.dist is present',
128
+ });
129
+ }
130
+ if (fileExists(cwd, '.rspec') || fileContains(cwd, 'Gemfile', 'rspec')) {
131
+ matches.push({
132
+ name: 'rspec',
133
+ command: 'bundle exec rspec',
134
+ reason: fileExists(cwd, '.rspec') ? '.rspec is present' : 'Gemfile mentions rspec',
135
+ });
136
+ }
137
+ return matches;
138
+ }
139
+ export function testCommand(cwd) {
140
+ const matches = detectTestFrameworks(cwd);
141
+ if (matches.length === 0) {
142
+ return `${theme.warn('No supported test frameworks detected.')}\n${theme.dim('Add a test script or framework configuration, then try /test again.')}\n`;
143
+ }
144
+ const lines = matches.map((match) => ` ${theme.ok(match.name)} ${theme.hl(match.command)} ${theme.dim(`(${match.reason})`)}`);
145
+ return `${theme.brand('Detected test frameworks')}\n${lines.join('\n')}\n`;
146
+ }
@@ -0,0 +1,172 @@
1
+ import crypto from 'node:crypto';
2
+ import { theme } from '../ui/theme.js';
3
+ export class TodoList {
4
+ items;
5
+ constructor(items = []) {
6
+ this.items = items.map(cloneTodo);
7
+ }
8
+ add(text) {
9
+ const item = {
10
+ id: crypto.randomUUID(),
11
+ text: text.trim(),
12
+ done: false,
13
+ createdAt: new Date().toISOString(),
14
+ };
15
+ this.items.push(item);
16
+ return cloneTodo(item);
17
+ }
18
+ complete(id) {
19
+ const item = this.items.find((candidate) => candidate.id === id);
20
+ if (!item)
21
+ return false;
22
+ item.done = true;
23
+ item.completedAt ??= new Date().toISOString();
24
+ return true;
25
+ }
26
+ uncomplete(id) {
27
+ const item = this.items.find((candidate) => candidate.id === id);
28
+ if (!item)
29
+ return false;
30
+ item.done = false;
31
+ delete item.completedAt;
32
+ return true;
33
+ }
34
+ remove(id) {
35
+ const index = this.items.findIndex((candidate) => candidate.id === id);
36
+ if (index < 0)
37
+ return false;
38
+ this.items.splice(index, 1);
39
+ return true;
40
+ }
41
+ list(filter = 'all') {
42
+ return this.items
43
+ .filter((item) => {
44
+ if (filter === 'pending')
45
+ return !item.done;
46
+ if (filter === 'done')
47
+ return item.done;
48
+ return true;
49
+ })
50
+ .map(cloneTodo);
51
+ }
52
+ clear() {
53
+ const count = this.items.length;
54
+ this.items = [];
55
+ return count;
56
+ }
57
+ toJSON() {
58
+ return this.items.map(cloneTodo);
59
+ }
60
+ static fromJSON(data) {
61
+ if (!Array.isArray(data))
62
+ return new TodoList();
63
+ const items = data.flatMap((item) => {
64
+ const parsed = parseTodoItem(item);
65
+ return parsed ? [parsed] : [];
66
+ });
67
+ return new TodoList(items);
68
+ }
69
+ }
70
+ export function todoCommand(args, todos) {
71
+ const [rawSubcommand = 'list', ...rest] = args;
72
+ const subcommand = rawSubcommand.toLowerCase();
73
+ if (subcommand === 'list') {
74
+ const filter = parseFilter(rest[0]);
75
+ return filter ? formatTodoList(todos, filter) : usage();
76
+ }
77
+ if (subcommand === 'add') {
78
+ const text = rest.join(' ').trim();
79
+ if (!text)
80
+ return usage();
81
+ const item = todos.add(text);
82
+ return `${theme.ok('Added')} ${formatMarker(item.done)} ${theme.hl(shortId(item.id))} ${item.text}\n`;
83
+ }
84
+ if (subcommand === 'done' || subcommand === 'undo' || subcommand === 'rm') {
85
+ const prefix = rest[0]?.trim();
86
+ if (!prefix)
87
+ return usage();
88
+ const match = findByPrefix(todos, prefix);
89
+ if (match.kind === 'missing') {
90
+ return `${theme.warn(`No todo matches "${prefix}".`)}\n`;
91
+ }
92
+ if (match.kind === 'ambiguous') {
93
+ const options = match.items.map((item) => shortId(item.id)).join(', ');
94
+ return `${theme.warn(`Multiple todos match "${prefix}": ${options}`)}\n`;
95
+ }
96
+ const item = match.item;
97
+ if (subcommand === 'done') {
98
+ todos.complete(item.id);
99
+ return `${theme.ok('Marked done')} ${formatMarker(true)} ${theme.hl(shortId(item.id))} ${item.text}\n`;
100
+ }
101
+ if (subcommand === 'undo') {
102
+ todos.uncomplete(item.id);
103
+ return `${theme.ok('Marked pending')} ${formatMarker(false)} ${theme.hl(shortId(item.id))} ${item.text}\n`;
104
+ }
105
+ todos.remove(item.id);
106
+ return `${theme.ok('Removed')} ${theme.hl(shortId(item.id))} ${item.text}\n`;
107
+ }
108
+ if (subcommand === 'clear') {
109
+ const count = todos.clear();
110
+ return `${theme.ok(`Cleared ${count} todo${count === 1 ? '' : 's'}.`)}\n`;
111
+ }
112
+ const filter = parseFilter(rawSubcommand);
113
+ return filter ? formatTodoList(todos, filter) : usage();
114
+ }
115
+ function formatTodoList(todos, filter = 'all') {
116
+ const items = todos.list(filter);
117
+ const suffix = filter === 'all' ? '' : ` ${theme.dim(`(${filter})`)}`;
118
+ const header = `${theme.brand('Todos')}${suffix}`;
119
+ if (items.length === 0)
120
+ return `${header}\n ${theme.dim('No todos.')}\n`;
121
+ const lines = items.map((item) => ` ${formatMarker(item.done)} ${theme.hl(shortId(item.id))} ${item.text}`);
122
+ return `${header}\n${lines.join('\n')}\n`;
123
+ }
124
+ function formatMarker(done) {
125
+ return done ? theme.ok('✓') : theme.dim('○');
126
+ }
127
+ function parseFilter(value) {
128
+ if (!value)
129
+ return 'all';
130
+ if (value === 'all' || value === 'pending' || value === 'done')
131
+ return value;
132
+ return undefined;
133
+ }
134
+ function shortId(id) {
135
+ return id.slice(0, 8);
136
+ }
137
+ function usage() {
138
+ return 'Usage: /todo [list [all|pending|done]] | /todo add <text> | /todo done <id-prefix> | /todo undo <id-prefix> | /todo rm <id-prefix> | /todo clear\n';
139
+ }
140
+ function cloneTodo(item) {
141
+ return { ...item };
142
+ }
143
+ function parseTodoItem(value) {
144
+ if (!value || typeof value !== 'object')
145
+ return null;
146
+ const item = value;
147
+ if (typeof item.id !== 'string' ||
148
+ typeof item.text !== 'string' ||
149
+ typeof item.done !== 'boolean' ||
150
+ typeof item.createdAt !== 'string') {
151
+ return null;
152
+ }
153
+ const parsed = {
154
+ id: item.id,
155
+ text: item.text,
156
+ done: item.done,
157
+ createdAt: item.createdAt,
158
+ };
159
+ if (typeof item.completedAt === 'string')
160
+ parsed.completedAt = item.completedAt;
161
+ return parsed;
162
+ }
163
+ function findByPrefix(todos, prefix) {
164
+ const matches = todos
165
+ .list('all')
166
+ .filter((item) => item.id === prefix || item.id.toLowerCase().startsWith(prefix.toLowerCase()));
167
+ if (matches.length === 0)
168
+ return { kind: 'missing' };
169
+ if (matches.length > 1)
170
+ return { kind: 'ambiguous', items: matches };
171
+ return { kind: 'match', item: matches[0] };
172
+ }
@@ -0,0 +1,277 @@
1
+ import { PLAN_SYSTEM, getAskSystemPrompt } from './prompts.js';
2
+ import { loadMemoryBlock } from '../context/memory.js';
3
+ import { loadConventionPromptContext } from '../knowledge/conventions.js';
4
+ import { loadStylePromptContext } from '../knowledge/style-learner.js';
5
+ import { theme } from '../ui/theme.js';
6
+ import { countTokensSync } from '../util/tokens.js';
7
+ import { config } from '../config.js';
8
+ const FILE_REF_HEADER = '### Referenced files';
9
+ export function tokensCommand(session) {
10
+ const baseSystemPrompt = session.state.mode === 'plan' ? PLAN_SYSTEM : getAskSystemPrompt();
11
+ const memoryBlock = loadMemoryBlock(session.state.cwd) ?? '';
12
+ const styleBlock = loadStylePromptContext(session.state.cwd) ?? '';
13
+ const conventionBlock = loadConventionPromptContext(session.state.cwd) ?? '';
14
+ const systemPrompt = segmentForText(baseSystemPrompt);
15
+ const stylePrompt = segmentForText(styleBlock);
16
+ const conventionPrompt = segmentForText(conventionBlock);
17
+ const memoryPrompt = segmentForText(memoryBlock);
18
+ const userMessages = { tokens: 0, bytes: 0 };
19
+ const assistantResponses = { tokens: 0, bytes: 0 };
20
+ const toolCalls = { tokens: 0, bytes: 0 };
21
+ const fileRefs = { tokens: 0, bytes: 0 };
22
+ const messageBreakdowns = [];
23
+ let fileRefBlocks = 0;
24
+ let assistantToolCalls = 0;
25
+ let toolMessages = 0;
26
+ for (const [index, message] of session.state.messages.entries()) {
27
+ const role = String(message.role || 'message');
28
+ const content = contentToText(message.content);
29
+ if (role === 'user') {
30
+ const { mainText, refText } = splitFileRefs(content);
31
+ const mainSegment = segmentForText(mainText);
32
+ const refSegment = segmentForText(refText);
33
+ userMessages.tokens += mainSegment.tokens;
34
+ userMessages.bytes += mainSegment.bytes;
35
+ fileRefs.tokens += refSegment.tokens;
36
+ fileRefs.bytes += refSegment.bytes;
37
+ if (refSegment.tokens > 0)
38
+ fileRefBlocks += 1;
39
+ messageBreakdowns.push({
40
+ index,
41
+ role,
42
+ tokens: mainSegment.tokens + refSegment.tokens,
43
+ bytes: mainSegment.bytes + refSegment.bytes,
44
+ details: refSegment.tokens > 0
45
+ ? `${mainSegment.tokens} user + ${refSegment.tokens} refs`
46
+ : `${mainSegment.tokens} user`,
47
+ });
48
+ continue;
49
+ }
50
+ if (role === 'assistant') {
51
+ const contentSegment = segmentForText(content);
52
+ assistantResponses.tokens += contentSegment.tokens;
53
+ assistantResponses.bytes += contentSegment.bytes;
54
+ const toolCallPayload = toolCallsToText(message.tool_calls);
55
+ const toolCallSegment = segmentForText(toolCallPayload);
56
+ toolCalls.tokens += toolCallSegment.tokens;
57
+ toolCalls.bytes += toolCallSegment.bytes;
58
+ if (toolCallSegment.tokens > 0)
59
+ assistantToolCalls += 1;
60
+ const totalTokens = contentSegment.tokens + toolCallSegment.tokens;
61
+ const totalBytes = contentSegment.bytes + toolCallSegment.bytes;
62
+ const detailParts = [];
63
+ if (contentSegment.tokens > 0)
64
+ detailParts.push(`${contentSegment.tokens} reply`);
65
+ if (toolCallSegment.tokens > 0)
66
+ detailParts.push(`${toolCallSegment.tokens} tool-call`);
67
+ messageBreakdowns.push({
68
+ index,
69
+ role,
70
+ tokens: totalTokens,
71
+ bytes: totalBytes,
72
+ details: detailParts.join(' + ') || undefined,
73
+ });
74
+ continue;
75
+ }
76
+ if (role === 'tool') {
77
+ const contentSegment = segmentForText(content);
78
+ const toolCallIdSegment = segmentForText(typeof message.tool_call_id === 'string'
79
+ ? message.tool_call_id
80
+ : '');
81
+ toolCalls.tokens += contentSegment.tokens + toolCallIdSegment.tokens;
82
+ toolCalls.bytes += contentSegment.bytes + toolCallIdSegment.bytes;
83
+ if (contentSegment.tokens > 0 || toolCallIdSegment.tokens > 0)
84
+ toolMessages += 1;
85
+ messageBreakdowns.push({
86
+ index,
87
+ role,
88
+ tokens: contentSegment.tokens + toolCallIdSegment.tokens,
89
+ bytes: contentSegment.bytes + toolCallIdSegment.bytes,
90
+ details: `${contentSegment.tokens} result`,
91
+ });
92
+ continue;
93
+ }
94
+ const segment = segmentForText(content);
95
+ if (role === 'system') {
96
+ systemPrompt.tokens += segment.tokens;
97
+ systemPrompt.bytes += segment.bytes;
98
+ }
99
+ messageBreakdowns.push({
100
+ index,
101
+ role,
102
+ tokens: segment.tokens,
103
+ bytes: segment.bytes,
104
+ details: role === 'system' ? `${segment.tokens} persisted system` : undefined,
105
+ });
106
+ }
107
+ const categories = [
108
+ {
109
+ category: 'System prompt',
110
+ tokens: systemPrompt.tokens,
111
+ percentage: 0,
112
+ details: session.state.mode === 'plan' ? 'plan mode prompt' : 'ask mode prompt',
113
+ bytes: systemPrompt.bytes,
114
+ },
115
+ {
116
+ category: 'Style profile',
117
+ tokens: stylePrompt.tokens,
118
+ percentage: 0,
119
+ details: stylePrompt.tokens > 0 ? 'loaded from .icopilot/style-profile.json' : 'none loaded',
120
+ bytes: stylePrompt.bytes,
121
+ },
122
+ {
123
+ category: 'Conventions',
124
+ tokens: conventionPrompt.tokens,
125
+ percentage: 0,
126
+ details: conventionPrompt.tokens > 0 ? 'loaded from .icopilot/conventions.yaml' : 'none loaded',
127
+ bytes: conventionPrompt.bytes,
128
+ },
129
+ {
130
+ category: 'User messages',
131
+ tokens: userMessages.tokens,
132
+ percentage: 0,
133
+ details: `${countByRole(session, 'user')} message(s)`,
134
+ bytes: userMessages.bytes,
135
+ },
136
+ {
137
+ category: 'Assistant responses',
138
+ tokens: assistantResponses.tokens,
139
+ percentage: 0,
140
+ details: `${countByRole(session, 'assistant')} message(s)`,
141
+ bytes: assistantResponses.bytes,
142
+ },
143
+ {
144
+ category: 'Tool calls',
145
+ tokens: toolCalls.tokens,
146
+ percentage: 0,
147
+ details: `${assistantToolCalls} assistant call(s), ${toolMessages} tool result(s)`,
148
+ bytes: toolCalls.bytes,
149
+ },
150
+ {
151
+ category: 'File references',
152
+ tokens: fileRefs.tokens,
153
+ percentage: 0,
154
+ details: `${fileRefBlocks} injected block(s)`,
155
+ bytes: fileRefs.bytes,
156
+ },
157
+ {
158
+ category: 'Memory block',
159
+ tokens: memoryPrompt.tokens,
160
+ percentage: 0,
161
+ details: memoryPrompt.tokens > 0
162
+ ? 'loaded from .icopilot memory files (including team memory)'
163
+ : 'none loaded',
164
+ bytes: memoryPrompt.bytes,
165
+ },
166
+ ];
167
+ const total = categories.reduce((sum, category) => sum + category.tokens, 0);
168
+ for (const category of categories) {
169
+ category.percentage = total > 0 ? (category.tokens / total) * 100 : 0;
170
+ }
171
+ const totalBytes = categories.reduce((sum, category) => sum + category.bytes, 0);
172
+ const budget = config.contextWindow;
173
+ const remaining = Math.max(0, budget - total);
174
+ const usedPct = budget > 0 ? (total / budget) * 100 : 0;
175
+ const largest = [...categories].sort((a, b) => b.tokens - a.tokens || a.category.localeCompare(b.category))[0];
176
+ const categoryLines = categories.map((category) => {
177
+ const name = category.category.padEnd(20);
178
+ const tokens = theme.hl(String(category.tokens).padStart(6));
179
+ const bytes = theme.dim(formatBytes(category.bytes).padStart(8));
180
+ const details = category.details ? ` ${theme.dim(`(${category.details})`)}` : '';
181
+ const badge = largest && largest.category === category.category && category.tokens > 0
182
+ ? ` ${theme.ok('← largest')}`
183
+ : '';
184
+ return ` ${name} ${tokens} tk ${renderTokenBar(category.tokens, total, 18)} ${bytes}${details}${badge}`;
185
+ });
186
+ const messageLines = messageBreakdowns.length > 0
187
+ ? messageBreakdowns.map((entry) => {
188
+ const label = `#${entry.index} ${entry.role}`.padEnd(16);
189
+ const details = entry.details ? ` ${theme.dim(`(${entry.details})`)}` : '';
190
+ return ` ${label} ${theme.hl(String(entry.tokens).padStart(6))} tk ${theme.dim(formatBytes(entry.bytes).padStart(8))}${details}`;
191
+ })
192
+ : [` ${theme.dim('No persisted messages.')}`];
193
+ return [
194
+ theme.brand('Token analysis'),
195
+ ` budget: ${theme.hl(String(budget))} tk`,
196
+ ` used: ${theme.hl(String(total))} tk ${renderTokenBar(total, budget, 24)} ${theme.dim(formatBytes(totalBytes))}`,
197
+ ` remaining: ${remaining > 0 ? theme.ok(String(remaining)) : theme.err(String(remaining))} tk`,
198
+ largest && largest.tokens > 0
199
+ ? ` largest: ${theme.warn(`${largest.category} (${largest.tokens} tk, ${largest.percentage.toFixed(1)}%)`)}`
200
+ : ` largest: ${theme.dim('none')}`,
201
+ usedPct >= 100
202
+ ? ` status: ${theme.err('over budget')}`
203
+ : usedPct >= config.contextWarn * 100
204
+ ? ` status: ${theme.warn('approaching budget')}`
205
+ : ` status: ${theme.ok('healthy')}`,
206
+ '',
207
+ theme.brand('By category'),
208
+ ...categoryLines,
209
+ '',
210
+ theme.brand('By message'),
211
+ ...messageLines,
212
+ '',
213
+ ].join('\n');
214
+ }
215
+ export function renderTokenBar(used, total, width) {
216
+ const safeWidth = Math.max(1, width);
217
+ const ratio = total <= 0 ? 0 : Math.max(0, Math.min(1, used / total));
218
+ const fill = Math.round(ratio * safeWidth);
219
+ const bar = '█'.repeat(fill) + '░'.repeat(Math.max(0, safeWidth - fill));
220
+ return `[${bar}] ${Math.round(ratio * 100)}%`;
221
+ }
222
+ function countByRole(session, role) {
223
+ return session.state.messages.filter((message) => String(message.role || '') === role).length;
224
+ }
225
+ function splitFileRefs(text) {
226
+ if (!text)
227
+ return { mainText: '', refText: '' };
228
+ const markerIndex = text.indexOf(FILE_REF_HEADER);
229
+ if (markerIndex === -1)
230
+ return { mainText: text, refText: '' };
231
+ return {
232
+ mainText: text.slice(0, markerIndex).trimEnd(),
233
+ refText: text.slice(markerIndex),
234
+ };
235
+ }
236
+ function segmentForText(text) {
237
+ if (!text)
238
+ return { tokens: 0, bytes: 0 };
239
+ return {
240
+ tokens: countTokensSync(text),
241
+ bytes: Buffer.byteLength(text, 'utf8'),
242
+ };
243
+ }
244
+ function toolCallsToText(toolCalls) {
245
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0)
246
+ return '';
247
+ return JSON.stringify(toolCalls);
248
+ }
249
+ function contentToText(content) {
250
+ if (typeof content === 'string')
251
+ return content;
252
+ if (Array.isArray(content)) {
253
+ return content
254
+ .map((part) => {
255
+ if (!part || typeof part !== 'object')
256
+ return '';
257
+ const record = part;
258
+ if (typeof record.text === 'string')
259
+ return record.text;
260
+ if (typeof record.type === 'string')
261
+ return JSON.stringify(record);
262
+ return '';
263
+ })
264
+ .filter(Boolean)
265
+ .join('\n');
266
+ }
267
+ if (content == null)
268
+ return '';
269
+ return JSON.stringify(content, null, 2);
270
+ }
271
+ function formatBytes(bytes) {
272
+ if (bytes < 1024)
273
+ return `${bytes} B`;
274
+ if (bytes < 1024 * 1024)
275
+ return `${(bytes / 1024).toFixed(1)} KB`;
276
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
277
+ }