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,83 @@
1
+ import { deleteSnippet, expandSnippet, listSnippets, readSnippet, saveSnippet, } from '../snippets/store.js';
2
+ import { theme } from '../ui/theme.js';
3
+ export async function snippetsCommand(rest) {
4
+ const [subcommandRaw, ...args] = rest;
5
+ const subcommand = (subcommandRaw || 'list').toLowerCase();
6
+ try {
7
+ switch (subcommand) {
8
+ case 'list':
9
+ return listCommand();
10
+ case 'save':
11
+ return saveCommand(args);
12
+ case 'show':
13
+ return showCommand(args);
14
+ case 'delete':
15
+ return deleteCommand(args);
16
+ case 'use':
17
+ return useCommand(args);
18
+ default:
19
+ return theme.warn('usage: /snippets [list|save|show|delete|use]\n');
20
+ }
21
+ }
22
+ catch (error) {
23
+ return theme.err(`snippets failed: ${error.message}\n`);
24
+ }
25
+ }
26
+ function listCommand() {
27
+ const snippets = listSnippets();
28
+ if (snippets.length === 0)
29
+ return theme.dim('No snippets saved.\n');
30
+ const lines = snippets.map((snippet) => {
31
+ const preview = firstLine(snippet.body);
32
+ return ` ${theme.hl(snippet.name)} ${theme.dim(preview)}`;
33
+ });
34
+ return `${theme.brand('Snippets')}\n${lines.join('\n')}\n`;
35
+ }
36
+ function saveCommand(args) {
37
+ const [name, ...bodyParts] = args;
38
+ const body = bodyParts.join(' ');
39
+ if (!name || !body)
40
+ return theme.warn('usage: /snippets save <name> <body>\n');
41
+ const snippet = saveSnippet(name, body);
42
+ return theme.ok(`✔ saved snippet ${snippet.name}\n`);
43
+ }
44
+ function showCommand(args) {
45
+ const [name] = args;
46
+ if (!name)
47
+ return theme.warn('usage: /snippets show <name>\n');
48
+ const snippet = readSnippet(name);
49
+ if (!snippet)
50
+ return theme.warn(`snippet not found: ${name}\n`);
51
+ return snippet.body.endsWith('\n') ? snippet.body : `${snippet.body}\n`;
52
+ }
53
+ function deleteCommand(args) {
54
+ const [name] = args;
55
+ if (!name)
56
+ return theme.warn('usage: /snippets delete <name>\n');
57
+ return deleteSnippet(name)
58
+ ? theme.ok(`✔ deleted snippet ${name}\n`)
59
+ : theme.warn(`snippet not found: ${name}\n`);
60
+ }
61
+ function useCommand(args) {
62
+ const [name, ...varArgs] = args;
63
+ if (!name)
64
+ return theme.warn('usage: /snippets use <name> [k=v ...]\n');
65
+ const snippet = readSnippet(name);
66
+ if (!snippet)
67
+ return theme.warn(`snippet not found: ${name}\n`);
68
+ const expanded = expandSnippet(snippet.body, parseVars(varArgs));
69
+ return expanded.endsWith('\n') ? expanded : `${expanded}\n`;
70
+ }
71
+ function parseVars(args) {
72
+ const vars = {};
73
+ for (const arg of args) {
74
+ const eq = arg.indexOf('=');
75
+ if (eq <= 0)
76
+ continue;
77
+ vars[arg.slice(0, eq)] = arg.slice(eq + 1);
78
+ }
79
+ return vars;
80
+ }
81
+ function firstLine(body) {
82
+ return body.split(/\r?\n/, 1)[0] || '(empty)';
83
+ }
@@ -0,0 +1,92 @@
1
+ import { theme } from '../ui/theme.js';
2
+ import { serializeSpaceConfig } from '../spaces/space-config.js';
3
+ import { SpaceManager } from '../spaces/space.js';
4
+ export function spaceCommand(args, options) {
5
+ const manager = new SpaceManager(() => options.cwd);
6
+ const [subcommandRaw, ...rest] = args;
7
+ const subcommand = (subcommandRaw || 'show').toLowerCase();
8
+ try {
9
+ switch (subcommand) {
10
+ case 'show':
11
+ return showSpace(manager, options.cwd);
12
+ case 'list':
13
+ return listSpaces(manager, options.cwd);
14
+ case 'create':
15
+ return createSpace(manager, rest[0], options);
16
+ case 'switch':
17
+ return switchSpace(manager, rest[0], options);
18
+ case 'config':
19
+ return showConfig(manager, options.cwd);
20
+ case 'delete':
21
+ case 'remove':
22
+ case 'rm':
23
+ return deleteSpace(manager, rest[0]);
24
+ default:
25
+ return usage();
26
+ }
27
+ }
28
+ catch (error) {
29
+ return theme.err(`space: ${error.message}\n`);
30
+ }
31
+ }
32
+ function showSpace(manager, cwd) {
33
+ const current = manager.currentSpace();
34
+ if (!current) {
35
+ return theme.dim(`No active space for ${cwd}.\n`);
36
+ }
37
+ const lines = [
38
+ theme.brand('Current space'),
39
+ ` name: ${theme.hl(current.name)}`,
40
+ ` root: ${current.rootPath}`,
41
+ ` branch: ${current.branch || theme.dim('none')}`,
42
+ ` sessions: ${current.sessions.length}`,
43
+ ];
44
+ return `${lines.join('\n')}\n`;
45
+ }
46
+ function listSpaces(manager, cwd) {
47
+ const spaces = manager.listSpaces();
48
+ if (!spaces.length)
49
+ return theme.dim('No spaces created.\n');
50
+ const currentName = manager.currentSpace()?.name;
51
+ const lines = spaces.map((space) => {
52
+ const current = space.name === currentName || isCurrentCwd(cwd, space.rootPath);
53
+ const marker = current ? theme.ok('*') : theme.dim('-');
54
+ const branch = space.branch ? theme.dim(` (${space.branch})`) : '';
55
+ return ` ${marker} ${theme.hl(space.name)} ${theme.dim('→')} ${space.rootPath}${branch}`;
56
+ });
57
+ return `${theme.brand('Spaces')}\n${lines.join('\n')}\n`;
58
+ }
59
+ function createSpace(manager, name, options) {
60
+ if (!name)
61
+ return theme.warn('usage: /space create <name>\n');
62
+ const space = manager.createSpace(name, options.cwd);
63
+ manager.switchSpace(space.name);
64
+ options.onSwitch?.(space);
65
+ return theme.ok(`✔ created space ${space.name} ${theme.dim(`→ ${space.rootPath}`)}\n`);
66
+ }
67
+ function switchSpace(manager, name, options) {
68
+ if (!name)
69
+ return theme.warn('usage: /space switch <name>\n');
70
+ manager.switchSpace(name);
71
+ const space = manager.loadSpace(name);
72
+ options.onSwitch?.(space);
73
+ return theme.ok(`✔ switched to space ${space.name} ${theme.dim(`→ ${space.rootPath}`)}\n`);
74
+ }
75
+ function showConfig(manager, cwd) {
76
+ const current = manager.currentSpace();
77
+ if (!current)
78
+ return theme.warn(`no active space for ${cwd}\n`);
79
+ return `${theme.brand('Space config')} ${theme.dim(current.name)}\n${serializeSpaceConfig(current.config)}`;
80
+ }
81
+ function deleteSpace(manager, name) {
82
+ if (!name)
83
+ return theme.warn('usage: /space delete <name>\n');
84
+ manager.deleteSpace(name);
85
+ return theme.ok(`✔ deleted space ${name}\n`);
86
+ }
87
+ function usage() {
88
+ return theme.warn('usage: /space [list|create|switch|config|delete]\n');
89
+ }
90
+ function isCurrentCwd(cwd, rootPath) {
91
+ return cwd === rootPath || cwd.startsWith(`${rootPath}${rootPath.endsWith('\\') ? '' : '\\'}`);
92
+ }
@@ -0,0 +1,156 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { theme } from '../ui/theme.js';
6
+ const STASHES_ENV = 'ICOPILOT_STASHES_DIR';
7
+ export function stashesDir() {
8
+ return process.env[STASHES_ENV] || path.join(os.homedir(), '.icopilot', 'stashes');
9
+ }
10
+ export function stashCommand(args, session) {
11
+ const action = args[0]?.toLowerCase() || 'list';
12
+ if (action === 'push') {
13
+ return pushStash(args.slice(1).join(' ').trim(), session);
14
+ }
15
+ if (action === 'pop') {
16
+ return popStash(args.slice(1).join(' ').trim(), session);
17
+ }
18
+ if (action === 'list') {
19
+ return listStashes();
20
+ }
21
+ if (action === 'drop' || action === 'delete' || action === 'rm') {
22
+ const target = args.slice(1).join(' ').trim();
23
+ if (!target)
24
+ return usage();
25
+ const stash = findStash(target);
26
+ if (!stash)
27
+ return `${theme.warn(`stash not found: ${target}`)}\n`;
28
+ fs.rmSync(stash.file, { force: true });
29
+ return `${theme.ok('Dropped')} stash ${theme.hl(stash.record.entry.name)}.\n`;
30
+ }
31
+ if (action === 'clear') {
32
+ const stashes = readStashes();
33
+ for (const stash of stashes) {
34
+ fs.rmSync(stash.file, { force: true });
35
+ }
36
+ return `${theme.ok('Cleared')} ${stashes.length} stash${stashes.length === 1 ? '' : 'es'}.\n`;
37
+ }
38
+ return usage();
39
+ }
40
+ function pushStash(name, session) {
41
+ const entry = {
42
+ id: crypto.randomUUID(),
43
+ name: name || autoName(),
44
+ messageCount: session.state.messages.length,
45
+ model: session.state.model,
46
+ cwd: session.state.cwd,
47
+ createdAt: new Date().toISOString(),
48
+ };
49
+ const data = {
50
+ messages: [...session.state.messages],
51
+ model: session.state.model,
52
+ cwd: session.state.cwd,
53
+ mode: session.state.mode,
54
+ };
55
+ fs.mkdirSync(stashesDir(), { recursive: true });
56
+ fs.writeFileSync(stashFile(entry.id), `${JSON.stringify({ entry, data }, null, 2)}\n`, 'utf8');
57
+ return (`${theme.ok('Stashed')} ${theme.hl(entry.name)} ${theme.dim(`(${entry.messageCount} messages)`)}` +
58
+ `\n`);
59
+ }
60
+ function popStash(target, session) {
61
+ const stash = target ? findStash(target) : readStashes()[0];
62
+ if (!stash)
63
+ return `${theme.warn('No stashes available.')}\n`;
64
+ session.state.messages = [...stash.record.data.messages];
65
+ session.state.model = stash.record.data.model;
66
+ session.state.cwd = stash.record.data.cwd;
67
+ session.state.mode = stash.record.data.mode;
68
+ session.persist();
69
+ fs.rmSync(stash.file, { force: true });
70
+ return (`${theme.ok('Restored')} ${theme.hl(stash.record.entry.name)} ${theme.dim(`(${stash.record.entry.messageCount} messages)`)}` +
71
+ `\n`);
72
+ }
73
+ function listStashes() {
74
+ const stashes = readStashes();
75
+ if (stashes.length === 0)
76
+ return 'No stashes.\n';
77
+ const lines = stashes.map(({ record }) => {
78
+ const when = formatDate(record.entry.createdAt);
79
+ const count = `${record.entry.messageCount} msg${record.entry.messageCount === 1 ? '' : 's'}`;
80
+ return ` ${theme.hl(record.entry.name)} ${theme.dim(`[${record.entry.id}]`)} ${theme.dim(when)} ${theme.dim(`(${count})`)}`;
81
+ });
82
+ return `${theme.brand('Stashes')}\n${lines.join('\n')}\n`;
83
+ }
84
+ function readStashes() {
85
+ const dir = stashesDir();
86
+ if (!fs.existsSync(dir))
87
+ return [];
88
+ return fs
89
+ .readdirSync(dir, { withFileTypes: true })
90
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
91
+ .map((entry) => {
92
+ const file = path.join(dir, entry.name);
93
+ try {
94
+ return { file, record: normalizeRecord(JSON.parse(fs.readFileSync(file, 'utf8'))) };
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ })
100
+ .filter((stash) => stash !== null)
101
+ .sort((a, b) => b.record.entry.createdAt.localeCompare(a.record.entry.createdAt));
102
+ }
103
+ function normalizeRecord(value) {
104
+ const source = typeof value === 'object' && value !== null ? value : {};
105
+ const entrySource = typeof source.entry === 'object' && source.entry !== null
106
+ ? source.entry
107
+ : {};
108
+ const dataSource = typeof source.data === 'object' && source.data !== null
109
+ ? source.data
110
+ : {};
111
+ const id = typeof entrySource.id === 'string' && entrySource.id ? entrySource.id : crypto.randomUUID();
112
+ const messages = Array.isArray(dataSource.messages) ? dataSource.messages : [];
113
+ const model = typeof dataSource.model === 'string' ? dataSource.model : '';
114
+ const cwd = typeof dataSource.cwd === 'string' ? dataSource.cwd : '';
115
+ const mode = typeof dataSource.mode === 'string' ? dataSource.mode : 'ask';
116
+ const createdAt = typeof entrySource.createdAt === 'string' && entrySource.createdAt
117
+ ? entrySource.createdAt
118
+ : new Date().toISOString();
119
+ return {
120
+ entry: {
121
+ id,
122
+ name: typeof entrySource.name === 'string' && entrySource.name ? entrySource.name : id,
123
+ messageCount: typeof entrySource.messageCount === 'number' && Number.isFinite(entrySource.messageCount)
124
+ ? Math.max(0, Math.trunc(entrySource.messageCount))
125
+ : messages.length,
126
+ model: typeof entrySource.model === 'string' ? entrySource.model : model,
127
+ cwd: typeof entrySource.cwd === 'string' ? entrySource.cwd : cwd,
128
+ createdAt,
129
+ },
130
+ data: {
131
+ messages,
132
+ model,
133
+ cwd,
134
+ mode,
135
+ },
136
+ };
137
+ }
138
+ function findStash(target) {
139
+ const normalized = target.trim().toLowerCase();
140
+ return readStashes().find(({ record }) => record.entry.id.toLowerCase() === normalized ||
141
+ record.entry.name.toLowerCase() === normalized);
142
+ }
143
+ function stashFile(id) {
144
+ return path.join(stashesDir(), `${id}.json`);
145
+ }
146
+ function autoName() {
147
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
148
+ return `stash-${timestamp}`;
149
+ }
150
+ function formatDate(value) {
151
+ const date = new Date(value);
152
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
153
+ }
154
+ function usage() {
155
+ return 'Usage: /stash [list] | /stash push [name] | /stash pop [id-or-name] | /stash drop <id-or-name> | /stash clear\n';
156
+ }
@@ -0,0 +1,36 @@
1
+ import { loadStats, resetStats, statsPath } from '../stats/store.js';
2
+ import { theme } from '../ui/theme.js';
3
+ export function statsCommand(sub = 'show') {
4
+ const cmd = sub.trim().toLowerCase() || 'show';
5
+ if (cmd === 'reset') {
6
+ resetStats();
7
+ return `${theme.ok('✔ usage stats reset')}\n`;
8
+ }
9
+ if (cmd === 'path') {
10
+ return `${theme.dim(statsPath())}\n`;
11
+ }
12
+ const s = loadStats();
13
+ return [
14
+ theme.brand('Usage stats'),
15
+ ` first seen: ${theme.hl(s.firstSeen)}`,
16
+ ` last update: ${theme.hl(s.lastUpdate)}`,
17
+ ` sessions: ${theme.hl(String(s.sessions))}`,
18
+ ` tokens in: ${theme.hl(String(s.tokensIn))}`,
19
+ ` tokens out: ${theme.hl(String(s.tokensOut))}`,
20
+ '',
21
+ theme.brand('Top tool calls'),
22
+ formatTop(s.toolCalls),
23
+ '',
24
+ theme.brand('Top commands'),
25
+ formatTop(s.commands),
26
+ '',
27
+ ].join('\n');
28
+ }
29
+ function formatTop(counters) {
30
+ const top = Object.entries(counters)
31
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
32
+ .slice(0, 5);
33
+ if (top.length === 0)
34
+ return ` ${theme.dim('none')}`;
35
+ return top.map(([name, count]) => ` ${theme.hl(String(count)).padStart(5)} ${name}`).join('\n');
36
+ }
@@ -0,0 +1,85 @@
1
+ import fg from 'fast-glob';
2
+ import { StyleLearner, loadStyleProfile, resetStyleProfile, resolveStyleProfilePath, } from '../knowledge/style-learner.js';
3
+ import { theme } from '../ui/theme.js';
4
+ const STYLE_PATTERNS = [
5
+ 'src/**/*.{ts,tsx,js,jsx,mjs,cjs}',
6
+ 'tests/**/*.{ts,tsx,js,jsx,mjs,cjs}',
7
+ '*.ts',
8
+ '*.tsx',
9
+ '*.js',
10
+ '*.jsx',
11
+ '*.mjs',
12
+ '*.cjs',
13
+ ];
14
+ const STYLE_IGNORE = [
15
+ '**/node_modules/**',
16
+ '**/dist/**',
17
+ '**/.git/**',
18
+ '**/.icopilot/**',
19
+ '**/coverage/**',
20
+ ];
21
+ export async function styleCommand(args, cwd) {
22
+ const [rawSubcommand = 'show'] = args;
23
+ const subcommand = rawSubcommand.toLowerCase();
24
+ try {
25
+ switch (subcommand) {
26
+ case 'show':
27
+ case 'current':
28
+ return showStyleProfile(cwd);
29
+ case 'learn':
30
+ return learnStyleProfile(cwd);
31
+ case 'reset':
32
+ case 'clear':
33
+ return resetStyleProfile(cwd)
34
+ ? `${theme.ok('✔ cleared learned style profile.\n')}`
35
+ : `${theme.dim('No learned style profile to clear.\n')}`;
36
+ default:
37
+ return theme.warn('usage: /style [learn|reset]\n');
38
+ }
39
+ }
40
+ catch (err) {
41
+ return theme.err(`style: ${err?.message || err}\n`);
42
+ }
43
+ }
44
+ async function learnStyleProfile(cwd) {
45
+ const files = await fg(STYLE_PATTERNS, {
46
+ cwd,
47
+ absolute: true,
48
+ onlyFiles: true,
49
+ unique: true,
50
+ ignore: STYLE_IGNORE,
51
+ });
52
+ if (files.length === 0)
53
+ return theme.warn('No source files found to learn from.\n');
54
+ const learner = new StyleLearner();
55
+ const profile = learner.analyze(files);
56
+ const profilePath = resolveStyleProfilePath(cwd);
57
+ learner.save(profilePath);
58
+ return [
59
+ `${theme.ok('✔ learned style profile')} ${theme.dim(`(${files.length} files)`)} ${theme.hl(profilePath)}`,
60
+ renderProfile(profile),
61
+ ].join('\n');
62
+ }
63
+ function showStyleProfile(cwd) {
64
+ const profile = loadStyleProfile(cwd);
65
+ if (!profile) {
66
+ return `${theme.brand('Style profile')}\n ${theme.dim('No learned profile. Run /style learn first.')}\n`;
67
+ }
68
+ return renderProfile(profile);
69
+ }
70
+ function renderProfile(profile) {
71
+ const entries = [
72
+ ['indentation', profile.indentation],
73
+ ['quotes', profile.quotes],
74
+ ['semicolons', profile.semicolons],
75
+ ['trailingComma', profile.trailingComma],
76
+ ['namingConvention', profile.namingConvention],
77
+ ['importStyle', profile.importStyle],
78
+ ['functionStyle', profile.functionStyle],
79
+ ['commentStyle', profile.commentStyle],
80
+ ['maxLineLength', profile.maxLineLength],
81
+ ];
82
+ return `${theme.brand('Style profile')}\n${entries
83
+ .map(([key, value]) => ` ${theme.hl(key.padEnd(16))} ${value}`)
84
+ .join('\n')}\n`;
85
+ }
@@ -0,0 +1,40 @@
1
+ import { streamChat } from '../api/github-models.js';
2
+ import { theme } from '../ui/theme.js';
3
+ const SUGGEST_SYSTEM_PROMPT = `You translate natural-language requests into exactly one shell command.
4
+ Respond with ONLY the command text.
5
+ Do not explain anything.
6
+ Do not use markdown fences.
7
+ Do not add bullets, labels, or commentary.
8
+ Prefer a safe, direct command that can run in the user's current working directory.`;
9
+ export async function suggestCommand(query, session, signal) {
10
+ const trimmedQuery = query.trim();
11
+ if (!trimmedQuery)
12
+ return theme.warn('usage: /suggest <request>\n');
13
+ const messages = [
14
+ { role: 'system', content: SUGGEST_SYSTEM_PROMPT },
15
+ {
16
+ role: 'user',
17
+ content: `Current working directory: ${session.state.cwd}\nRequest: ${trimmedQuery}`,
18
+ },
19
+ ];
20
+ let suggestion = '';
21
+ const result = await streamChat({
22
+ model: session.state.model,
23
+ messages,
24
+ temperature: 0.1,
25
+ signal,
26
+ onToken: (token) => {
27
+ suggestion += token;
28
+ },
29
+ });
30
+ const command = sanitizeSuggestion(result.content || suggestion);
31
+ return `${theme.brand('Suggested command')}\n ${theme.hl(command)}\n`;
32
+ }
33
+ function sanitizeSuggestion(content) {
34
+ const withoutFences = content
35
+ .trim()
36
+ .replace(/^```(?:\w+)?\s*/u, '')
37
+ .replace(/\s*```$/u, '')
38
+ .trim();
39
+ return withoutFences || 'echo "No command suggested"';
40
+ }
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const SKIP_NAMES = new Set(['node_modules', '.git', 'dist']);
4
+ export function buildSummary(cwd) {
5
+ const packageJson = readPackageJson(cwd);
6
+ const projectName = packageJson?.name?.trim() || path.basename(path.resolve(cwd));
7
+ const scripts = sortRecordEntries(packageJson?.scripts);
8
+ const dependencies = sortKeys(packageJson?.dependencies);
9
+ const devDependencies = sortKeys(packageJson?.devDependencies);
10
+ const topLevelEntries = listTopLevelEntries(cwd);
11
+ const detectedStack = detectStack(cwd, packageJson);
12
+ const structure = [
13
+ `Project: ${projectName}`,
14
+ `Workspace: ${path.resolve(cwd)}`,
15
+ '',
16
+ 'Detected stack:',
17
+ ...formatList(detectedStack),
18
+ '',
19
+ 'Top-level entries:',
20
+ ...formatList(topLevelEntries),
21
+ '',
22
+ 'Scripts:',
23
+ ...formatKeyValueList(scripts),
24
+ '',
25
+ `Dependencies (${dependencies.length}):`,
26
+ ...formatList(dependencies),
27
+ '',
28
+ `Dev dependencies (${devDependencies.length}):`,
29
+ ...formatList(devDependencies),
30
+ ].join('\n');
31
+ const prompt = [
32
+ `Summarize the architecture of the project "${projectName}" based on the workspace overview below.`,
33
+ 'Cover:',
34
+ '1. The likely purpose of the project',
35
+ '2. Primary languages, frameworks, and tooling',
36
+ '3. Important top-level directories/files and their likely responsibilities',
37
+ '4. Build, test, or run workflows implied by scripts/config',
38
+ '5. Notable extension points, risks, or missing context',
39
+ 'If details are uncertain, say so explicitly.',
40
+ '',
41
+ structure,
42
+ ].join('\n');
43
+ return { projectName, structure, prompt };
44
+ }
45
+ function readPackageJson(cwd) {
46
+ const packageJsonPath = path.join(cwd, 'package.json');
47
+ if (!fs.existsSync(packageJsonPath)) {
48
+ return undefined;
49
+ }
50
+ try {
51
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function listTopLevelEntries(cwd) {
58
+ return fs
59
+ .readdirSync(cwd, { withFileTypes: true })
60
+ .filter((entry) => !SKIP_NAMES.has(entry.name))
61
+ .sort((a, b) => a.name.localeCompare(b.name))
62
+ .map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
63
+ }
64
+ function detectStack(cwd, packageJson) {
65
+ const detected = new Set();
66
+ const packageDeps = {
67
+ ...packageJson?.dependencies,
68
+ ...packageJson?.devDependencies,
69
+ };
70
+ if (packageJson) {
71
+ detected.add('Node.js');
72
+ if (hasAnyFile(cwd, ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.build.json']) ||
73
+ Boolean(packageDeps.typescript)) {
74
+ detected.add('TypeScript');
75
+ }
76
+ else {
77
+ detected.add('JavaScript');
78
+ }
79
+ }
80
+ if (hasAnyFile(cwd, ['pyproject.toml', 'requirements.txt', 'setup.py'])) {
81
+ detected.add('Python');
82
+ }
83
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
84
+ detected.add('Rust');
85
+ }
86
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) {
87
+ detected.add('Go');
88
+ }
89
+ if (hasAnyFile(cwd, ['pom.xml', 'build.gradle', 'build.gradle.kts'])) {
90
+ detected.add('Java');
91
+ }
92
+ if (hasAnyMatch(cwd, (name) => name.endsWith('.csproj') || name.endsWith('.sln'))) {
93
+ detected.add('.NET');
94
+ }
95
+ if (packageDeps.react)
96
+ detected.add('React');
97
+ if (packageDeps.next)
98
+ detected.add('Next.js');
99
+ if (packageDeps.vue)
100
+ detected.add('Vue');
101
+ if (packageDeps.nuxt)
102
+ detected.add('Nuxt');
103
+ if (packageDeps.svelte)
104
+ detected.add('Svelte');
105
+ if (packageDeps.express)
106
+ detected.add('Express');
107
+ if (packageDeps['@nestjs/core'])
108
+ detected.add('NestJS');
109
+ if (packageDeps.vitest)
110
+ detected.add('Vitest');
111
+ if (packageDeps.jest)
112
+ detected.add('Jest');
113
+ return Array.from(detected).sort((a, b) => a.localeCompare(b));
114
+ }
115
+ function hasAnyFile(cwd, fileNames) {
116
+ return fileNames.some((fileName) => fs.existsSync(path.join(cwd, fileName)));
117
+ }
118
+ function hasAnyMatch(cwd, match) {
119
+ return fs.readdirSync(cwd).some((entryName) => match(entryName));
120
+ }
121
+ function sortKeys(record) {
122
+ return Object.keys(record ?? {}).sort((a, b) => a.localeCompare(b));
123
+ }
124
+ function sortRecordEntries(record) {
125
+ return Object.entries(record ?? {}).sort(([left], [right]) => left.localeCompare(right));
126
+ }
127
+ function formatList(items) {
128
+ if (!items.length) {
129
+ return ['- (none)'];
130
+ }
131
+ return items.map((item) => `- ${item}`);
132
+ }
133
+ function formatKeyValueList(items) {
134
+ if (!items.length) {
135
+ return ['- (none)'];
136
+ }
137
+ return items.map(([key, value]) => `- ${key}: ${value}`);
138
+ }
@@ -0,0 +1,58 @@
1
+ import { theme } from '../ui/theme.js';
2
+ export function taskCommand(args, manager) {
3
+ const [rawSubcommand = 'list', ...rest] = args;
4
+ const subcommand = rawSubcommand.toLowerCase();
5
+ if (subcommand === 'list')
6
+ return manager.formatTaskList();
7
+ if (subcommand === 'status') {
8
+ const id = rest[0]?.trim() ?? '';
9
+ if (!id)
10
+ return usage();
11
+ const match = findByPrefix(manager, id);
12
+ if (match.kind === 'match') {
13
+ return manager.formatTaskResult(match.id);
14
+ }
15
+ return match.message;
16
+ }
17
+ if (subcommand === 'cancel') {
18
+ const id = rest[0]?.trim() ?? '';
19
+ if (!id)
20
+ return usage();
21
+ const match = findByPrefix(manager, id);
22
+ if (match.kind !== 'match')
23
+ return match.message;
24
+ const matchId = match.id;
25
+ const task = manager.getTask(matchId);
26
+ if (!task)
27
+ return `${theme.warn(`No background task matches "${id}".`)}\n`;
28
+ if (task.status !== 'running') {
29
+ return `${theme.warn(`Task ${task.id.slice(0, 8)} is already ${task.status}.`)}\n`;
30
+ }
31
+ manager.failTask(task.id, 'Cancelled by user.');
32
+ return `${theme.ok('Cancelled')} ${theme.hl(task.id.slice(0, 8))} ${task.goal}\n`;
33
+ }
34
+ return usage();
35
+ }
36
+ function usage() {
37
+ return 'Usage: /tasks | /task list | /task status <id-prefix> | /task cancel <id-prefix>\n';
38
+ }
39
+ function findByPrefix(manager, prefix) {
40
+ const normalizedPrefix = prefix.toLowerCase();
41
+ const matches = manager
42
+ .listTasks()
43
+ .filter((task) => task.id === prefix || task.id.toLowerCase().startsWith(normalizedPrefix));
44
+ if (matches.length === 0) {
45
+ return {
46
+ kind: 'missing',
47
+ message: `${theme.warn(`No background task matches "${prefix}".`)}\n`,
48
+ };
49
+ }
50
+ if (matches.length > 1) {
51
+ const options = matches.map((task) => task.id.slice(0, 8)).join(', ');
52
+ return {
53
+ kind: 'ambiguous',
54
+ message: `${theme.warn(`Multiple background tasks match "${prefix}": ${options}`)}\n`,
55
+ };
56
+ }
57
+ return { kind: 'match', id: matches[0].id };
58
+ }