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,107 @@
1
+ import { SlackNotificationHandler } from '../extensions/slack-provider.js';
2
+ import { TeamsNotificationHandler } from '../extensions/teams-provider.js';
3
+ import { registerNotificationHandler } from '../extensions/team.js';
4
+ const logDebug = (msg) => {
5
+ if (process.env.ICOPILOT_LOG_LEVEL === 'debug') {
6
+ process.stderr.write(`[notifications] ${msg}\n`);
7
+ }
8
+ };
9
+ export class NotificationManager {
10
+ config;
11
+ handler = null;
12
+ constructor(config) {
13
+ this.config = config ?? null;
14
+ this.initializeHandler();
15
+ }
16
+ initializeHandler() {
17
+ if (!this.config || !this.config.token || !this.config.channel) {
18
+ logDebug('No notification config provided');
19
+ return;
20
+ }
21
+ try {
22
+ let handler;
23
+ if (this.config.provider === 'slack') {
24
+ handler = new SlackNotificationHandler(this.config.token, this.config.channel);
25
+ logDebug('Initialized Slack notification handler');
26
+ }
27
+ else if (this.config.provider === 'teams') {
28
+ handler = new TeamsNotificationHandler(this.config.token, this.config.channel, '');
29
+ logDebug('Initialized Teams notification handler');
30
+ }
31
+ else {
32
+ logDebug(`Unknown notification provider: ${this.config.provider}`);
33
+ return;
34
+ }
35
+ this.handler = handler;
36
+ registerNotificationHandler(handler);
37
+ }
38
+ catch (error) {
39
+ logDebug(`Failed to initialize notification handler: ${error}`);
40
+ }
41
+ }
42
+ async sendNotification(message, options) {
43
+ if (!this.handler || !this.config) {
44
+ logDebug('Notification handler not configured');
45
+ return;
46
+ }
47
+ const channel = options?.channel || this.config.channel;
48
+ try {
49
+ await this.handler.notify(channel, message, options?.metadata);
50
+ logDebug(`Sent notification to ${channel}`);
51
+ }
52
+ catch (error) {
53
+ const msg = error instanceof Error ? error.message : String(error);
54
+ process.stderr.write(`[notifications] Failed to send notification: ${msg}\n`);
55
+ }
56
+ }
57
+ async requestUserApproval(action, details = {}) {
58
+ if (!this.handler || !this.config) {
59
+ logDebug('Notification handler not configured');
60
+ return { approved: false };
61
+ }
62
+ try {
63
+ const result = await this.handler.requestApproval(this.config.channel, action, details);
64
+ logDebug(`Approval request completed: ${action} - ${result.approved ? 'approved' : 'denied'}`);
65
+ return result;
66
+ }
67
+ catch (error) {
68
+ const msg = error instanceof Error ? error.message : String(error);
69
+ process.stderr.write(`[notifications] Failed to request approval: ${msg}\n`);
70
+ return { approved: false };
71
+ }
72
+ }
73
+ async getApprovalStatus(id) {
74
+ return null;
75
+ }
76
+ formatOutput(content, type = 'text') {
77
+ if (type === 'error') {
78
+ return `\`\`\`\n❌ Error:\n${content}\n\`\`\``;
79
+ }
80
+ if (type === 'code') {
81
+ return `\`\`\`\n${content}\n\`\`\``;
82
+ }
83
+ return content;
84
+ }
85
+ async getStatus() {
86
+ if (!this.handler) {
87
+ return { connected: false, error: 'No handler configured' };
88
+ }
89
+ return this.handler.getStatus();
90
+ }
91
+ isConfigured() {
92
+ return this.handler != null && this.config != null;
93
+ }
94
+ getConfig() {
95
+ return this.config;
96
+ }
97
+ }
98
+ let globalManager = null;
99
+ export function initializeNotificationManager(config) {
100
+ globalManager = new NotificationManager(config);
101
+ }
102
+ export function getNotificationManager() {
103
+ if (!globalManager) {
104
+ globalManager = new NotificationManager(null);
105
+ }
106
+ return globalManager;
107
+ }
@@ -0,0 +1,244 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { theme } from '../ui/theme.js';
5
+ const DEFAULT_PLUGINS = [
6
+ {
7
+ name: 'azure-tools',
8
+ version: '1.2.0',
9
+ description: 'Azure deployment helpers, diagnostics, and workflow shortcuts.',
10
+ author: 'iCopilot',
11
+ installed: false,
12
+ },
13
+ {
14
+ name: 'jira-sync',
15
+ version: '0.8.1',
16
+ description: 'Create and update Jira issues from terminal workflows.',
17
+ author: 'iCopilot',
18
+ installed: false,
19
+ },
20
+ {
21
+ name: 'shell-utils',
22
+ version: '2.0.3',
23
+ description: 'Reusable shell helpers for scripts, prompts, and environment checks.',
24
+ author: 'iCopilot',
25
+ installed: false,
26
+ },
27
+ ];
28
+ export class Marketplace {
29
+ registryFile;
30
+ pluginsDir;
31
+ seedPlugins;
32
+ constructor(options = {}) {
33
+ const homeDir = options.homeDir ?? os.homedir();
34
+ this.registryFile =
35
+ options.registryFile ?? path.join(homeDir, '.icopilot', 'plugin-registry.json');
36
+ this.pluginsDir = options.pluginsDir ?? path.join(homeDir, '.icopilot', 'plugins');
37
+ this.seedPlugins = dedupePlugins(options.seedPlugins ?? DEFAULT_PLUGINS);
38
+ }
39
+ async search(query) {
40
+ const normalizedQuery = query.trim().toLowerCase();
41
+ const plugins = await this.list();
42
+ if (!normalizedQuery)
43
+ return plugins;
44
+ return plugins.filter((plugin) => [plugin.name, plugin.version, plugin.description, plugin.author]
45
+ .join('\n')
46
+ .toLowerCase()
47
+ .includes(normalizedQuery));
48
+ }
49
+ async install(name) {
50
+ const registry = await this.readRegistry();
51
+ const plugin = findPlugin(registry.plugins, name);
52
+ if (!plugin) {
53
+ throw new Error(`plugin not found: ${name}`);
54
+ }
55
+ plugin.installed = true;
56
+ await this.writeRegistry(registry);
57
+ await this.writeInstalledPlugin(plugin);
58
+ return { ...plugin };
59
+ }
60
+ async uninstall(name) {
61
+ const registry = await this.readRegistry();
62
+ const plugin = findPlugin(registry.plugins, name);
63
+ if (!plugin) {
64
+ throw new Error(`plugin not found: ${name}`);
65
+ }
66
+ plugin.installed = false;
67
+ await this.writeRegistry(registry);
68
+ await fs.rm(this.pluginDir(plugin.name), { recursive: true, force: true });
69
+ return { ...plugin };
70
+ }
71
+ async update(name) {
72
+ const registry = await this.readRegistry();
73
+ const candidates = name
74
+ ? [findPlugin(registry.plugins, name)].filter((plugin) => Boolean(plugin))
75
+ : registry.plugins.filter((plugin) => plugin.installed);
76
+ if (name && candidates.length === 0) {
77
+ throw new Error(`plugin not found: ${name}`);
78
+ }
79
+ const notInstalled = candidates.find((plugin) => !plugin.installed);
80
+ if (notInstalled) {
81
+ throw new Error(`plugin is not installed: ${notInstalled.name}`);
82
+ }
83
+ for (const plugin of candidates) {
84
+ await this.writeInstalledPlugin(plugin);
85
+ }
86
+ if (candidates.length > 0) {
87
+ await this.writeRegistry(registry);
88
+ }
89
+ return candidates.map((plugin) => ({ ...plugin }));
90
+ }
91
+ async list() {
92
+ const registry = await this.readRegistry();
93
+ return registry.plugins.map((plugin) => ({ ...plugin }));
94
+ }
95
+ async getInfo(name) {
96
+ const registry = await this.readRegistry();
97
+ const plugin = findPlugin(registry.plugins, name);
98
+ return plugin ? { ...plugin } : null;
99
+ }
100
+ async readRegistry() {
101
+ await fs.mkdir(path.dirname(this.registryFile), { recursive: true });
102
+ try {
103
+ const raw = await fs.readFile(this.registryFile, 'utf8');
104
+ const parsed = JSON.parse(raw);
105
+ const plugins = mergeWithSeed(parseRegistryPlugins(parsed), this.seedPlugins);
106
+ const registry = { plugins };
107
+ await this.writeRegistry(registry);
108
+ return registry;
109
+ }
110
+ catch (error) {
111
+ if (error?.code !== 'ENOENT')
112
+ throw error;
113
+ const registry = { plugins: this.seedPlugins.map((plugin) => ({ ...plugin })) };
114
+ await this.writeRegistry(registry);
115
+ return registry;
116
+ }
117
+ }
118
+ async writeRegistry(registry) {
119
+ await fs.mkdir(path.dirname(this.registryFile), { recursive: true });
120
+ await fs.writeFile(this.registryFile, `${JSON.stringify({ plugins: dedupePlugins(registry.plugins) }, null, 2)}\n`, 'utf8');
121
+ }
122
+ async writeInstalledPlugin(plugin) {
123
+ const dir = this.pluginDir(plugin.name);
124
+ await fs.mkdir(dir, { recursive: true });
125
+ await fs.writeFile(path.join(dir, 'plugin.json'), `${JSON.stringify(plugin, null, 2)}\n`, 'utf8');
126
+ }
127
+ pluginDir(name) {
128
+ return path.join(this.pluginsDir, name);
129
+ }
130
+ }
131
+ export async function pluginCommand(args, marketplace = new Marketplace()) {
132
+ const [subcommandRaw, ...rest] = args;
133
+ const subcommand = (subcommandRaw || 'list').toLowerCase();
134
+ switch (subcommand) {
135
+ case 'search': {
136
+ const query = rest.join(' ').trim();
137
+ if (!query)
138
+ return theme.warn('usage: /plugin search <query>\n');
139
+ return formatPluginList(`Plugin search ${theme.dim(query)}`, await marketplace.search(query));
140
+ }
141
+ case 'install': {
142
+ const name = rest.join(' ').trim();
143
+ if (!name)
144
+ return theme.warn('usage: /plugin install <name>\n');
145
+ const plugin = await marketplace.install(name);
146
+ return theme.ok(`✔ installed ${plugin.name} ${theme.dim(`v${plugin.version}`)}\n`);
147
+ }
148
+ case 'uninstall': {
149
+ const name = rest.join(' ').trim();
150
+ if (!name)
151
+ return theme.warn('usage: /plugin uninstall <name>\n');
152
+ const plugin = await marketplace.uninstall(name);
153
+ return theme.ok(`✔ uninstalled ${plugin.name} ${theme.dim(`v${plugin.version}`)}\n`);
154
+ }
155
+ case 'update': {
156
+ const name = rest.join(' ').trim();
157
+ const updated = await marketplace.update(name || undefined);
158
+ if (updated.length === 0)
159
+ return theme.dim('No installed plugins to update.\n');
160
+ const scope = name ? `Updated ${updated[0].name}` : 'Updated plugins';
161
+ return formatPluginList(scope, updated);
162
+ }
163
+ case 'list':
164
+ return formatPluginList('Plugins', await marketplace.list());
165
+ case 'info': {
166
+ const name = rest.join(' ').trim();
167
+ if (!name)
168
+ return theme.warn('usage: /plugin info <name>\n');
169
+ const plugin = await marketplace.getInfo(name);
170
+ if (!plugin)
171
+ return theme.warn(`plugin not found: ${name}\n`);
172
+ return formatPluginDetails(plugin);
173
+ }
174
+ default:
175
+ return theme.warn('usage: /plugin list|search <query>|install <name>|uninstall <name>|update [name]|info <name>\n');
176
+ }
177
+ }
178
+ function formatPluginList(title, plugins) {
179
+ if (plugins.length === 0)
180
+ return `${theme.brand(title)}\n${theme.dim(' no plugins found')}\n`;
181
+ const lines = plugins.map((plugin) => ` ${theme.hl(plugin.name)} ${theme.dim(`v${plugin.version}`)} ${plugin.description} ${theme.dim(`(${plugin.author})`)}${plugin.installed ? ` ${theme.ok('[installed]')}` : ''}`);
182
+ return `${theme.brand(title)}\n${lines.join('\n')}\n`;
183
+ }
184
+ function formatPluginDetails(plugin) {
185
+ return [
186
+ `${theme.brand('Plugin')} ${theme.hl(plugin.name)} ${theme.dim(`v${plugin.version}`)}`,
187
+ ` ${plugin.description}`,
188
+ ` ${theme.dim('author:')} ${plugin.author}`,
189
+ ` ${theme.dim('installed:')} ${plugin.installed ? 'yes' : 'no'}`,
190
+ '',
191
+ ].join('\n');
192
+ }
193
+ function mergeWithSeed(plugins, seedPlugins) {
194
+ const merged = new Map();
195
+ for (const plugin of seedPlugins) {
196
+ merged.set(plugin.name.toLowerCase(), { ...plugin });
197
+ }
198
+ for (const plugin of plugins) {
199
+ const key = plugin.name.toLowerCase();
200
+ const base = merged.get(key);
201
+ merged.set(key, { ...(base ?? {}), ...plugin, installed: plugin.installed });
202
+ }
203
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
204
+ }
205
+ function parseRegistryPlugins(value) {
206
+ const rawPlugins = Array.isArray(value)
207
+ ? value
208
+ : value &&
209
+ typeof value === 'object' &&
210
+ Array.isArray(value.plugins)
211
+ ? value.plugins
212
+ : [];
213
+ return dedupePlugins(rawPlugins.filter(isPlugin));
214
+ }
215
+ function dedupePlugins(plugins) {
216
+ const deduped = new Map();
217
+ for (const plugin of plugins) {
218
+ deduped.set(plugin.name.toLowerCase(), normalizePlugin(plugin));
219
+ }
220
+ return [...deduped.values()].sort((a, b) => a.name.localeCompare(b.name));
221
+ }
222
+ function normalizePlugin(plugin) {
223
+ return {
224
+ name: plugin.name.trim(),
225
+ version: plugin.version.trim(),
226
+ description: plugin.description.trim(),
227
+ author: plugin.author.trim(),
228
+ installed: Boolean(plugin.installed),
229
+ };
230
+ }
231
+ function findPlugin(plugins, name) {
232
+ const normalizedName = name.trim().toLowerCase();
233
+ return plugins.find((plugin) => plugin.name.toLowerCase() === normalizedName);
234
+ }
235
+ function isPlugin(value) {
236
+ if (!value || typeof value !== 'object' || Array.isArray(value))
237
+ return false;
238
+ const plugin = value;
239
+ return (typeof plugin.name === 'string' &&
240
+ typeof plugin.version === 'string' &&
241
+ typeof plugin.description === 'string' &&
242
+ typeof plugin.author === 'string' &&
243
+ typeof plugin.installed === 'boolean');
244
+ }
@@ -0,0 +1,298 @@
1
+ import fs from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import OpenAI from 'openai';
6
+ import { LOCAL_PROVIDER_DEFAULTS } from './local-model.js';
7
+ const DEFAULT_PROVIDER_NAME = 'github';
8
+ let cachedGithubCliToken;
9
+ const BUILTIN_PROVIDERS = [
10
+ {
11
+ name: 'github',
12
+ baseUrl: 'https://models.inference.ai.azure.com',
13
+ models: [
14
+ 'gpt-4o-mini',
15
+ 'gpt-4.1-mini',
16
+ 'gpt-4.1',
17
+ 'o4-mini',
18
+ 'claude-3.5-sonnet',
19
+ 'Llama-3.3-70B-Instruct',
20
+ ],
21
+ defaultModel: 'gpt-4o-mini',
22
+ },
23
+ {
24
+ name: 'openai',
25
+ baseUrl: 'https://api.openai.com/v1',
26
+ models: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'o4-mini'],
27
+ defaultModel: 'gpt-4o-mini',
28
+ },
29
+ {
30
+ name: 'anthropic',
31
+ baseUrl: 'https://api.anthropic.com/v1',
32
+ models: ['claude-3-5-haiku-latest', 'claude-3-5-sonnet-latest', 'claude-3-7-sonnet-latest'],
33
+ defaultModel: 'claude-3-5-haiku-latest',
34
+ },
35
+ {
36
+ name: 'ollama',
37
+ baseUrl: LOCAL_PROVIDER_DEFAULTS.ollama.baseUrl,
38
+ models: [LOCAL_PROVIDER_DEFAULTS.ollama.model],
39
+ defaultModel: LOCAL_PROVIDER_DEFAULTS.ollama.model,
40
+ },
41
+ {
42
+ name: 'vllm',
43
+ baseUrl: LOCAL_PROVIDER_DEFAULTS.vllm.baseUrl,
44
+ models: [LOCAL_PROVIDER_DEFAULTS.vllm.model],
45
+ defaultModel: LOCAL_PROVIDER_DEFAULTS.vllm.model,
46
+ },
47
+ {
48
+ name: 'lmstudio',
49
+ baseUrl: LOCAL_PROVIDER_DEFAULTS.lmstudio.baseUrl,
50
+ models: [LOCAL_PROVIDER_DEFAULTS.lmstudio.model],
51
+ defaultModel: LOCAL_PROVIDER_DEFAULTS.lmstudio.model,
52
+ },
53
+ ];
54
+ function normalizeName(name) {
55
+ return name.trim().toLowerCase();
56
+ }
57
+ function trimTrailingSlash(url) {
58
+ return url.trim().replace(/\/+$/, '');
59
+ }
60
+ function normalizeHeaders(headers) {
61
+ if (!headers)
62
+ return undefined;
63
+ const normalized = Object.fromEntries(Object.entries(headers)
64
+ .filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
65
+ .map(([key, value]) => [key.trim(), value]));
66
+ return Object.keys(normalized).length ? normalized : undefined;
67
+ }
68
+ function normalizeModels(models) {
69
+ return [...new Set(models.map((model) => model.trim()).filter(Boolean))];
70
+ }
71
+ function normalizeProviderConfig(config) {
72
+ const name = normalizeName(config.name);
73
+ const baseUrl = trimTrailingSlash(config.baseUrl);
74
+ const models = normalizeModels(config.models);
75
+ if (!name)
76
+ throw new Error('provider name is required');
77
+ if (!baseUrl)
78
+ throw new Error(`provider ${name} is missing a baseUrl`);
79
+ if (!models.length)
80
+ throw new Error(`provider ${name} must define at least one model`);
81
+ const defaultModel = config.defaultModel && models.includes(config.defaultModel) ? config.defaultModel : models[0];
82
+ const maxTokens = typeof config.maxTokens === 'number' &&
83
+ Number.isFinite(config.maxTokens) &&
84
+ config.maxTokens > 0
85
+ ? Math.floor(config.maxTokens)
86
+ : undefined;
87
+ return {
88
+ name,
89
+ baseUrl,
90
+ apiKey: typeof config.apiKey === 'string' && config.apiKey.trim() ? config.apiKey : undefined,
91
+ models,
92
+ defaultModel,
93
+ headers: normalizeHeaders(config.headers),
94
+ maxTokens,
95
+ };
96
+ }
97
+ export function providerConfigPath() {
98
+ return (process.env.ICOPILOT_PROVIDERS_PATH || path.join(os.homedir(), '.icopilot', 'providers.json'));
99
+ }
100
+ function resolveGitHubCliToken() {
101
+ if (cachedGithubCliToken !== undefined)
102
+ return cachedGithubCliToken || undefined;
103
+ try {
104
+ const token = execFileSync('gh', ['auth', 'token'], {
105
+ encoding: 'utf8',
106
+ stdio: ['ignore', 'pipe', 'ignore'],
107
+ }).trim();
108
+ cachedGithubCliToken = token || null;
109
+ return cachedGithubCliToken || undefined;
110
+ }
111
+ catch {
112
+ cachedGithubCliToken = null;
113
+ return undefined;
114
+ }
115
+ }
116
+ export function resolveProviderApiKey(provider) {
117
+ if (provider.apiKey)
118
+ return provider.apiKey;
119
+ switch (provider.name) {
120
+ case 'github':
121
+ return process.env.GITHUB_TOKEN || process.env.ICOPILOT_TOKEN || resolveGitHubCliToken();
122
+ case 'openai':
123
+ return process.env.OPENAI_API_KEY || process.env.ICOPILOT_TOKEN;
124
+ case 'anthropic':
125
+ return process.env.ANTHROPIC_API_KEY || process.env.ICOPILOT_TOKEN;
126
+ case 'ollama':
127
+ case 'vllm':
128
+ case 'lmstudio':
129
+ return process.env.ICOPILOT_LOCAL_API_KEY || process.env.ICOPILOT_TOKEN;
130
+ default:
131
+ return process.env.ICOPILOT_TOKEN;
132
+ }
133
+ }
134
+ export class ProviderRegistry {
135
+ configPath;
136
+ builtIns = new Map();
137
+ customProviders = new Map();
138
+ clientCache = new Map();
139
+ openAIClient;
140
+ activeName = DEFAULT_PROVIDER_NAME;
141
+ constructor(options = {}) {
142
+ this.configPath = options.configPath || providerConfigPath();
143
+ this.openAIClient = options.openAIClient || OpenAI;
144
+ for (const provider of options.builtIns || BUILTIN_PROVIDERS) {
145
+ const normalized = normalizeProviderConfig(provider);
146
+ this.builtIns.set(normalized.name, normalized);
147
+ }
148
+ this.load();
149
+ }
150
+ register(config) {
151
+ const normalized = normalizeProviderConfig(config);
152
+ if (this.builtIns.has(normalized.name)) {
153
+ throw new Error(`cannot override built-in provider: ${normalized.name}`);
154
+ }
155
+ this.customProviders.set(normalized.name, normalized);
156
+ this.clearClientCache(normalized.name);
157
+ this.save();
158
+ return normalized;
159
+ }
160
+ remove(name) {
161
+ const normalizedName = normalizeName(name);
162
+ if (this.builtIns.has(normalizedName))
163
+ return false;
164
+ const deleted = this.customProviders.delete(normalizedName);
165
+ if (!deleted)
166
+ return false;
167
+ if (this.activeName === normalizedName)
168
+ this.activeName = DEFAULT_PROVIDER_NAME;
169
+ this.clearClientCache(normalizedName);
170
+ this.save();
171
+ return true;
172
+ }
173
+ get(name) {
174
+ const normalizedName = normalizeName(name);
175
+ return this.customProviders.get(normalizedName) || this.builtIns.get(normalizedName);
176
+ }
177
+ list() {
178
+ return [...this.builtIns.values(), ...this.customProviders.values()].sort((left, right) => left.name.localeCompare(right.name));
179
+ }
180
+ getActive() {
181
+ return this.get(this.activeName) || this.get(DEFAULT_PROVIDER_NAME);
182
+ }
183
+ setActive(name) {
184
+ const provider = this.get(name);
185
+ if (!provider)
186
+ throw new Error(`unknown provider: ${name}`);
187
+ this.activeName = provider.name;
188
+ this.save();
189
+ return provider;
190
+ }
191
+ createClient(name) {
192
+ const provider = name ? this.get(name) : this.getActive();
193
+ if (!provider)
194
+ throw new Error(`unknown provider: ${name}`);
195
+ const apiKey = resolveProviderApiKey(provider) || 'not-needed';
196
+ const cacheKey = JSON.stringify({
197
+ name: provider.name,
198
+ baseUrl: provider.baseUrl,
199
+ apiKey,
200
+ headers: provider.headers || null,
201
+ });
202
+ const cached = this.clientCache.get(cacheKey);
203
+ if (cached)
204
+ return cached;
205
+ const client = new this.openAIClient({
206
+ apiKey,
207
+ baseURL: provider.baseUrl,
208
+ defaultHeaders: provider.headers,
209
+ });
210
+ this.clientCache.set(cacheKey, client);
211
+ return client;
212
+ }
213
+ async testProvider(name) {
214
+ const provider = this.get(name);
215
+ if (!provider)
216
+ throw new Error(`unknown provider: ${name}`);
217
+ const client = this.createClient(provider.name);
218
+ try {
219
+ const models = await client.models.list();
220
+ const modelIds = models.data.map((model) => model.id).filter(Boolean);
221
+ return { ok: true, provider: provider.name, models: modelIds };
222
+ }
223
+ catch (modelError) {
224
+ const fallbackModel = provider.defaultModel || provider.models[0];
225
+ if (!fallbackModel) {
226
+ return {
227
+ ok: false,
228
+ provider: provider.name,
229
+ models: [],
230
+ error: stringifyError(modelError),
231
+ };
232
+ }
233
+ try {
234
+ await client.chat.completions.create({
235
+ model: fallbackModel,
236
+ messages: [{ role: 'user', content: 'ping' }],
237
+ max_tokens: 1,
238
+ });
239
+ return { ok: true, provider: provider.name, models: [fallbackModel] };
240
+ }
241
+ catch (chatError) {
242
+ return {
243
+ ok: false,
244
+ provider: provider.name,
245
+ models: [],
246
+ error: stringifyError(chatError),
247
+ };
248
+ }
249
+ }
250
+ }
251
+ clearClientCache(name) {
252
+ for (const key of this.clientCache.keys()) {
253
+ if (key.includes(`"name":"${name}"`)) {
254
+ this.clientCache.delete(key);
255
+ }
256
+ }
257
+ }
258
+ load() {
259
+ if (!fs.existsSync(this.configPath))
260
+ return;
261
+ try {
262
+ const parsed = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
263
+ const providers = Array.isArray(parsed.providers) ? parsed.providers : [];
264
+ this.customProviders.clear();
265
+ for (const provider of providers) {
266
+ try {
267
+ const normalized = normalizeProviderConfig(provider);
268
+ if (!this.builtIns.has(normalized.name)) {
269
+ this.customProviders.set(normalized.name, normalized);
270
+ }
271
+ }
272
+ catch {
273
+ // Ignore invalid provider entries and keep loading the rest.
274
+ }
275
+ }
276
+ const active = typeof parsed.active === 'string' ? normalizeName(parsed.active) : DEFAULT_PROVIDER_NAME;
277
+ this.activeName = this.get(active)?.name || DEFAULT_PROVIDER_NAME;
278
+ }
279
+ catch {
280
+ this.customProviders.clear();
281
+ this.activeName = DEFAULT_PROVIDER_NAME;
282
+ }
283
+ }
284
+ save() {
285
+ const data = {
286
+ active: this.activeName,
287
+ providers: [...this.customProviders.values()],
288
+ };
289
+ fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
290
+ fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
291
+ }
292
+ }
293
+ function stringifyError(error) {
294
+ if (error instanceof Error)
295
+ return error.message;
296
+ return String(error);
297
+ }
298
+ export const providerRegistry = new ProviderRegistry();