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,144 @@
1
+ import path from 'node:path';
2
+ import { getCustomAgent, loadCustomAgents } from '../agents/custom-agents.js';
3
+ import { theme } from '../ui/theme.js';
4
+ const BUILT_IN_AGENT_CONFIGS = {
5
+ explore: {
6
+ type: 'explore',
7
+ systemPrompt: 'You are a codebase exploration agent. Analyze code structure, find relevant files, explain architecture. Use grep/glob tools to search. Be concise and factual.',
8
+ },
9
+ task: {
10
+ type: 'task',
11
+ systemPrompt: 'You are a task execution agent. Run commands, report results concisely. On success: brief summary. On failure: full error output.',
12
+ },
13
+ review: {
14
+ type: 'review',
15
+ systemPrompt: 'You are a code review agent. Focus only on real bugs, security issues, and logic errors. Never comment on style. Be extremely concise.',
16
+ },
17
+ plan: {
18
+ type: 'plan',
19
+ systemPrompt: "You are a planning agent. Break down the user's goal into numbered implementation steps. Each step should be actionable and specific.",
20
+ },
21
+ };
22
+ export function getAgentConfig(type) {
23
+ return { ...BUILT_IN_AGENT_CONFIGS[type] };
24
+ }
25
+ export function buildAgentPrompt(type, query, cwd) {
26
+ const config = getAgentConfig(type);
27
+ const trimmedQuery = query.trim() || defaultQuery(type);
28
+ return [
29
+ config.systemPrompt,
30
+ '',
31
+ 'Project context:',
32
+ `- Current working directory: ${cwd}`,
33
+ `- Project folder name: ${path.basename(cwd) || cwd}`,
34
+ `- Agent type: ${type}`,
35
+ '',
36
+ 'Task:',
37
+ trimmedQuery,
38
+ ].join('\n');
39
+ }
40
+ export function formatAgentResult(result) {
41
+ const metrics = theme.dim(`${result.tokensUsed} tokens • ${result.durationMs}ms`);
42
+ return `${theme.badge(result.type.toUpperCase())} ${metrics}\n${result.output}\n`;
43
+ }
44
+ export function agentCommand(args, cwd) {
45
+ const [subcommand, ...rest] = args;
46
+ if (!subcommand)
47
+ return usage();
48
+ if (subcommand.toLowerCase() === 'list')
49
+ return listAgents(cwd);
50
+ loadCustomAgents(cwd);
51
+ const customAgent = getCustomAgent(subcommand);
52
+ if (!isAgentType(subcommand) && !customAgent) {
53
+ return `${theme.warn(`unknown agent subcommand: ${subcommand}`)}\n${usage()}`;
54
+ }
55
+ const startedAt = Date.now();
56
+ const query = normalizeQuery(subcommand, rest, customAgent);
57
+ const prompt = customAgent
58
+ ? buildCustomAgentPrompt(customAgent, query, cwd)
59
+ : buildAgentPrompt(subcommand, query, cwd);
60
+ const result = {
61
+ type: subcommand,
62
+ output: prompt,
63
+ tokensUsed: estimateTokens(prompt),
64
+ durationMs: Math.max(0, Date.now() - startedAt),
65
+ };
66
+ return formatAgentResult(result);
67
+ }
68
+ function usage() {
69
+ return [
70
+ theme.brand('Agent command'),
71
+ ' /agent explore <question> delegate repository exploration',
72
+ ' /agent task <command-description> delegate task execution',
73
+ ' /agent review [target] delegate code review (default: staged changes)',
74
+ ' /agent plan <goal> delegate planning',
75
+ ' /agent <custom-agent> [task] run a project custom agent from .icopilot/agents',
76
+ ' /agent list show available agents',
77
+ '',
78
+ ].join('\n');
79
+ }
80
+ function listAgents(cwd) {
81
+ const builtInLines = Object.keys(BUILT_IN_AGENT_CONFIGS).map((type) => {
82
+ const config = getAgentConfig(type);
83
+ return ` ${theme.ok(type)} ${theme.dim(`- ${config.systemPrompt}`)}`;
84
+ });
85
+ const customAgents = loadCustomAgents(cwd);
86
+ const customLines = customAgents.length
87
+ ? customAgents.map((agent) => ` ${theme.ok(agent.name)} ${theme.dim(`- ${agent.description}`)}`)
88
+ : [` ${theme.dim('none found in .icopilot/agents/')}`];
89
+ return [
90
+ theme.brand('Available agents'),
91
+ theme.brand('Built-in'),
92
+ builtInLines.join('\n'),
93
+ '',
94
+ theme.brand('Custom'),
95
+ customLines.join('\n'),
96
+ '',
97
+ ].join('\n');
98
+ }
99
+ function isAgentType(value) {
100
+ return value === 'explore' || value === 'task' || value === 'review' || value === 'plan';
101
+ }
102
+ function normalizeQuery(type, args, customAgent) {
103
+ const query = args.join(' ').trim();
104
+ if (query)
105
+ return query;
106
+ if (customAgent)
107
+ return `Complete the next task as the "${customAgent.name}" agent.`;
108
+ return defaultQuery(type);
109
+ }
110
+ function defaultQuery(type) {
111
+ switch (type) {
112
+ case 'explore':
113
+ return 'Explore the current codebase and summarize the most relevant files and architecture.';
114
+ case 'task':
115
+ return 'Run the requested task in the current project and report the outcome.';
116
+ case 'review':
117
+ return 'Review the staged changes for bugs, security issues, and logic errors.';
118
+ case 'plan':
119
+ return "Break down the user's goal into an actionable implementation plan.";
120
+ }
121
+ }
122
+ function estimateTokens(text) {
123
+ return Math.max(1, Math.ceil(text.trim().length / 4));
124
+ }
125
+ function buildCustomAgentPrompt(agent, query, cwd) {
126
+ const metadata = [
127
+ agent.model ? `- Preferred model: ${agent.model}` : undefined,
128
+ agent.temperature !== undefined ? `- Temperature: ${agent.temperature}` : undefined,
129
+ agent.maxTokens !== undefined ? `- Max tokens: ${agent.maxTokens}` : undefined,
130
+ agent.tools?.length ? `- Allowed tools: ${agent.tools.join(', ')}` : undefined,
131
+ ].filter(Boolean);
132
+ return [
133
+ agent.systemPrompt,
134
+ '',
135
+ 'Project context:',
136
+ `- Current working directory: ${cwd}`,
137
+ `- Project folder name: ${path.basename(cwd) || cwd}`,
138
+ `- Agent type: ${agent.name}`,
139
+ ...metadata,
140
+ '',
141
+ 'Task:',
142
+ query.trim(),
143
+ ].join('\n');
144
+ }
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { theme } from '../ui/theme.js';
5
+ const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,32}$/i;
6
+ const ALIASES_ENV = 'ICOPILOT_ALIASES_PATH';
7
+ export function aliasesPath() {
8
+ return process.env[ALIASES_ENV] || path.join(os.homedir(), '.icopilot', 'aliases.json');
9
+ }
10
+ export function loadAliases() {
11
+ const file = aliasesPath();
12
+ if (!fs.existsSync(file))
13
+ return [];
14
+ try {
15
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ if (!Array.isArray(parsed))
17
+ return [];
18
+ return parsed.filter(isAlias).sort((a, b) => a.name.localeCompare(b.name));
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ export function saveAlias(name, expansion) {
25
+ const trimmedName = name.trim();
26
+ if (!NAME_RE.test(trimmedName)) {
27
+ throw new Error('alias name must match /^[a-z0-9][a-z0-9_-]{0,32}$/i');
28
+ }
29
+ const trimmedExpansion = expansion.trim();
30
+ if (!trimmedExpansion) {
31
+ throw new Error('alias expansion is required');
32
+ }
33
+ const aliases = loadAliases();
34
+ const existing = aliases.find((alias) => alias.name.localeCompare(trimmedName, undefined, { sensitivity: 'accent' }) === 0);
35
+ const alias = {
36
+ name: trimmedName,
37
+ expansion: trimmedExpansion,
38
+ createdAt: existing?.createdAt || new Date().toISOString(),
39
+ };
40
+ const next = aliases.filter((entry) => entry.name.localeCompare(trimmedName, undefined, { sensitivity: 'accent' }) !== 0);
41
+ next.push(alias);
42
+ writeAliases(next);
43
+ return alias;
44
+ }
45
+ export function deleteAlias(name) {
46
+ const trimmedName = name.trim();
47
+ const aliases = loadAliases();
48
+ const next = aliases.filter((alias) => alias.name.localeCompare(trimmedName, undefined, { sensitivity: 'accent' }) !== 0);
49
+ if (next.length === aliases.length)
50
+ return false;
51
+ writeAliases(next);
52
+ return true;
53
+ }
54
+ export function resolveAlias(input, aliases) {
55
+ const trimmed = input.trimStart();
56
+ if (!trimmed)
57
+ return null;
58
+ const ordered = [...aliases].sort((a, b) => b.name.length - a.name.length || a.name.localeCompare(b.name));
59
+ for (const alias of ordered) {
60
+ if (!trimmed.toLowerCase().startsWith(alias.name.toLowerCase()))
61
+ continue;
62
+ const remainder = trimmed.slice(alias.name.length);
63
+ if (remainder.length > 0 && !/^\s/.test(remainder))
64
+ continue;
65
+ const suffix = remainder.trimStart();
66
+ return suffix ? `${alias.expansion} ${suffix}` : alias.expansion;
67
+ }
68
+ return null;
69
+ }
70
+ export function aliasCommand(args) {
71
+ const [subcommandRaw, ...rest] = args;
72
+ const subcommand = (subcommandRaw || 'list').toLowerCase();
73
+ try {
74
+ switch (subcommand) {
75
+ case 'list':
76
+ return listCommand();
77
+ case 'set':
78
+ return setCommand(rest);
79
+ case 'remove':
80
+ case 'delete':
81
+ case 'rm':
82
+ return removeCommand(rest);
83
+ default:
84
+ return usage();
85
+ }
86
+ }
87
+ catch (error) {
88
+ return theme.err(`alias: ${error.message}\n`);
89
+ }
90
+ }
91
+ function listCommand() {
92
+ const aliases = loadAliases();
93
+ if (aliases.length === 0)
94
+ return theme.dim('No aliases saved.\n');
95
+ const lines = aliases.map((alias) => ` ${theme.hl(alias.name)} ${theme.dim('→')} ${alias.expansion}`);
96
+ return `${theme.brand('Aliases')}\n${lines.join('\n')}\n`;
97
+ }
98
+ function setCommand(args) {
99
+ const [name, ...expansionParts] = args;
100
+ const expansion = expansionParts.join(' ').trim();
101
+ if (!name || !expansion)
102
+ return theme.warn('usage: /alias set <name> <expansion...>\n');
103
+ const alias = saveAlias(name, expansion);
104
+ return theme.ok(`✔ saved alias ${alias.name} ${theme.dim('→')} ${alias.expansion}\n`);
105
+ }
106
+ function removeCommand(args) {
107
+ const [name] = args;
108
+ if (!name)
109
+ return theme.warn('usage: /alias remove <name>\n');
110
+ return deleteAlias(name)
111
+ ? theme.ok(`✔ deleted alias ${name}\n`)
112
+ : theme.warn(`alias not found: ${name}\n`);
113
+ }
114
+ function usage() {
115
+ return theme.warn('usage: /alias [list|set|remove]\n');
116
+ }
117
+ function writeAliases(aliases) {
118
+ const file = aliasesPath();
119
+ fs.mkdirSync(path.dirname(file), { recursive: true });
120
+ const sorted = [...aliases].sort((a, b) => a.name.localeCompare(b.name));
121
+ fs.writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8');
122
+ }
123
+ function isAlias(value) {
124
+ if (!value || typeof value !== 'object')
125
+ return false;
126
+ const alias = value;
127
+ return (typeof alias.name === 'string' &&
128
+ typeof alias.expansion === 'string' &&
129
+ typeof alias.createdAt === 'string' &&
130
+ NAME_RE.test(alias.name) &&
131
+ alias.expansion.trim().length > 0);
132
+ }
@@ -0,0 +1,77 @@
1
+ import { addBookmark, deleteBookmark, getBookmark, listBookmarks } from '../session/bookmarks.js';
2
+ import { theme } from '../ui/theme.js';
3
+ export function bookmarkCommand(session, rest) {
4
+ const action = rest[0]?.toLowerCase() || 'list';
5
+ if (action === 'add') {
6
+ const name = rest[1];
7
+ if (!name)
8
+ return { message: usage() };
9
+ const index = session.state.messages.length - 1;
10
+ if (index < 0)
11
+ return { message: theme.warn('No messages to bookmark.') };
12
+ const preview = messagePreview(session.state.messages[index]);
13
+ const bookmark = addBookmark(session.state.id, name, index, preview);
14
+ return { message: `${theme.ok('Bookmarked')} ${bookmark.name} at message ${bookmark.index}.` };
15
+ }
16
+ if (action === 'go') {
17
+ const name = rest[1];
18
+ if (!name)
19
+ return { message: usage() };
20
+ const bookmark = getBookmark(session.state.id, name);
21
+ if (!bookmark)
22
+ return { message: theme.warn(`Bookmark not found: ${name}`) };
23
+ return {
24
+ message: `${theme.ok('Rewind')} to ${bookmark.name} at message ${bookmark.index}.`,
25
+ rewindTo: bookmark.index,
26
+ };
27
+ }
28
+ if (action === 'delete' || action === 'del' || action === 'rm') {
29
+ const name = rest[1];
30
+ if (!name)
31
+ return { message: usage() };
32
+ const deleted = deleteBookmark(session.state.id, name);
33
+ return {
34
+ message: deleted
35
+ ? `${theme.ok('Deleted')} bookmark ${name}.`
36
+ : theme.warn(`Bookmark not found: ${name}`),
37
+ };
38
+ }
39
+ if (action !== 'list')
40
+ return { message: usage() };
41
+ const bookmarks = listBookmarks(session.state.id);
42
+ if (!bookmarks.length)
43
+ return { message: 'No bookmarks for this session.' };
44
+ return {
45
+ message: bookmarks
46
+ .map((bookmark) => `${bookmark.name} @ ${bookmark.index}: ${bookmark.preview}`)
47
+ .join('\n'),
48
+ };
49
+ }
50
+ function usage() {
51
+ return 'Usage: /bookmark [list] | /bookmark add <name> | /bookmark go <name> | /bookmark delete <name>';
52
+ }
53
+ function messagePreview(message) {
54
+ return contentToText(message.content).slice(0, 80);
55
+ }
56
+ function contentToText(content) {
57
+ if (typeof content === 'string')
58
+ return content;
59
+ if (Array.isArray(content)) {
60
+ return content
61
+ .map((part) => {
62
+ if (!part || typeof part !== 'object')
63
+ return '';
64
+ const record = part;
65
+ if (typeof record.text === 'string')
66
+ return record.text;
67
+ if (typeof record.type === 'string')
68
+ return JSON.stringify(record);
69
+ return '';
70
+ })
71
+ .filter(Boolean)
72
+ .join('\n');
73
+ }
74
+ if (content == null)
75
+ return '';
76
+ return JSON.stringify(content);
77
+ }
@@ -0,0 +1,99 @@
1
+ import simpleGit from 'simple-git';
2
+ export async function buildChangelogPrompt(args, cwd) {
3
+ const git = simpleGit({ baseDir: cwd });
4
+ try {
5
+ const isRepo = await git.checkIsRepo();
6
+ if (!isRepo) {
7
+ return buildEmptyPayload(`Cannot generate a changelog because "${cwd}" is not a git repository.`);
8
+ }
9
+ const parsed = parseArgs(args);
10
+ if (parsed.kind === 'range') {
11
+ const commits = mapCommits((await git.log([`${parsed.fromRef}..${parsed.toRef}`])).all);
12
+ return buildPayload(commits, parsed.fromRef, parsed.toRef);
13
+ }
14
+ if (parsed.kind === 'last') {
15
+ const commits = mapCommits((await git.log({ maxCount: parsed.count })).all);
16
+ return buildPayload(commits, commits.at(-1)?.hash ?? 'HEAD', 'HEAD');
17
+ }
18
+ const tags = await git.tags();
19
+ if (tags.latest) {
20
+ const commits = mapCommits((await git.log([`${tags.latest}..HEAD`])).all);
21
+ return buildPayload(commits, tags.latest, 'HEAD');
22
+ }
23
+ const commits = mapCommits((await git.log({ maxCount: 20 })).all);
24
+ return buildPayload(commits, commits.at(-1)?.hash ?? 'HEAD', 'HEAD');
25
+ }
26
+ catch (error) {
27
+ if (isNotGitRepositoryError(error)) {
28
+ return buildEmptyPayload(`Cannot generate a changelog because "${cwd}" is not a git repository.`);
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ function parseArgs(args) {
34
+ if (args.length === 0) {
35
+ return { kind: 'default' };
36
+ }
37
+ if (args[0] === '--last') {
38
+ const count = Number.parseInt(args[1] ?? '', 10);
39
+ if (!Number.isInteger(count) || count <= 0) {
40
+ throw new Error('Usage: /changelog [<from>..<to> | --last <n>]');
41
+ }
42
+ return { kind: 'last', count };
43
+ }
44
+ if (args.length === 1 && args[0].includes('..')) {
45
+ const [fromRef, toRef] = args[0].split('..');
46
+ if (!fromRef || !toRef) {
47
+ throw new Error('Usage: /changelog [<from>..<to> | --last <n>]');
48
+ }
49
+ return { kind: 'range', fromRef, toRef };
50
+ }
51
+ throw new Error('Usage: /changelog [<from>..<to> | --last <n>]');
52
+ }
53
+ function mapCommits(entries) {
54
+ return entries.map((entry) => ({
55
+ hash: entry.hash,
56
+ subject: entry.message,
57
+ author: entry.author_name,
58
+ date: entry.date,
59
+ }));
60
+ }
61
+ function buildPayload(commits, fromRef, toRef) {
62
+ return {
63
+ commits,
64
+ fromRef,
65
+ toRef,
66
+ prompt: buildPrompt(commits, fromRef, toRef),
67
+ };
68
+ }
69
+ function buildEmptyPayload(message) {
70
+ return {
71
+ commits: [],
72
+ fromRef: '',
73
+ toRef: '',
74
+ prompt: message,
75
+ };
76
+ }
77
+ function buildPrompt(commits, fromRef, toRef) {
78
+ const commitLines = commits.length === 0
79
+ ? ['- No commits found in the selected range.']
80
+ : commits.map((commit) => `- ${commit.subject} (${commit.hash}) — ${commit.author} on ${commit.date}`);
81
+ return [
82
+ `Generate a markdown changelog entry for git commits from ${fromRef} to ${toRef}.`,
83
+ 'Instructions:',
84
+ '- Group commits by type using these sections when applicable: Features, Fixes, Chores, Documentation.',
85
+ '- Infer the type from conventional commit prefixes such as feat, fix, chore, and docs.',
86
+ '- Include a dedicated Breaking Changes section when any commit indicates a breaking change.',
87
+ '- Keep the writing concise, user-facing, and release-note friendly.',
88
+ '- Output markdown only.',
89
+ '',
90
+ 'Commits:',
91
+ ...commitLines,
92
+ ].join('\n');
93
+ }
94
+ function isNotGitRepositoryError(error) {
95
+ if (!(error instanceof Error)) {
96
+ return false;
97
+ }
98
+ return /not a git repository/i.test(error.message);
99
+ }
@@ -0,0 +1,120 @@
1
+ import simpleGit from 'simple-git';
2
+ import { theme } from '../ui/theme.js';
3
+ const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
4
+ let activeSession = null;
5
+ export async function ensureChangeTracking(session) {
6
+ const resolved = resolveSession(session);
7
+ activeSession = resolved;
8
+ if (resolved.state.changeTracking) {
9
+ return resolved.state.changeTracking;
10
+ }
11
+ const sessionStartRef = await createSnapshotRef(resolved.state.cwd);
12
+ const tracking = {
13
+ sessionStartRef,
14
+ sessionStartAt: new Date().toISOString(),
15
+ turnSnapshots: [],
16
+ };
17
+ resolved.state.changeTracking = tracking;
18
+ resolved.persist();
19
+ return tracking;
20
+ }
21
+ export async function recordTurnSnapshot(session) {
22
+ const resolved = resolveSession(session);
23
+ const tracking = await ensureChangeTracking(resolved);
24
+ const turnIndex = countAssistantMessages(resolved);
25
+ const ref = await createSnapshotRef(resolved.state.cwd);
26
+ const snapshot = {
27
+ turnIndex,
28
+ ref,
29
+ createdAt: new Date().toISOString(),
30
+ };
31
+ if (tracking.turnSnapshots.at(-1)?.turnIndex === turnIndex) {
32
+ tracking.turnSnapshots[tracking.turnSnapshots.length - 1] = snapshot;
33
+ }
34
+ else {
35
+ tracking.turnSnapshots.push(snapshot);
36
+ }
37
+ resolved.state.changeTracking = tracking;
38
+ resolved.persist();
39
+ return snapshot;
40
+ }
41
+ export async function showChangesSinceSessionStart(session) {
42
+ const resolved = resolveSession(session);
43
+ const tracking = await ensureChangeTracking(resolved);
44
+ return formatDiff(await diffSinceRef(resolved.state.cwd, tracking.sessionStartRef), 'session start', 'No uncommitted changes since session start.');
45
+ }
46
+ export async function showChangesSinceLastTurn(session) {
47
+ const resolved = resolveSession(session);
48
+ const tracking = await ensureChangeTracking(resolved);
49
+ const snapshot = tracking.turnSnapshots.at(-1);
50
+ if (!snapshot) {
51
+ return theme.warn('No AI turns have been recorded yet.\n');
52
+ }
53
+ return formatDiff(await diffSinceRef(resolved.state.cwd, snapshot.ref), `AI turn ${snapshot.turnIndex + 1}`, 'No uncommitted changes since the last AI turn.');
54
+ }
55
+ export async function showChangesSinceMessage(turnIndex, session) {
56
+ const resolved = resolveSession(session);
57
+ const tracking = await ensureChangeTracking(resolved);
58
+ const snapshot = tracking.turnSnapshots.find((entry) => entry.turnIndex === turnIndex);
59
+ if (!snapshot) {
60
+ return theme.warn(`No snapshot recorded for AI turn ${turnIndex + 1}.\n`);
61
+ }
62
+ return formatDiff(await diffSinceRef(resolved.state.cwd, snapshot.ref), `AI turn ${turnIndex + 1}`, `No uncommitted changes since AI turn ${turnIndex + 1}.`);
63
+ }
64
+ function resolveSession(session) {
65
+ const resolved = session ?? activeSession;
66
+ if (!resolved) {
67
+ throw new Error('No active session is available for /changes.');
68
+ }
69
+ return resolved;
70
+ }
71
+ async function createSnapshotRef(cwd) {
72
+ const git = createGit(cwd);
73
+ const isRepo = await git.checkIsRepo();
74
+ if (!isRepo) {
75
+ throw new Error(`Not a git repository: ${cwd}`);
76
+ }
77
+ const snapshot = (await git.raw(['stash', 'create', 'icli-turn-snapshot'])).trim();
78
+ if (snapshot)
79
+ return snapshot;
80
+ try {
81
+ return (await git.raw(['rev-parse', '--verify', 'HEAD'])).trim();
82
+ }
83
+ catch {
84
+ return EMPTY_TREE_SHA;
85
+ }
86
+ }
87
+ async function diffSinceRef(cwd, ref) {
88
+ const git = createGit(cwd);
89
+ const isRepo = await git.checkIsRepo();
90
+ if (!isRepo) {
91
+ throw new Error(`Not a git repository: ${cwd}`);
92
+ }
93
+ return git.diff([ref]);
94
+ }
95
+ function createGit(cwd) {
96
+ return simpleGit({ baseDir: cwd });
97
+ }
98
+ function countAssistantMessages(session) {
99
+ return session.state.messages.filter((message) => message.role === 'assistant').length;
100
+ }
101
+ function formatDiff(diff, label, emptyMessage) {
102
+ if (!diff.trim()) {
103
+ return theme.dim(`${emptyMessage}\n`);
104
+ }
105
+ return `${theme.hl(`Changes since ${label}:`)}\n${colorize(diff)}\n`;
106
+ }
107
+ function colorize(diff) {
108
+ return diff
109
+ .split('\n')
110
+ .map((line) => line.startsWith('+') && !line.startsWith('+++')
111
+ ? theme.ok(line)
112
+ : line.startsWith('-') && !line.startsWith('---')
113
+ ? theme.err(line)
114
+ : line.startsWith('@@')
115
+ ? theme.hl(line)
116
+ : line.startsWith('diff ') || line.startsWith('index ')
117
+ ? theme.dim(line)
118
+ : line)
119
+ .join('\n');
120
+ }