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,109 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const scheduled = new Map();
3
+ let currentRunner = null;
4
+ export function setScheduleRunner(runner) {
5
+ currentRunner = runner;
6
+ }
7
+ export function scheduleRecurring(interval, prompt) {
8
+ return scheduleTask('recurring', interval, prompt);
9
+ }
10
+ export function scheduleOnce(delay, prompt) {
11
+ return scheduleTask('once', delay, prompt);
12
+ }
13
+ export function listScheduled() {
14
+ return [...scheduled.values()]
15
+ .map((entry) => ({ ...entry.task, nextRun: new Date(entry.task.nextRun) }))
16
+ .sort((left, right) => left.nextRun.getTime() - right.nextRun.getTime());
17
+ }
18
+ export function cancelSchedule(id) {
19
+ const entry = scheduled.get(id);
20
+ if (!entry)
21
+ return false;
22
+ clearTimer(entry.task.type, entry.timer);
23
+ scheduled.delete(id);
24
+ return true;
25
+ }
26
+ export function resetScheduledTasks() {
27
+ for (const entry of scheduled.values()) {
28
+ clearTimer(entry.task.type, entry.timer);
29
+ }
30
+ scheduled.clear();
31
+ }
32
+ export function parseInterval(input) {
33
+ const trimmed = input.trim().toLowerCase();
34
+ if (!trimmed)
35
+ throw new Error('interval is required');
36
+ const pattern = /(\d+)([hms])/g;
37
+ let total = 0;
38
+ let consumed = 0;
39
+ let match;
40
+ while ((match = pattern.exec(trimmed)) !== null) {
41
+ const value = Number(match[1]);
42
+ const unit = match[2];
43
+ consumed += match[0].length;
44
+ total += value * unitToMs(unit);
45
+ }
46
+ if (consumed !== trimmed.length || total <= 0) {
47
+ throw new Error(`invalid interval: ${input}`);
48
+ }
49
+ return total;
50
+ }
51
+ function scheduleTask(type, intervalSource, prompt) {
52
+ const normalizedPrompt = prompt.trim();
53
+ if (!normalizedPrompt)
54
+ throw new Error('prompt is required');
55
+ if (!currentRunner)
56
+ throw new Error('scheduled prompts are not available in this session');
57
+ const interval = parseInterval(intervalSource);
58
+ const task = {
59
+ id: randomUUID(),
60
+ type,
61
+ interval,
62
+ prompt: normalizedPrompt,
63
+ nextRun: new Date(Date.now() + interval),
64
+ };
65
+ const run = currentRunner;
66
+ if (type === 'recurring') {
67
+ const timer = setInterval(() => {
68
+ void fireTask(task.id);
69
+ }, interval);
70
+ scheduled.set(task.id, { task, timer, run });
71
+ }
72
+ else {
73
+ const timer = setTimeout(() => {
74
+ void fireTask(task.id);
75
+ }, interval);
76
+ scheduled.set(task.id, { task, timer, run });
77
+ }
78
+ return { ...task, nextRun: new Date(task.nextRun) };
79
+ }
80
+ async function fireTask(id) {
81
+ const entry = scheduled.get(id);
82
+ if (!entry)
83
+ return;
84
+ if (entry.task.type === 'once') {
85
+ scheduled.delete(id);
86
+ }
87
+ else {
88
+ entry.task.nextRun = new Date(Date.now() + entry.task.interval);
89
+ }
90
+ await entry.run(entry.task.prompt);
91
+ }
92
+ function clearTimer(type, timer) {
93
+ if (type === 'recurring')
94
+ clearInterval(timer);
95
+ else
96
+ clearTimeout(timer);
97
+ }
98
+ function unitToMs(unit) {
99
+ switch (unit) {
100
+ case 'h':
101
+ return 60 * 60 * 1000;
102
+ case 'm':
103
+ return 60 * 1000;
104
+ case 's':
105
+ return 1000;
106
+ default:
107
+ throw new Error(`unsupported interval unit: ${unit}`);
108
+ }
109
+ }
@@ -0,0 +1,47 @@
1
+ import { searchIndex } from '../index/store.js';
2
+ import { theme } from '../ui/theme.js';
3
+ const DEFAULT_LIMIT = 6;
4
+ const PREVIEW_LIMIT = 180;
5
+ export async function searchCommand(args, cwd) {
6
+ const query = args.join(' ').trim();
7
+ if (!query) {
8
+ return theme.warn('usage: /search <query>\n');
9
+ }
10
+ try {
11
+ const hits = await searchIndex(cwd, query, DEFAULT_LIMIT);
12
+ if (!hits.length) {
13
+ return theme.dim('No matches found.\n');
14
+ }
15
+ return formatResults(query, hits);
16
+ }
17
+ catch (error) {
18
+ if (isMissingIndexError(error)) {
19
+ return theme.warn('No index found. Run `/index build` first.\n');
20
+ }
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ return theme.err(`search failed: ${message}\n`);
23
+ }
24
+ }
25
+ function formatResults(query, hits) {
26
+ const lines = [`${theme.brand('Search results')} ${theme.dim(`for "${query}"`)}`, ''];
27
+ for (const hit of hits) {
28
+ lines.push(`${theme.hl(hit.file)} ${theme.dim(`(score ${hit.score.toFixed(3)})`)}`);
29
+ lines.push(` ${formatPreview(hit.text)}`);
30
+ lines.push('');
31
+ }
32
+ return `${lines.join('\n').trimEnd()}\n`;
33
+ }
34
+ function formatPreview(text) {
35
+ const normalized = text.replace(/\s+/g, ' ').trim();
36
+ if (normalized.length <= PREVIEW_LIMIT)
37
+ return normalized;
38
+ return `${normalized.slice(0, PREVIEW_LIMIT - 3)}...`;
39
+ }
40
+ function isMissingIndexError(error) {
41
+ if (typeof error !== 'object' || error === null) {
42
+ return false;
43
+ }
44
+ const code = 'code' in error ? String(error.code ?? '') : '';
45
+ const message = 'message' in error ? String(error.message ?? '') : '';
46
+ return code === 'ENOENT' || /no index found|index not found/i.test(message);
47
+ }
@@ -0,0 +1,156 @@
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
+ export const SCANNABLE_EXTENSIONS = [
6
+ '.ts',
7
+ '.js',
8
+ '.py',
9
+ '.env',
10
+ '.yml',
11
+ '.yaml',
12
+ '.json',
13
+ '.toml',
14
+ '.cfg',
15
+ '.ini',
16
+ '.conf',
17
+ '.sh',
18
+ '.bash',
19
+ '.rb',
20
+ '.go',
21
+ '.java',
22
+ '.cs',
23
+ '.php',
24
+ ];
25
+ const MAX_FILES = 1000;
26
+ const MAX_LINES = 500;
27
+ const SECURITY_PATTERNS = [
28
+ {
29
+ name: 'AWS key',
30
+ regex: /AKIA[0-9A-Z]{16}/,
31
+ severity: 'high',
32
+ description: 'Possible AWS access key exposed in source.',
33
+ },
34
+ {
35
+ name: 'Private key',
36
+ regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
37
+ severity: 'high',
38
+ description: 'Private key material should never be committed.',
39
+ },
40
+ {
41
+ name: 'GitHub token',
42
+ regex: /gh[pousr]_[A-Za-z0-9_]{36,}/,
43
+ severity: 'high',
44
+ description: 'Possible GitHub token found in a tracked file.',
45
+ },
46
+ {
47
+ name: 'Password in code',
48
+ regex: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}/i,
49
+ severity: 'medium',
50
+ description: 'Literal password-like value detected in code or config.',
51
+ },
52
+ {
53
+ name: 'API key',
54
+ regex: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9]{20,}/i,
55
+ severity: 'medium',
56
+ description: 'API key assignment detected; consider environment-based secrets.',
57
+ },
58
+ {
59
+ name: 'Generic secret',
60
+ regex: /(?:secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=]{20,}/i,
61
+ severity: 'low',
62
+ description: 'Secret or token-like literal detected.',
63
+ },
64
+ ];
65
+ function isEnvFile(filePath) {
66
+ const baseName = path.basename(filePath).toLowerCase();
67
+ return baseName === '.env' || baseName.startsWith('.env.');
68
+ }
69
+ function isScannableFile(filePath) {
70
+ if (isEnvFile(filePath))
71
+ return true;
72
+ return SCANNABLE_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
73
+ }
74
+ function readLines(filePath) {
75
+ try {
76
+ return fs.readFileSync(filePath, 'utf8').split(/\r?\n/).slice(0, MAX_LINES);
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ }
82
+ function listCandidateFiles(cwd) {
83
+ return fg
84
+ .sync('**/*', {
85
+ cwd,
86
+ absolute: true,
87
+ onlyFiles: true,
88
+ dot: true,
89
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
90
+ })
91
+ .filter(isScannableFile)
92
+ .slice(0, MAX_FILES);
93
+ }
94
+ export function scanFilesForSecrets(cwd, relativePaths) {
95
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory())
96
+ return [];
97
+ const findings = [];
98
+ for (const relativePath of relativePaths) {
99
+ const normalizedRelativePath = path.normalize(relativePath);
100
+ const filePath = path.join(cwd, normalizedRelativePath);
101
+ if (!isScannableFile(filePath) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
102
+ continue;
103
+ }
104
+ const lines = readLines(filePath);
105
+ lines.forEach((lineText, index) => {
106
+ for (const pattern of SECURITY_PATTERNS) {
107
+ if (pattern.regex.test(lineText)) {
108
+ findings.push({
109
+ file: normalizedRelativePath,
110
+ line: index + 1,
111
+ pattern: pattern.name,
112
+ severity: pattern.severity,
113
+ description: pattern.description,
114
+ });
115
+ }
116
+ }
117
+ });
118
+ }
119
+ return findings;
120
+ }
121
+ export function scanForSecrets(cwd) {
122
+ return scanFilesForSecrets(cwd, listCandidateFiles(cwd).map((filePath) => path.relative(cwd, filePath)));
123
+ }
124
+ function severityColor(severity) {
125
+ switch (severity) {
126
+ case 'high':
127
+ return theme.err;
128
+ case 'medium':
129
+ return theme.warn;
130
+ case 'low':
131
+ return theme.dim;
132
+ }
133
+ }
134
+ function formatGroup(severity, findings) {
135
+ if (findings.length === 0)
136
+ return '';
137
+ const color = severityColor(severity);
138
+ const title = color(`${severity.toUpperCase()} (${findings.length})`);
139
+ const lines = findings.map((finding) => ` ${color(finding.file)}:${finding.line} ${finding.pattern} ${theme.dim(`- ${finding.description}`)}`);
140
+ return `${title}\n${lines.join('\n')}`;
141
+ }
142
+ export function securityCommand(cwd) {
143
+ const findings = scanForSecrets(cwd);
144
+ if (findings.length === 0) {
145
+ return `${theme.ok('No obvious security issues detected.')}\n${theme.dim('Scanned common secret and credential patterns in source files.')}\n`;
146
+ }
147
+ const grouped = {
148
+ high: findings.filter((finding) => finding.severity === 'high'),
149
+ medium: findings.filter((finding) => finding.severity === 'medium'),
150
+ low: findings.filter((finding) => finding.severity === 'low'),
151
+ };
152
+ const sections = ['high', 'medium', 'low']
153
+ .map((severity) => formatGroup(severity, grouped[severity]))
154
+ .filter(Boolean);
155
+ return `${theme.brand('Security findings')}\n${sections.join('\n\n')}\n`;
156
+ }
@@ -0,0 +1,238 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config, getDefaultConfig, rcFilePath, } from '../config.js';
4
+ import { theme } from '../ui/theme.js';
5
+ const BOOLEAN_TRUE = /^(1|true|yes|on)$/i;
6
+ const BOOLEAN_FALSE = /^(0|false|no|off)$/i;
7
+ const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
8
+ const THEMES = ['auto', 'light', 'dark', 'none'];
9
+ const REASONING_LEVELS = ['low', 'medium', 'high', 'max'];
10
+ const KEYBINDING_MODES = ['default', 'vi', 'emacs'];
11
+ const SETTING_DEFINITIONS = [
12
+ textSetting('model', 'defaultModel', ['defaultModel']),
13
+ enumSetting('theme', 'theme', THEMES),
14
+ enumSetting('editFormat', 'editFormat', ['whole', 'diff'], ['edit-format']),
15
+ booleanSetting('autoLint', 'autoLint', ['auto-lint']),
16
+ booleanSetting('autoTest', 'autoTest', ['auto-test']),
17
+ optionalEnumSetting('reasoningEffort', 'reasoningEffort', REASONING_LEVELS, [
18
+ 'reasoning',
19
+ 'reasoning-effort',
20
+ ]),
21
+ optionalNumberSetting('thinkTokens', 'thinkTokens', ['think-tokens']),
22
+ booleanSetting('sandbox', 'sandbox'),
23
+ numberSetting('contextWindow', 'contextWindow', ['context-window']),
24
+ numberSetting('contextWarn', 'contextWarn', ['context-warn']),
25
+ booleanSetting('autoCompact', 'autoCompact', ['auto-compact']),
26
+ numberSetting('autoCompactThreshold', 'autoCompactThreshold', ['auto-compact-threshold']),
27
+ booleanSetting('verbose', 'verbose'),
28
+ enumSetting('logLevel', 'logLevel', LOG_LEVELS, ['log-level']),
29
+ textSetting('sessionDir', 'sessionDir', ['session-dir']),
30
+ textSetting('endpoint', 'endpoint'),
31
+ optionalTextSetting('policyPath', 'policyPath', ['policy', 'policy-path']),
32
+ booleanSetting('jsonOutput', 'jsonOutput', ['json', 'json-output']),
33
+ booleanSetting('quiet', 'quiet'),
34
+ booleanSetting('autoApprove', 'autoApprove', ['auto-approve']),
35
+ {
36
+ key: 'keybindings',
37
+ configKey: 'keybindings',
38
+ aliases: ['keybinding'],
39
+ parse: (value) => {
40
+ const mode = value.trim().toLowerCase();
41
+ if (!KEYBINDING_MODES.includes(mode)) {
42
+ throw new Error(`keybindings must be one of: ${KEYBINDING_MODES.join(', ')}`);
43
+ }
44
+ return { mode };
45
+ },
46
+ format: (value) => {
47
+ if (!value || typeof value !== 'object' || !('mode' in value))
48
+ return 'default';
49
+ return value.mode;
50
+ },
51
+ },
52
+ ];
53
+ export function showSettings() {
54
+ const defaults = getDefaultConfig();
55
+ const persisted = loadRcData();
56
+ const keyWidth = Math.max(...SETTING_DEFINITIONS.map((entry) => entry.key.length), 'setting'.length) + 2;
57
+ const valueWidth = 18;
58
+ const defaultWidth = 18;
59
+ const lines = [
60
+ theme.brand('Settings'),
61
+ ` ${pad('setting', keyWidth)}${pad('value', valueWidth)}${pad('default', defaultWidth)}persisted`,
62
+ ` ${theme.dim('-'.repeat(keyWidth + valueWidth + defaultWidth + 'persisted'.length))}`,
63
+ ];
64
+ for (const definition of SETTING_DEFINITIONS) {
65
+ const current = formatValue(definition, config[definition.configKey]);
66
+ const fallback = formatValue(definition, defaults[definition.configKey]);
67
+ const stored = [definition.key, definition.configKey, ...(definition.aliases ?? [])].some((candidate) => Object.prototype.hasOwnProperty.call(persisted, candidate));
68
+ lines.push(` ${pad(definition.key, keyWidth)}${pad(current, valueWidth)}${pad(fallback, defaultWidth)}${stored ? theme.ok('yes') : theme.dim('no')}`);
69
+ }
70
+ lines.push('', ` rc file: ${theme.dim(rcFilePath())}`, '');
71
+ return lines.join('\n');
72
+ }
73
+ export function setSetting(key, value) {
74
+ const definition = resolveSetting(key);
75
+ const trimmedValue = value.trim();
76
+ if (!trimmedValue) {
77
+ throw new Error(`value is required for setting: ${definition.key}`);
78
+ }
79
+ const parsed = definition.parse(trimmedValue);
80
+ applyRuntimeSetting(definition, parsed);
81
+ const rcData = loadRcData();
82
+ rcData[definition.key] = toPersistedValue(parsed);
83
+ writeRcData(rcData);
84
+ return theme.ok(`✔ setting ${definition.key} → ${formatValue(definition, parsed)}\n`);
85
+ }
86
+ export function resetSetting(key) {
87
+ const definition = resolveSetting(key);
88
+ const defaults = getDefaultConfig();
89
+ applyRuntimeSetting(definition, defaults[definition.configKey]);
90
+ const rcData = loadRcData();
91
+ delete rcData[definition.key];
92
+ writeRcData(rcData);
93
+ return theme.ok(`✔ reset ${definition.key} → ${formatValue(definition, defaults[definition.configKey])}\n`);
94
+ }
95
+ export function isModelSettingKey(key) {
96
+ return resolveSetting(key).key === 'model';
97
+ }
98
+ function resolveSetting(key) {
99
+ const normalized = key.trim().toLowerCase();
100
+ const definition = SETTING_DEFINITIONS.find((entry) => [entry.key, ...(entry.aliases ?? [])].some((candidate) => candidate.toLowerCase() === normalized));
101
+ if (!definition) {
102
+ throw new Error(`unsupported setting: ${key}`);
103
+ }
104
+ return definition;
105
+ }
106
+ function loadRcData() {
107
+ const file = rcFilePath();
108
+ if (!fs.existsSync(file))
109
+ return {};
110
+ try {
111
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
112
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
113
+ ? { ...parsed }
114
+ : {};
115
+ }
116
+ catch {
117
+ return {};
118
+ }
119
+ }
120
+ function writeRcData(data) {
121
+ const file = rcFilePath();
122
+ fs.mkdirSync(path.dirname(file), { recursive: true });
123
+ const entries = Object.entries(data).filter(([, value]) => value !== undefined);
124
+ const sorted = Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
125
+ fs.writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8');
126
+ }
127
+ function applyRuntimeSetting(definition, value) {
128
+ const mutableConfig = config;
129
+ mutableConfig[definition.configKey] = value;
130
+ if (definition.key === 'sandbox') {
131
+ if (value)
132
+ process.env.ICOPILOT_SANDBOX = '1';
133
+ else
134
+ delete process.env.ICOPILOT_SANDBOX;
135
+ }
136
+ }
137
+ function formatValue(definition, value) {
138
+ if (value === undefined || value === null || value === '')
139
+ return theme.dim('(default)');
140
+ if (definition.format)
141
+ return definition.format(value);
142
+ if (typeof value === 'boolean')
143
+ return value ? 'true' : 'false';
144
+ return String(value);
145
+ }
146
+ function toPersistedValue(value) {
147
+ return value === undefined ? undefined : value;
148
+ }
149
+ function parseBoolean(value) {
150
+ if (BOOLEAN_TRUE.test(value))
151
+ return true;
152
+ if (BOOLEAN_FALSE.test(value))
153
+ return false;
154
+ throw new Error(`invalid boolean: ${value}`);
155
+ }
156
+ function pad(value, width) {
157
+ return `${value}`.padEnd(width);
158
+ }
159
+ function textSetting(key, configKey, aliases = []) {
160
+ return {
161
+ key,
162
+ configKey,
163
+ aliases,
164
+ parse: (value) => value,
165
+ };
166
+ }
167
+ function optionalTextSetting(key, configKey, aliases = []) {
168
+ return {
169
+ key,
170
+ configKey,
171
+ aliases,
172
+ parse: (value) => (/^(default|none|off)$/i.test(value) ? undefined : value),
173
+ };
174
+ }
175
+ function booleanSetting(key, configKey, aliases = []) {
176
+ return {
177
+ key,
178
+ configKey,
179
+ aliases,
180
+ parse: parseBoolean,
181
+ };
182
+ }
183
+ function enumSetting(key, configKey, allowed, aliases = []) {
184
+ return {
185
+ key,
186
+ configKey,
187
+ aliases,
188
+ parse: (value) => {
189
+ const normalized = value.trim().toLowerCase();
190
+ const mapped = normalized === 'no-color' ? 'none' : normalized;
191
+ const match = allowed.find((entry) => entry === mapped);
192
+ if (!match) {
193
+ throw new Error(`invalid ${key}: ${value}`);
194
+ }
195
+ return match;
196
+ },
197
+ };
198
+ }
199
+ function optionalEnumSetting(key, configKey, allowed, aliases = []) {
200
+ return {
201
+ key,
202
+ configKey,
203
+ aliases,
204
+ parse: (value) => {
205
+ if (/^(default|none|off)$/i.test(value))
206
+ return undefined;
207
+ return enumSetting(key, configKey, allowed, aliases).parse(value);
208
+ },
209
+ format: (value) => (value === undefined ? theme.dim('(default)') : String(value)),
210
+ };
211
+ }
212
+ function numberSetting(key, configKey, aliases = []) {
213
+ return {
214
+ key,
215
+ configKey,
216
+ aliases,
217
+ parse: (value) => {
218
+ const parsed = Number(value);
219
+ if (!Number.isFinite(parsed)) {
220
+ throw new Error(`invalid number: ${value}`);
221
+ }
222
+ return parsed;
223
+ },
224
+ };
225
+ }
226
+ function optionalNumberSetting(key, configKey, aliases = []) {
227
+ return {
228
+ key,
229
+ configKey,
230
+ aliases,
231
+ parse: (value) => {
232
+ if (/^(default|none|off)$/i.test(value))
233
+ return undefined;
234
+ return numberSetting(key, configKey, aliases).parse(value);
235
+ },
236
+ format: (value) => (value === undefined ? theme.dim('(default)') : String(value)),
237
+ };
238
+ }