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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { ProxyAgent } from 'proxy-agent';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
const PROXY_PATH_ENV = 'ICOPILOT_PROXY_PATH';
|
|
9
|
+
export class ProxyManager {
|
|
10
|
+
static singleton = new ProxyManager();
|
|
11
|
+
currentConfig = null;
|
|
12
|
+
currentSource = null;
|
|
13
|
+
cachedAgent = null;
|
|
14
|
+
cachedAgentKey = null;
|
|
15
|
+
static shared() {
|
|
16
|
+
return ProxyManager.singleton;
|
|
17
|
+
}
|
|
18
|
+
static parseProxyUrl(raw) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error('proxy URL must be a valid absolute URL');
|
|
25
|
+
}
|
|
26
|
+
const type = protocolToType(parsed.protocol);
|
|
27
|
+
const host = parsed.hostname.trim();
|
|
28
|
+
if (!host)
|
|
29
|
+
throw new Error('proxy host is required');
|
|
30
|
+
const port = parsed.port ? Number(parsed.port) : defaultPortForType(type);
|
|
31
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
32
|
+
throw new Error('proxy port must be between 1 and 65535');
|
|
33
|
+
}
|
|
34
|
+
const auth = parsed.username.length > 0
|
|
35
|
+
? {
|
|
36
|
+
username: decodeURIComponent(parsed.username),
|
|
37
|
+
...(parsed.password ? { password: decodeURIComponent(parsed.password) } : {}),
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
return { type, host, port, ...(auth ? { auth } : {}) };
|
|
41
|
+
}
|
|
42
|
+
loadConfig() {
|
|
43
|
+
const envConfig = readEnvProxyConfig();
|
|
44
|
+
const fileConfig = envConfig ? null : readProxyConfigFile(this.configPath());
|
|
45
|
+
const noProxy = readNoProxyEnv();
|
|
46
|
+
const active = cloneConfig(envConfig ?? fileConfig);
|
|
47
|
+
if (active && noProxy !== undefined) {
|
|
48
|
+
active.noProxy = noProxy;
|
|
49
|
+
}
|
|
50
|
+
this.currentConfig = active;
|
|
51
|
+
this.currentSource = envConfig ? 'env' : fileConfig ? 'file' : null;
|
|
52
|
+
return cloneConfig(active);
|
|
53
|
+
}
|
|
54
|
+
setProxy(config) {
|
|
55
|
+
const normalized = normalizeProxyConfig(config);
|
|
56
|
+
const file = this.configPath();
|
|
57
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
58
|
+
fs.writeFileSync(file, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
59
|
+
this.currentConfig = cloneConfig(normalized);
|
|
60
|
+
this.currentSource = 'file';
|
|
61
|
+
this.resetAgentCache();
|
|
62
|
+
return cloneConfig(normalized);
|
|
63
|
+
}
|
|
64
|
+
clearProxy() {
|
|
65
|
+
const file = this.configPath();
|
|
66
|
+
fs.rmSync(file, { force: true });
|
|
67
|
+
this.currentConfig = null;
|
|
68
|
+
this.currentSource = null;
|
|
69
|
+
this.resetAgentCache();
|
|
70
|
+
}
|
|
71
|
+
getAgent(targetUrl) {
|
|
72
|
+
const active = this.loadConfig();
|
|
73
|
+
if (!active)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (targetUrl && !this.isProxied(targetUrl, active))
|
|
76
|
+
return undefined;
|
|
77
|
+
const agentKey = JSON.stringify(active);
|
|
78
|
+
if (this.cachedAgent && this.cachedAgentKey === agentKey) {
|
|
79
|
+
return this.cachedAgent;
|
|
80
|
+
}
|
|
81
|
+
const proxyUrl = proxyConfigToUrl(active);
|
|
82
|
+
this.cachedAgent = new ProxyAgent({
|
|
83
|
+
getProxyForUrl: (url) => (this.isProxied(url, active) ? proxyUrl : ''),
|
|
84
|
+
});
|
|
85
|
+
this.cachedAgentKey = agentKey;
|
|
86
|
+
return this.cachedAgent;
|
|
87
|
+
}
|
|
88
|
+
isProxied(targetUrl, active = this.loadConfig()) {
|
|
89
|
+
if (!active)
|
|
90
|
+
return false;
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(targetUrl);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
99
|
+
return false;
|
|
100
|
+
return !matchesNoProxy(parsed, active.noProxy ?? []);
|
|
101
|
+
}
|
|
102
|
+
async testConnection(targetUrl = config.endpoint) {
|
|
103
|
+
const proxied = this.isProxied(targetUrl);
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = new URL(targetUrl);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { ok: false, proxied, url: targetUrl, error: 'invalid target URL' };
|
|
110
|
+
}
|
|
111
|
+
const agent = this.getAgent(targetUrl);
|
|
112
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const req = transport.request(parsed, {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
agent,
|
|
117
|
+
headers: { 'user-agent': 'icopilot-proxy-test/1.0' },
|
|
118
|
+
timeout: 5_000,
|
|
119
|
+
}, (res) => {
|
|
120
|
+
res.resume();
|
|
121
|
+
resolve({
|
|
122
|
+
ok: true,
|
|
123
|
+
proxied,
|
|
124
|
+
status: res.statusCode,
|
|
125
|
+
url: targetUrl,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
req.on('timeout', () => req.destroy(new Error('request timed out')));
|
|
129
|
+
req.on('error', (error) => {
|
|
130
|
+
resolve({
|
|
131
|
+
ok: false,
|
|
132
|
+
proxied,
|
|
133
|
+
url: targetUrl,
|
|
134
|
+
error: error.message,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
req.end();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
getSource() {
|
|
141
|
+
return this.currentSource;
|
|
142
|
+
}
|
|
143
|
+
getConfigPath() {
|
|
144
|
+
return this.configPath();
|
|
145
|
+
}
|
|
146
|
+
configPath() {
|
|
147
|
+
return process.env[PROXY_PATH_ENV] || path.join(os.homedir(), '.icopilot', 'proxy.json');
|
|
148
|
+
}
|
|
149
|
+
resetAgentCache() {
|
|
150
|
+
this.cachedAgent?.destroy();
|
|
151
|
+
this.cachedAgent = null;
|
|
152
|
+
this.cachedAgentKey = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function readEnvProxyConfig() {
|
|
156
|
+
const raw = process.env.HTTPS_PROXY ??
|
|
157
|
+
process.env.https_proxy ??
|
|
158
|
+
process.env.HTTP_PROXY ??
|
|
159
|
+
process.env.http_proxy;
|
|
160
|
+
if (!raw)
|
|
161
|
+
return null;
|
|
162
|
+
return normalizeProxyConfig(ProxyManager.parseProxyUrl(raw));
|
|
163
|
+
}
|
|
164
|
+
function readNoProxyEnv() {
|
|
165
|
+
const raw = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
166
|
+
if (raw === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
return parseNoProxy(raw);
|
|
169
|
+
}
|
|
170
|
+
function readProxyConfigFile(file) {
|
|
171
|
+
if (!fs.existsSync(file))
|
|
172
|
+
return null;
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
175
|
+
return normalizeProxyConfig(parsed);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function normalizeProxyConfig(value) {
|
|
182
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
183
|
+
throw new Error('proxy config must be an object');
|
|
184
|
+
}
|
|
185
|
+
const record = value;
|
|
186
|
+
const type = normalizeProxyType(record.type);
|
|
187
|
+
if (!type)
|
|
188
|
+
throw new Error('proxy type must be http, https, or socks5');
|
|
189
|
+
const host = typeof record.host === 'string' ? record.host.trim() : '';
|
|
190
|
+
if (!host)
|
|
191
|
+
throw new Error('proxy host is required');
|
|
192
|
+
const port = typeof record.port === 'number' ? record.port : Number(record.port);
|
|
193
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
194
|
+
throw new Error('proxy port must be between 1 and 65535');
|
|
195
|
+
}
|
|
196
|
+
const auth = normalizeProxyAuth(record.auth);
|
|
197
|
+
const noProxy = normalizeNoProxy(record.noProxy);
|
|
198
|
+
return {
|
|
199
|
+
type,
|
|
200
|
+
host,
|
|
201
|
+
port,
|
|
202
|
+
...(auth ? { auth } : {}),
|
|
203
|
+
...(noProxy.length ? { noProxy } : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function normalizeProxyType(value) {
|
|
207
|
+
if (value === 'http' || value === 'https' || value === 'socks5')
|
|
208
|
+
return value;
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function normalizeProxyAuth(value) {
|
|
212
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
213
|
+
return undefined;
|
|
214
|
+
const auth = value;
|
|
215
|
+
const username = typeof auth.username === 'string' ? auth.username : '';
|
|
216
|
+
if (!username)
|
|
217
|
+
return undefined;
|
|
218
|
+
const password = typeof auth.password === 'string' ? auth.password : undefined;
|
|
219
|
+
return password ? { username, password } : { username };
|
|
220
|
+
}
|
|
221
|
+
function normalizeNoProxy(value) {
|
|
222
|
+
if (typeof value === 'string')
|
|
223
|
+
return parseNoProxy(value);
|
|
224
|
+
if (!Array.isArray(value))
|
|
225
|
+
return [];
|
|
226
|
+
return value
|
|
227
|
+
.filter((entry) => typeof entry === 'string')
|
|
228
|
+
.map((entry) => entry.trim())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
}
|
|
231
|
+
function parseNoProxy(raw) {
|
|
232
|
+
return raw
|
|
233
|
+
.split(',')
|
|
234
|
+
.map((entry) => entry.trim())
|
|
235
|
+
.filter(Boolean);
|
|
236
|
+
}
|
|
237
|
+
function protocolToType(protocol) {
|
|
238
|
+
switch (protocol.toLowerCase()) {
|
|
239
|
+
case 'http:':
|
|
240
|
+
return 'http';
|
|
241
|
+
case 'https:':
|
|
242
|
+
return 'https';
|
|
243
|
+
case 'socks5:':
|
|
244
|
+
case 'socks:':
|
|
245
|
+
return 'socks5';
|
|
246
|
+
default:
|
|
247
|
+
throw new Error('proxy protocol must be http, https, or socks5');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function defaultPortForType(type) {
|
|
251
|
+
switch (type) {
|
|
252
|
+
case 'http':
|
|
253
|
+
return 80;
|
|
254
|
+
case 'https':
|
|
255
|
+
return 443;
|
|
256
|
+
case 'socks5':
|
|
257
|
+
return 1080;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function proxyConfigToUrl(proxy) {
|
|
261
|
+
const auth = proxy.auth?.username
|
|
262
|
+
? `${encodeURIComponent(proxy.auth.username)}${proxy.auth.password ? `:${encodeURIComponent(proxy.auth.password)}` : ''}@`
|
|
263
|
+
: '';
|
|
264
|
+
return `${proxy.type}://${auth}${proxy.host}:${proxy.port}`;
|
|
265
|
+
}
|
|
266
|
+
function matchesNoProxy(target, rules) {
|
|
267
|
+
const hostname = target.hostname.toLowerCase();
|
|
268
|
+
const port = target.port || defaultPortForProtocol(target.protocol);
|
|
269
|
+
return rules.some((rule) => matchesNoProxyRule(hostname, port, rule));
|
|
270
|
+
}
|
|
271
|
+
function matchesNoProxyRule(hostname, port, rawRule) {
|
|
272
|
+
const rule = rawRule.trim().toLowerCase();
|
|
273
|
+
if (!rule)
|
|
274
|
+
return false;
|
|
275
|
+
if (rule === '*')
|
|
276
|
+
return true;
|
|
277
|
+
const separator = rule.lastIndexOf(':');
|
|
278
|
+
const hasPort = separator > -1 && /^\d+$/.test(rule.slice(separator + 1));
|
|
279
|
+
const ruleHost = hasPort ? rule.slice(0, separator) : rule;
|
|
280
|
+
const rulePort = hasPort ? rule.slice(separator + 1) : '';
|
|
281
|
+
if (rulePort && rulePort !== port)
|
|
282
|
+
return false;
|
|
283
|
+
const bareHost = ruleHost.replace(/^\*\./, '.');
|
|
284
|
+
if (bareHost.startsWith('.')) {
|
|
285
|
+
const suffix = bareHost.slice(1);
|
|
286
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
287
|
+
}
|
|
288
|
+
return hostname === bareHost || hostname.endsWith(`.${bareHost}`);
|
|
289
|
+
}
|
|
290
|
+
function defaultPortForProtocol(protocol) {
|
|
291
|
+
return protocol === 'http:' ? '80' : protocol === 'https:' ? '443' : '';
|
|
292
|
+
}
|
|
293
|
+
function cloneConfig(config) {
|
|
294
|
+
if (!config)
|
|
295
|
+
return null;
|
|
296
|
+
return {
|
|
297
|
+
...config,
|
|
298
|
+
...(config.auth ? { auth: { ...config.auth } } : {}),
|
|
299
|
+
...(config.noProxy ? { noProxy: [...config.noProxy] } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
|
+
import { config } from '../config.js';
|
|
6
|
+
import { auditLogPath } from './audit.js';
|
|
7
|
+
import { theme } from '../ui/theme.js';
|
|
8
|
+
const CONCRETE_TARGETS = ['sessions', 'audit', 'memory'];
|
|
9
|
+
export function retentionConfigPath() {
|
|
10
|
+
return path.join(os.homedir(), '.icopilot', 'retention.yaml');
|
|
11
|
+
}
|
|
12
|
+
export class RetentionManager {
|
|
13
|
+
configPath;
|
|
14
|
+
sessionDir;
|
|
15
|
+
auditPath;
|
|
16
|
+
memoryDir;
|
|
17
|
+
now;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.configPath = path.resolve(options.configPath ?? retentionConfigPath());
|
|
20
|
+
this.sessionDir = path.resolve(options.sessionDir ?? config.sessionDir);
|
|
21
|
+
this.auditPath = path.resolve(options.auditPath ?? auditLogPath());
|
|
22
|
+
this.memoryDir = path.resolve(options.memoryDir ?? path.join(os.homedir(), '.icopilot', 'memory'));
|
|
23
|
+
this.now = options.now ?? (() => new Date());
|
|
24
|
+
}
|
|
25
|
+
loadPolicies() {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(this.configPath))
|
|
28
|
+
return [];
|
|
29
|
+
const raw = parse(fs.readFileSync(this.configPath, 'utf8'));
|
|
30
|
+
const source = Array.isArray(raw)
|
|
31
|
+
? raw
|
|
32
|
+
: raw && typeof raw === 'object' && Array.isArray(raw.policies)
|
|
33
|
+
? raw.policies
|
|
34
|
+
: [];
|
|
35
|
+
const normalized = source
|
|
36
|
+
.map(normalizePolicy)
|
|
37
|
+
.filter((policy) => policy !== null);
|
|
38
|
+
return dedupePolicies(normalized);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
setPolicy(policy) {
|
|
45
|
+
const normalized = normalizePolicy(policy);
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
throw new Error('Invalid retention policy.');
|
|
48
|
+
}
|
|
49
|
+
const next = dedupePolicies([
|
|
50
|
+
...this.loadPolicies().filter((entry) => entry.target !== normalized.target),
|
|
51
|
+
normalized,
|
|
52
|
+
]);
|
|
53
|
+
fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
|
|
54
|
+
fs.writeFileSync(this.configPath, stringify({ policies: next }), 'utf8');
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
preview() {
|
|
58
|
+
const expired = this.getExpired('all');
|
|
59
|
+
return {
|
|
60
|
+
policies: this.loadPolicies(),
|
|
61
|
+
expired,
|
|
62
|
+
totals: this.buildTotals(expired),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
enforce() {
|
|
66
|
+
const preview = this.preview();
|
|
67
|
+
const deleted = [];
|
|
68
|
+
const errors = [];
|
|
69
|
+
for (const candidate of preview.expired) {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(candidate.path)) {
|
|
72
|
+
fs.rmSync(candidate.path, { force: true, recursive: false });
|
|
73
|
+
}
|
|
74
|
+
deleted.push(candidate);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
errors.push({
|
|
78
|
+
path: candidate.path,
|
|
79
|
+
message: error instanceof Error ? error.message : String(error),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...preview,
|
|
85
|
+
deleted,
|
|
86
|
+
errors,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
getExpired(target) {
|
|
90
|
+
if (target === 'all') {
|
|
91
|
+
return CONCRETE_TARGETS.flatMap((entry) => this.getExpired(entry));
|
|
92
|
+
}
|
|
93
|
+
const policy = this.policyForTarget(target);
|
|
94
|
+
if (!policy || !policy.enabled)
|
|
95
|
+
return [];
|
|
96
|
+
const items = this.itemsForTarget(target).sort((left, right) => right.modifiedAt.getTime() - left.modifiedAt.getTime());
|
|
97
|
+
const cutoff = this.now().getTime() - policy.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
98
|
+
return items.flatMap((item, index) => {
|
|
99
|
+
const reasons = [];
|
|
100
|
+
if (item.modifiedAt.getTime() <= cutoff)
|
|
101
|
+
reasons.push('age');
|
|
102
|
+
if (typeof policy.maxCount === 'number' && index >= policy.maxCount)
|
|
103
|
+
reasons.push('count');
|
|
104
|
+
if (reasons.length === 0)
|
|
105
|
+
return [];
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
target: item.target,
|
|
109
|
+
path: item.path,
|
|
110
|
+
modifiedAt: item.modifiedAt.toISOString(),
|
|
111
|
+
ageDays: ageDaysBetween(item.modifiedAt, this.now()),
|
|
112
|
+
reasons,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
buildTotals(expired) {
|
|
118
|
+
return {
|
|
119
|
+
sessions: {
|
|
120
|
+
scanned: this.itemsForTarget('sessions').length,
|
|
121
|
+
expired: expired.filter((entry) => entry.target === 'sessions').length,
|
|
122
|
+
},
|
|
123
|
+
audit: {
|
|
124
|
+
scanned: this.itemsForTarget('audit').length,
|
|
125
|
+
expired: expired.filter((entry) => entry.target === 'audit').length,
|
|
126
|
+
},
|
|
127
|
+
memory: {
|
|
128
|
+
scanned: this.itemsForTarget('memory').length,
|
|
129
|
+
expired: expired.filter((entry) => entry.target === 'memory').length,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
policyForTarget(target) {
|
|
134
|
+
const policies = this.loadPolicies();
|
|
135
|
+
return (policies.find((policy) => policy.target === target) ??
|
|
136
|
+
policies.find((policy) => policy.target === 'all') ??
|
|
137
|
+
null);
|
|
138
|
+
}
|
|
139
|
+
itemsForTarget(target) {
|
|
140
|
+
switch (target) {
|
|
141
|
+
case 'sessions':
|
|
142
|
+
return listFiles(this.sessionDir, target);
|
|
143
|
+
case 'audit':
|
|
144
|
+
return listSingleFile(this.auditPath, target);
|
|
145
|
+
case 'memory':
|
|
146
|
+
return listFiles(this.memoryDir, target);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function formatPolicies(manager) {
|
|
151
|
+
const policies = manager.loadPolicies();
|
|
152
|
+
if (policies.length === 0) {
|
|
153
|
+
return `${theme.brand('Retention policies')} ${theme.dim(manager.configPath)}\n ${theme.dim('No retention policies configured.')}\n`;
|
|
154
|
+
}
|
|
155
|
+
const lines = policies.map((policy) => {
|
|
156
|
+
const bits = [
|
|
157
|
+
`age=${theme.hl(String(policy.maxAgeDays))}d`,
|
|
158
|
+
typeof policy.maxCount === 'number' ? `count=${theme.hl(String(policy.maxCount))}` : null,
|
|
159
|
+
policy.enabled ? theme.ok('enabled') : theme.warn('disabled'),
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
return ` ${theme.hl(policy.target)} ${bits.join(' ')}`;
|
|
162
|
+
});
|
|
163
|
+
return `${theme.brand('Retention policies')} ${theme.dim(manager.configPath)}\n${lines.join('\n')}\n`;
|
|
164
|
+
}
|
|
165
|
+
export function formatPreview(preview, manager) {
|
|
166
|
+
const lines = [
|
|
167
|
+
`${theme.brand('Retention preview')} ${theme.dim(manager.configPath)}`,
|
|
168
|
+
...formatTotals(preview.totals),
|
|
169
|
+
];
|
|
170
|
+
if (preview.expired.length === 0) {
|
|
171
|
+
lines.push('', theme.ok('No expired retention items.'));
|
|
172
|
+
return `${lines.join('\n')}\n`;
|
|
173
|
+
}
|
|
174
|
+
lines.push('', theme.brand('Expired items'));
|
|
175
|
+
for (const candidate of preview.expired) {
|
|
176
|
+
lines.push(` ${theme.hl(candidate.target)} ${candidate.path} ${theme.dim(`(${candidate.ageDays.toFixed(1)}d, ${candidate.reasons.join('+')})`)}`);
|
|
177
|
+
}
|
|
178
|
+
return `${lines.join('\n')}\n`;
|
|
179
|
+
}
|
|
180
|
+
export function formatResult(result, manager) {
|
|
181
|
+
const lines = [
|
|
182
|
+
`${theme.brand('Retention enforce')} ${theme.dim(manager.configPath)}`,
|
|
183
|
+
...formatTotals(result.totals),
|
|
184
|
+
'',
|
|
185
|
+
`${theme.ok(`Deleted ${result.deleted.length} item${result.deleted.length === 1 ? '' : 's'}.`)}`,
|
|
186
|
+
];
|
|
187
|
+
if (result.errors.length > 0) {
|
|
188
|
+
lines.push(theme.warn(`Errors: ${result.errors.length}`));
|
|
189
|
+
for (const error of result.errors) {
|
|
190
|
+
lines.push(` ${error.path} ${theme.dim(`(${error.message})`)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return `${lines.join('\n')}\n`;
|
|
194
|
+
}
|
|
195
|
+
function formatTotals(totals) {
|
|
196
|
+
return CONCRETE_TARGETS.map((target) => {
|
|
197
|
+
const summary = totals[target];
|
|
198
|
+
return ` ${theme.hl(target)} scanned=${summary.scanned} expired=${summary.expired}`;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function normalizePolicy(value) {
|
|
202
|
+
if (!value || typeof value !== 'object')
|
|
203
|
+
return null;
|
|
204
|
+
const candidate = value;
|
|
205
|
+
if (candidate.target !== 'sessions' &&
|
|
206
|
+
candidate.target !== 'audit' &&
|
|
207
|
+
candidate.target !== 'memory' &&
|
|
208
|
+
candidate.target !== 'all') {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const maxAgeDays = normalizeCount(candidate.maxAgeDays);
|
|
212
|
+
if (maxAgeDays === null)
|
|
213
|
+
return null;
|
|
214
|
+
const maxCount = candidate.maxCount === undefined ? undefined : normalizeCount(candidate.maxCount);
|
|
215
|
+
if (candidate.maxCount !== undefined && maxCount === null)
|
|
216
|
+
return null;
|
|
217
|
+
const normalizedMaxCount = maxCount ?? undefined;
|
|
218
|
+
if (typeof candidate.enabled !== 'boolean')
|
|
219
|
+
return null;
|
|
220
|
+
return {
|
|
221
|
+
target: candidate.target,
|
|
222
|
+
maxAgeDays,
|
|
223
|
+
maxCount: normalizedMaxCount,
|
|
224
|
+
enabled: candidate.enabled,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function normalizeCount(value) {
|
|
228
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
229
|
+
return null;
|
|
230
|
+
const normalized = Math.trunc(value);
|
|
231
|
+
return normalized >= 0 ? normalized : null;
|
|
232
|
+
}
|
|
233
|
+
function dedupePolicies(policies) {
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
const ordered = [];
|
|
236
|
+
for (const target of ['sessions', 'audit', 'memory', 'all']) {
|
|
237
|
+
const policy = policies.find((entry) => entry.target === target);
|
|
238
|
+
if (!policy || seen.has(policy.target))
|
|
239
|
+
continue;
|
|
240
|
+
seen.add(policy.target);
|
|
241
|
+
ordered.push(policy);
|
|
242
|
+
}
|
|
243
|
+
return ordered;
|
|
244
|
+
}
|
|
245
|
+
function listFiles(dirPath, target) {
|
|
246
|
+
try {
|
|
247
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
|
|
248
|
+
return [];
|
|
249
|
+
return fs.readdirSync(dirPath).flatMap((name) => {
|
|
250
|
+
const filePath = path.join(dirPath, name);
|
|
251
|
+
try {
|
|
252
|
+
const stat = fs.statSync(filePath);
|
|
253
|
+
if (!stat.isFile())
|
|
254
|
+
return [];
|
|
255
|
+
return [{ target, path: filePath, modifiedAt: stat.mtime }];
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function listSingleFile(filePath, target) {
|
|
267
|
+
try {
|
|
268
|
+
if (!fs.existsSync(filePath))
|
|
269
|
+
return [];
|
|
270
|
+
const stat = fs.statSync(filePath);
|
|
271
|
+
if (!stat.isFile())
|
|
272
|
+
return [];
|
|
273
|
+
return [{ target, path: filePath, modifiedAt: stat.mtime }];
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function ageDaysBetween(from, to) {
|
|
280
|
+
return Math.max(0, (to.getTime() - from.getTime()) / (24 * 60 * 60 * 1000));
|
|
281
|
+
}
|