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,92 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import simpleGit from 'simple-git';
4
+ import { streamChat } from '../api/github-models.js';
5
+ import { theme } from '../ui/theme.js';
6
+ const DIFF_REVIEW_SYSTEM = `You are reviewing a git diff.
7
+ Focus on bugs, security issues, style problems, and concrete improvements.
8
+ Prefer specific, actionable feedback tied to files or hunks.
9
+ If the diff looks good, say so briefly.`;
10
+ export async function buildDiffReviewPrompt(args, cwd) {
11
+ const git = simpleGit({ baseDir: cwd });
12
+ const isRepo = await git.checkIsRepo();
13
+ if (!isRepo) {
14
+ throw new Error(`Not a git repository: ${cwd}`);
15
+ }
16
+ const selection = selectDiff(args, cwd);
17
+ const diff = await git.diff(selection.diffArgs);
18
+ const prompt = [
19
+ `Review this git diff (${selection.scope}).`,
20
+ 'Check for:',
21
+ '- bugs and logic errors',
22
+ '- security issues',
23
+ '- style or readability problems',
24
+ '- missing edge cases or tests',
25
+ '- concrete improvement suggestions',
26
+ '',
27
+ 'Diff:',
28
+ diff || '(no diff)',
29
+ ].join('\n');
30
+ return { diff, scope: selection.scope, prompt };
31
+ }
32
+ export async function reviewDiff(session, args, signal) {
33
+ try {
34
+ const payload = await buildDiffReviewPrompt(args, session.state.cwd);
35
+ if (!payload.diff.trim()) {
36
+ process.stdout.write(theme.warn(`No changes found for ${payload.scope}.\n`));
37
+ return;
38
+ }
39
+ const messages = [
40
+ { role: 'system', content: DIFF_REVIEW_SYSTEM },
41
+ { role: 'user', content: payload.prompt },
42
+ ];
43
+ process.stdout.write(theme.dim(`Reviewing ${payload.scope}…\n\n`));
44
+ await streamChat({
45
+ model: session.state.model,
46
+ messages,
47
+ temperature: 0.2,
48
+ signal,
49
+ onToken: (token) => process.stdout.write(token),
50
+ });
51
+ process.stdout.write('\n');
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ process.stdout.write(theme.err(`diff review failed: ${message}\n`));
56
+ }
57
+ }
58
+ function selectDiff(args, cwd) {
59
+ if (!args.length) {
60
+ return { diffArgs: [], scope: 'unstaged changes' };
61
+ }
62
+ const target = args.join(' ').trim();
63
+ if (target === '--staged') {
64
+ return { diffArgs: ['--cached'], scope: 'staged changes' };
65
+ }
66
+ if (isExistingFileTarget(target, cwd)) {
67
+ return {
68
+ diffArgs: ['--', target],
69
+ scope: `changes for ${path.relative(cwd, path.resolve(cwd, target)) || target}`,
70
+ };
71
+ }
72
+ if (/^[^\s]+\.\.[^\s]+$/.test(target)) {
73
+ return { diffArgs: [target], scope: `changes between ${target}` };
74
+ }
75
+ if (looksLikePathTarget(target)) {
76
+ return {
77
+ diffArgs: ['--', target],
78
+ scope: `changes for ${path.relative(cwd, path.resolve(cwd, target)) || target}`,
79
+ };
80
+ }
81
+ return { diffArgs: [`${target}...HEAD`], scope: `changes from ${target} to HEAD` };
82
+ }
83
+ function isExistingFileTarget(target, cwd) {
84
+ const resolved = path.resolve(cwd, target);
85
+ return fs.existsSync(resolved) && fs.statSync(resolved).isFile();
86
+ }
87
+ function looksLikePathTarget(target) {
88
+ return (target.includes('/') ||
89
+ target.includes('\\') ||
90
+ target.startsWith('.') ||
91
+ path.extname(target).length > 0);
92
+ }
@@ -0,0 +1,412 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { theme } from '../ui/theme.js';
5
+ const DOC_STYLES = new Set(['jsdoc', 'tsdoc', 'numpy', 'google']);
6
+ const SOURCE_GLOBS = [
7
+ '**/*.ts',
8
+ '**/*.tsx',
9
+ '**/*.js',
10
+ '**/*.jsx',
11
+ '**/*.mjs',
12
+ '**/*.cjs',
13
+ '**/*.py',
14
+ ];
15
+ const IGNORE_GLOBS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/coverage/**'];
16
+ export function generateDoc(code, style) {
17
+ const resolvedStyle = normalizeStyle(style);
18
+ const signature = parseSignature(code);
19
+ switch (resolvedStyle) {
20
+ case 'jsdoc':
21
+ return buildJsLikeDoc(signature, { includeThrows: true });
22
+ case 'tsdoc':
23
+ return buildJsLikeDoc(signature, { includeThrows: false });
24
+ case 'numpy':
25
+ return buildPythonDoc(signature, 'numpy');
26
+ case 'google':
27
+ return buildPythonDoc(signature, 'google');
28
+ }
29
+ }
30
+ export function findUndocumented(rootDir) {
31
+ const matches = [];
32
+ const files = fg.sync(SOURCE_GLOBS, {
33
+ cwd: rootDir,
34
+ absolute: true,
35
+ onlyFiles: true,
36
+ dot: false,
37
+ ignore: IGNORE_GLOBS,
38
+ });
39
+ for (const file of files) {
40
+ const code = safeReadFile(file);
41
+ if (!code)
42
+ continue;
43
+ for (const symbol of collectSymbols(code, file)) {
44
+ if (!symbol.documented && isProjectExport(symbol, code)) {
45
+ matches.push({
46
+ name: symbol.name,
47
+ file,
48
+ line: symbol.line,
49
+ kind: symbol.kind,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return matches.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
55
+ }
56
+ export function docCommand(args, cwd) {
57
+ const parsed = parseDocArgs(args);
58
+ if (parsed.error)
59
+ return `${theme.warn(parsed.error)}\n`;
60
+ if (parsed.all) {
61
+ return handleAllDocs(cwd, parsed.style, parsed.overwrite);
62
+ }
63
+ if (!parsed.file) {
64
+ return `${theme.warn('usage: /doc <file> [symbol] [--style <jsdoc|tsdoc|numpy|google>] [--overwrite]')}\n`;
65
+ }
66
+ const options = {
67
+ file: parsed.file,
68
+ symbol: parsed.symbol,
69
+ style: parsed.style,
70
+ overwrite: parsed.overwrite,
71
+ };
72
+ return handleSingleDoc(options, cwd);
73
+ }
74
+ function handleSingleDoc(options, cwd) {
75
+ const resolvedFile = path.resolve(cwd, options.file);
76
+ const code = safeReadFile(resolvedFile);
77
+ if (!code)
78
+ return `${theme.err(`unable to read file: ${resolvedFile}`)}\n`;
79
+ const symbols = collectSymbols(code, resolvedFile);
80
+ const style = normalizeStyle(options.style, resolvedFile);
81
+ if (options.symbol) {
82
+ const symbol = symbols.find((entry) => entry.name === options.symbol);
83
+ if (!symbol) {
84
+ return `${theme.warn(`symbol not found: ${options.symbol}`)}\n`;
85
+ }
86
+ if (options.overwrite) {
87
+ writeDocsToFile(resolvedFile, code, [symbol], style);
88
+ return `${theme.ok(`✔ wrote ${style} docs for ${symbol.name}`)}\n`;
89
+ }
90
+ const doc = indentDoc(generateDoc(symbol.signature, style), symbol.language === 'python' ? nextIndent(symbol.indent) : symbol.indent);
91
+ return `${theme.brand('Generated docs')} ${theme.dim(`${resolvedFile}:${symbol.line}`)}\n\n${doc}\n`;
92
+ }
93
+ const targets = symbols.filter((symbol) => !symbol.documented);
94
+ if (targets.length === 0) {
95
+ return `${theme.dim(`No undocumented functions or classes found in ${resolvedFile}.\n`)}`;
96
+ }
97
+ if (options.overwrite) {
98
+ writeDocsToFile(resolvedFile, code, targets, style);
99
+ return `${theme.ok(`✔ wrote ${targets.length} ${style} doc block${targets.length === 1 ? '' : 's'} in ${resolvedFile}`)}\n`;
100
+ }
101
+ const blocks = targets.map((symbol) => {
102
+ const doc = indentDoc(generateDoc(symbol.signature, style), symbol.language === 'python' ? nextIndent(symbol.indent) : symbol.indent);
103
+ return `${theme.hl(`${symbol.kind} ${symbol.name}`)} ${theme.dim(`line ${symbol.line}`)}\n${doc}`;
104
+ });
105
+ return `${theme.brand('Generated docs')} ${theme.dim(resolvedFile)}\n\n${blocks.join('\n\n')}\n`;
106
+ }
107
+ function handleAllDocs(cwd, requestedStyle, overwrite = false) {
108
+ const missing = findUndocumented(cwd);
109
+ if (missing.length === 0)
110
+ return `${theme.dim('No undocumented exports found.\n')}`;
111
+ if (overwrite) {
112
+ const byFile = groupByFile(missing);
113
+ let written = 0;
114
+ for (const [file, symbols] of byFile) {
115
+ const code = safeReadFile(file);
116
+ if (!code)
117
+ continue;
118
+ const available = collectSymbols(code, file);
119
+ const targets = symbols
120
+ .map((entry) => available.find((candidate) => candidate.name === entry.name && candidate.line === entry.line))
121
+ .filter((entry) => Boolean(entry));
122
+ if (!targets.length)
123
+ continue;
124
+ writeDocsToFile(file, code, targets, normalizeStyle(requestedStyle, file));
125
+ written += targets.length;
126
+ }
127
+ return `${theme.ok(`✔ wrote ${written} doc block${written === 1 ? '' : 's'} across ${byFile.size} file${byFile.size === 1 ? '' : 's'}`)}\n`;
128
+ }
129
+ const lines = missing.map((entry) => {
130
+ const style = normalizeStyle(requestedStyle, entry.file);
131
+ return ` ${theme.ok(entry.name)} ${theme.dim(`(${entry.kind}, ${path.relative(cwd, entry.file)}:${entry.line}, ${style})`)}`;
132
+ });
133
+ return `${theme.brand('Undocumented exports')}\n${lines.join('\n')}\n`;
134
+ }
135
+ function writeDocsToFile(filePath, code, symbols, style) {
136
+ const lines = code.split(/\r?\n/);
137
+ const sorted = [...symbols].sort((a, b) => b.line - a.line);
138
+ for (const symbol of sorted) {
139
+ const insertAt = Math.max(0, symbol.line - 1);
140
+ const doc = generateDoc(symbol.signature, style);
141
+ const indent = symbol.language === 'python' ? nextIndent(symbol.indent) : symbol.indent;
142
+ const docLines = indentDoc(doc, indent).split('\n');
143
+ if (symbol.language === 'python') {
144
+ lines.splice(insertAt + 1, 0, ...docLines);
145
+ }
146
+ else {
147
+ lines.splice(insertAt, 0, ...docLines);
148
+ }
149
+ }
150
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
151
+ }
152
+ function collectSymbols(code, filePath) {
153
+ const language = detectLanguage(filePath);
154
+ return language === 'python'
155
+ ? collectPythonSymbols(code, filePath)
156
+ : collectJsLikeSymbols(code, filePath, language);
157
+ }
158
+ function collectJsLikeSymbols(code, filePath, language) {
159
+ const lines = code.split(/\r?\n/);
160
+ const symbols = [];
161
+ for (let index = 0; index < lines.length; index += 1) {
162
+ const line = lines[index];
163
+ const signature = line.trim();
164
+ const match = line.match(/^(?<indent>\s*)(?<export>export\s+(?:default\s+)?)?(?:async\s+)?function\s+(?<name>[A-Za-z_$][\w$]*)\s*\(/) ??
165
+ line.match(/^(?<indent>\s*)(?<export>export\s+)?class\s+(?<name>[A-Za-z_$][\w$]*)\b/) ??
166
+ line.match(/^(?<indent>\s*)(?<export>export\s+)?const\s+(?<name>[A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/) ??
167
+ line.match(/^(?<indent>\s*)(?<export>export\s+)?const\s+(?<name>[A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\s*\(/);
168
+ if (!match?.groups?.name)
169
+ continue;
170
+ symbols.push({
171
+ name: match.groups.name,
172
+ file: filePath,
173
+ line: index + 1,
174
+ kind: signature.includes('class ') ? 'class' : 'function',
175
+ documented: hasLeadingBlockDoc(lines, index),
176
+ indent: match.groups.indent ?? '',
177
+ signature,
178
+ language,
179
+ });
180
+ }
181
+ return symbols;
182
+ }
183
+ function collectPythonSymbols(code, filePath) {
184
+ const lines = code.split(/\r?\n/);
185
+ const symbols = [];
186
+ for (let index = 0; index < lines.length; index += 1) {
187
+ const line = lines[index];
188
+ const match = line.match(/^(?<indent>\s*)def\s+(?<name>[A-Za-z_][\w]*)\s*\(/) ??
189
+ line.match(/^(?<indent>\s*)class\s+(?<name>[A-Za-z_][\w]*)\b/);
190
+ if (!match?.groups?.name)
191
+ continue;
192
+ symbols.push({
193
+ name: match.groups.name,
194
+ file: filePath,
195
+ line: index + 1,
196
+ kind: line.includes('class ') ? 'class' : 'function',
197
+ documented: hasPythonDocstring(lines, index),
198
+ indent: match.groups.indent ?? '',
199
+ signature: line.trim(),
200
+ language: 'python',
201
+ });
202
+ }
203
+ return symbols;
204
+ }
205
+ function isProjectExport(symbol, code) {
206
+ if (symbol.language === 'python')
207
+ return true;
208
+ const line = code.split(/\r?\n/)[symbol.line - 1] ?? '';
209
+ return /\bexport\b/.test(line);
210
+ }
211
+ function hasLeadingBlockDoc(lines, symbolIndex) {
212
+ for (let index = symbolIndex - 1; index >= 0; index -= 1) {
213
+ const line = lines[index].trim();
214
+ if (!line)
215
+ continue;
216
+ if (line.endsWith('*/'))
217
+ return true;
218
+ return false;
219
+ }
220
+ return false;
221
+ }
222
+ function hasPythonDocstring(lines, symbolIndex) {
223
+ for (let index = symbolIndex + 1; index < lines.length; index += 1) {
224
+ const line = lines[index].trim();
225
+ if (!line)
226
+ continue;
227
+ return line.startsWith('"""') || line.startsWith("'''");
228
+ }
229
+ return false;
230
+ }
231
+ function parseSignature(code) {
232
+ const trimmed = code.trim();
233
+ const kind = /\bclass\b/.test(trimmed) ? 'class' : 'function';
234
+ const name = trimmed.match(/\b(?:function|class|def)\s+([A-Za-z_$][\w$]*)/)?.[1] ??
235
+ trimmed.match(/\bconst\s+([A-Za-z_$][\w$]*)\b/)?.[1];
236
+ const paramSource = trimmed.match(/\((.*)\)/)?.[1] ?? '';
237
+ const params = splitParams(paramSource).map(cleanParamName).filter(Boolean);
238
+ const throws = /\bthrow\b/.test(trimmed);
239
+ const returns = kind === 'function' && !/\bconstructor\s*\(/.test(trimmed);
240
+ return {
241
+ kind,
242
+ name,
243
+ params,
244
+ returns,
245
+ throws,
246
+ };
247
+ }
248
+ function splitParams(input) {
249
+ if (!input.trim())
250
+ return [];
251
+ const params = [];
252
+ let current = '';
253
+ let depth = 0;
254
+ for (const char of input) {
255
+ if (char === ',' && depth === 0) {
256
+ params.push(current.trim());
257
+ current = '';
258
+ continue;
259
+ }
260
+ if (char === '(' || char === '[' || char === '{' || char === '<')
261
+ depth += 1;
262
+ if (char === ')' || char === ']' || char === '}' || char === '>')
263
+ depth = Math.max(0, depth - 1);
264
+ current += char;
265
+ }
266
+ if (current.trim())
267
+ params.push(current.trim());
268
+ return params;
269
+ }
270
+ function cleanParamName(param) {
271
+ return param
272
+ .replace(/^\.{3}/, '')
273
+ .replace(/[:=].*$/, '')
274
+ .replace(/\?$/, '')
275
+ .replace(/^\{.*\}$/s, 'options')
276
+ .replace(/^\[.*\]$/s, 'items')
277
+ .trim();
278
+ }
279
+ function buildJsLikeDoc(signature, options) {
280
+ const lines = [
281
+ '/**',
282
+ ` * ${signature.name ? `Describe ${signature.name}.` : 'Describe this symbol.'}`,
283
+ ];
284
+ for (const param of signature.params) {
285
+ lines.push(` * @param ${param} - Describe ${param}.`);
286
+ }
287
+ if (signature.returns) {
288
+ lines.push(' * @returns Describe the return value.');
289
+ }
290
+ if (options.includeThrows) {
291
+ lines.push(' * @throws {Error} Describe when this throws.');
292
+ }
293
+ lines.push(' */');
294
+ return lines.join('\n');
295
+ }
296
+ function buildPythonDoc(signature, style) {
297
+ if (style === 'numpy') {
298
+ const lines = ['"""Describe this symbol.', ''];
299
+ if (signature.params.length) {
300
+ lines.push('Parameters', '----------');
301
+ for (const param of signature.params) {
302
+ lines.push(`${param} : type`, ` Describe ${param}.`);
303
+ }
304
+ lines.push('');
305
+ }
306
+ if (signature.returns) {
307
+ lines.push('Returns', '-------', 'type', ' Describe the return value.', '');
308
+ }
309
+ lines.push('"""');
310
+ return lines.join('\n');
311
+ }
312
+ const lines = ['"""Describe this symbol.', ''];
313
+ if (signature.params.length) {
314
+ lines.push('Args:');
315
+ for (const param of signature.params) {
316
+ lines.push(` ${param}: Describe ${param}.`);
317
+ }
318
+ lines.push('');
319
+ }
320
+ if (signature.returns) {
321
+ lines.push('Returns:', ' Describe the return value.', '');
322
+ }
323
+ lines.push('"""');
324
+ return lines.join('\n');
325
+ }
326
+ function indentDoc(doc, indent) {
327
+ return doc
328
+ .split('\n')
329
+ .map((line) => `${indent}${line}`)
330
+ .join('\n');
331
+ }
332
+ function nextIndent(indent) {
333
+ return `${indent} `;
334
+ }
335
+ function normalizeStyle(style, filePath) {
336
+ if (style) {
337
+ const normalized = style.toLowerCase();
338
+ if (DOC_STYLES.has(normalized))
339
+ return normalized;
340
+ }
341
+ if (filePath) {
342
+ const ext = path.extname(filePath).toLowerCase();
343
+ if (ext === '.ts' || ext === '.tsx')
344
+ return 'tsdoc';
345
+ if (ext === '.py')
346
+ return 'google';
347
+ }
348
+ return 'jsdoc';
349
+ }
350
+ function detectLanguage(filePath) {
351
+ const ext = path.extname(filePath).toLowerCase();
352
+ if (ext === '.py')
353
+ return 'python';
354
+ if (ext === '.ts' || ext === '.tsx')
355
+ return 'typescript';
356
+ return 'javascript';
357
+ }
358
+ function safeReadFile(filePath) {
359
+ try {
360
+ return fs.readFileSync(filePath, 'utf8');
361
+ }
362
+ catch {
363
+ return undefined;
364
+ }
365
+ }
366
+ function parseDocArgs(args) {
367
+ const positional = [];
368
+ let all = false;
369
+ let overwrite = false;
370
+ let style;
371
+ for (let index = 0; index < args.length; index += 1) {
372
+ const arg = args[index];
373
+ if (arg === '--all') {
374
+ all = true;
375
+ continue;
376
+ }
377
+ if (arg === '--overwrite') {
378
+ overwrite = true;
379
+ continue;
380
+ }
381
+ if (arg === '--style') {
382
+ style = args[index + 1];
383
+ index += 1;
384
+ if (!style) {
385
+ return { all, overwrite, error: 'missing value for --style' };
386
+ }
387
+ if (!DOC_STYLES.has(style.toLowerCase())) {
388
+ return { all, overwrite, error: `unsupported doc style: ${style}` };
389
+ }
390
+ continue;
391
+ }
392
+ positional.push(arg);
393
+ }
394
+ if (all)
395
+ return { all, overwrite, style };
396
+ return {
397
+ all,
398
+ overwrite,
399
+ style,
400
+ file: positional[0],
401
+ symbol: positional[1],
402
+ };
403
+ }
404
+ function groupByFile(symbols) {
405
+ const grouped = new Map();
406
+ for (const symbol of symbols) {
407
+ const entries = grouped.get(symbol.file) ?? [];
408
+ entries.push(symbol);
409
+ grouped.set(symbol.file, entries);
410
+ }
411
+ return grouped;
412
+ }
@@ -0,0 +1,152 @@
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
+ export function runDiagnostics() {
6
+ const home = os.homedir();
7
+ const rcPath = path.join(home, '.icopilotrc.json');
8
+ const icopilotDir = path.join(home, '.icopilot');
9
+ return [
10
+ {
11
+ name: 'GITHUB_TOKEN',
12
+ status: process.env.GITHUB_TOKEN ? 'ok' : 'fail',
13
+ message: process.env.GITHUB_TOKEN ? 'set' : 'not set',
14
+ },
15
+ {
16
+ name: '~/.icopilotrc.json',
17
+ status: fs.existsSync(rcPath) ? 'ok' : 'warn',
18
+ message: fs.existsSync(rcPath) ? 'found' : 'using defaults',
19
+ },
20
+ nodeVersionCheck(),
21
+ gitCheck(),
22
+ {
23
+ name: '~/.icopilot/',
24
+ status: fs.existsSync(icopilotDir) ? 'ok' : 'warn',
25
+ message: fs.existsSync(icopilotDir) ? 'found' : 'will be created on first use',
26
+ },
27
+ sessionDirectoryCheck(home),
28
+ mcpConfigCheck(home),
29
+ ];
30
+ }
31
+ export function formatDiagnostics(checks) {
32
+ const lines = checks.map((check) => {
33
+ const icon = statusIcon(check.status);
34
+ const label = statusLabel(check.status);
35
+ return ` ${icon} ${theme.hl(check.name)} ${theme.dim(`(${label})`)} ${check.message}`;
36
+ });
37
+ return `${theme.brand('iCopilot doctor')}\n${lines.join('\n')}\n`;
38
+ }
39
+ function nodeVersionCheck() {
40
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
41
+ const ok = Number.isFinite(major) && major >= 18;
42
+ return {
43
+ name: 'Node.js >= 18',
44
+ status: ok ? 'ok' : 'fail',
45
+ message: `detected ${process.versions.node}`,
46
+ };
47
+ }
48
+ function gitCheck() {
49
+ const available = hasGitOnPath();
50
+ return {
51
+ name: 'git',
52
+ status: available ? 'ok' : 'warn',
53
+ message: available ? 'available on PATH' : 'not found on PATH',
54
+ };
55
+ }
56
+ function sessionDirectoryCheck(home) {
57
+ const sessionDir = process.env.ICOPILOT_SESSION_DIR || path.join(home, '.terminal-copilot', 'sessions');
58
+ const writable = isWritableDirectory(sessionDir);
59
+ return {
60
+ name: 'session directory',
61
+ status: writable ? 'ok' : 'fail',
62
+ message: writable ? sessionDir : `not writable: ${sessionDir}`,
63
+ };
64
+ }
65
+ function mcpConfigCheck(home) {
66
+ const hasUserConfig = fs.existsSync(path.join(home, '.icopilot', 'mcp.json'));
67
+ const hasProjectConfig = fs.existsSync(path.join(process.cwd(), '.mcp.json'));
68
+ return {
69
+ name: 'MCP config',
70
+ status: hasUserConfig || hasProjectConfig ? 'ok' : 'warn',
71
+ message: hasProjectConfig || hasUserConfig
72
+ ? hasProjectConfig
73
+ ? '.mcp.json found'
74
+ : '~/.icopilot/mcp.json found'
75
+ : 'no MCP servers configured',
76
+ };
77
+ }
78
+ function statusIcon(status) {
79
+ switch (status) {
80
+ case 'ok':
81
+ return theme.ok('✔');
82
+ case 'warn':
83
+ return theme.warn('⚠');
84
+ case 'fail':
85
+ return theme.err('✖');
86
+ }
87
+ }
88
+ function statusLabel(status) {
89
+ switch (status) {
90
+ case 'ok':
91
+ return 'ok';
92
+ case 'warn':
93
+ return 'warn';
94
+ case 'fail':
95
+ return 'fail';
96
+ }
97
+ }
98
+ function hasGitOnPath() {
99
+ const candidates = new Set();
100
+ const pathValue = process.env.PATH || '';
101
+ const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
102
+ const executableNames = process.platform === 'win32' ? ['git.exe', 'git.cmd', 'git.bat', 'git'] : ['git'];
103
+ for (const dir of pathEntries) {
104
+ for (const executableName of executableNames) {
105
+ candidates.add(path.join(dir, executableName));
106
+ }
107
+ }
108
+ if (process.platform === 'win32') {
109
+ candidates.add(path.join('C:\\Program Files\\Git\\cmd', 'git.exe'));
110
+ candidates.add(path.join('C:\\Program Files\\Git\\bin', 'git.exe'));
111
+ candidates.add(path.join('C:\\Program Files (x86)\\Git\\cmd', 'git.exe'));
112
+ candidates.add(path.join('C:\\Program Files (x86)\\Git\\bin', 'git.exe'));
113
+ }
114
+ else {
115
+ candidates.add('/usr/bin/git');
116
+ candidates.add('/usr/local/bin/git');
117
+ }
118
+ for (const candidate of candidates) {
119
+ try {
120
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
121
+ return true;
122
+ }
123
+ catch {
124
+ // Ignore unreadable paths and keep checking.
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+ function isWritableDirectory(target) {
130
+ const existing = nearestExistingPath(target);
131
+ if (!existing)
132
+ return false;
133
+ try {
134
+ const stats = fs.statSync(existing);
135
+ const probe = stats.isDirectory() ? existing : path.dirname(existing);
136
+ fs.accessSync(probe, fs.constants.W_OK);
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ function nearestExistingPath(target) {
144
+ let current = path.resolve(target);
145
+ while (!fs.existsSync(current)) {
146
+ const parent = path.dirname(current);
147
+ if (parent === current)
148
+ return null;
149
+ current = parent;
150
+ }
151
+ return current;
152
+ }