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,121 @@
1
+ import OpenAI from 'openai';
2
+ const DEFAULT_API_KEY = 'air-gapped-local';
3
+ export const LOCAL_PROVIDER_DEFAULTS = {
4
+ ollama: {
5
+ baseUrl: 'http://127.0.0.1:11434/v1',
6
+ model: 'llama3.2',
7
+ },
8
+ vllm: {
9
+ baseUrl: 'http://127.0.0.1:8000/v1',
10
+ model: 'local-model',
11
+ },
12
+ lmstudio: {
13
+ baseUrl: 'http://127.0.0.1:1234/v1',
14
+ model: 'local-model',
15
+ },
16
+ custom: {
17
+ baseUrl: 'http://127.0.0.1:8000/v1',
18
+ model: 'local-model',
19
+ },
20
+ };
21
+ export function isLocalProviderName(value) {
22
+ return value === 'ollama' || value === 'vllm' || value === 'lmstudio' || value === 'custom';
23
+ }
24
+ export function resolveLocalModelConfig(provider, overrides = {}) {
25
+ const defaults = LOCAL_PROVIDER_DEFAULTS[provider];
26
+ return {
27
+ provider,
28
+ baseUrl: normalizeBaseUrl(overrides.baseUrl ?? defaults.baseUrl),
29
+ model: overrides.model?.trim() || defaults.model,
30
+ apiKey: overrides.apiKey?.trim() || undefined,
31
+ };
32
+ }
33
+ export class LocalModelProvider {
34
+ current = null;
35
+ currentKey = '';
36
+ currentClient = null;
37
+ configure(config) {
38
+ const normalized = resolveLocalModelConfig(config.provider, config);
39
+ const nextKey = JSON.stringify(normalized);
40
+ if (!this.currentClient || this.currentKey !== nextKey) {
41
+ this.currentClient = new OpenAI({
42
+ apiKey: normalized.apiKey || DEFAULT_API_KEY,
43
+ baseURL: normalized.baseUrl,
44
+ });
45
+ this.currentKey = nextKey;
46
+ }
47
+ this.current = normalized;
48
+ return normalized;
49
+ }
50
+ getConfig() {
51
+ return this.current ? { ...this.current } : null;
52
+ }
53
+ getClient() {
54
+ if (!this.current) {
55
+ throw new Error('Local model provider is not configured.');
56
+ }
57
+ if (!this.currentClient) {
58
+ this.configure(this.current);
59
+ }
60
+ return this.currentClient;
61
+ }
62
+ async isAvailable() {
63
+ try {
64
+ if ((await this.listModels()).length > 0) {
65
+ return true;
66
+ }
67
+ const cfg = this.requireConfig();
68
+ const response = await fetch(stripV1Suffix(cfg.baseUrl), { method: 'GET' });
69
+ return response.ok;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ async listModels() {
76
+ const cfg = this.requireConfig();
77
+ try {
78
+ const response = await this.getClient().models.list();
79
+ const ids = response.data
80
+ .map((item) => item.id)
81
+ .filter((id) => typeof id === 'string' && id.trim().length > 0);
82
+ if (ids.length > 0) {
83
+ return [...new Set(ids)].sort((a, b) => a.localeCompare(b));
84
+ }
85
+ }
86
+ catch {
87
+ // fall through to provider-specific probing
88
+ }
89
+ if (cfg.provider === 'ollama') {
90
+ try {
91
+ const response = await fetch(`${stripV1Suffix(cfg.baseUrl)}/api/tags`);
92
+ if (response.ok) {
93
+ const payload = (await response.json());
94
+ const ids = (payload.models ?? [])
95
+ .map((entry) => entry.name)
96
+ .filter((name) => typeof name === 'string' && name.trim().length > 0);
97
+ if (ids.length > 0) {
98
+ return [...new Set(ids)].sort((a, b) => a.localeCompare(b));
99
+ }
100
+ }
101
+ }
102
+ catch {
103
+ // ignore and fall through
104
+ }
105
+ }
106
+ return [];
107
+ }
108
+ requireConfig() {
109
+ if (!this.current) {
110
+ throw new Error('Local model provider is not configured.');
111
+ }
112
+ return this.current;
113
+ }
114
+ }
115
+ export const localModelProvider = new LocalModelProvider();
116
+ function normalizeBaseUrl(value) {
117
+ return value.trim().replace(/\/+$/, '');
118
+ }
119
+ function stripV1Suffix(value) {
120
+ return normalizeBaseUrl(value).replace(/\/v1$/i, '');
121
+ }
@@ -0,0 +1,44 @@
1
+ const gpt4oMini = 'gpt-4o-mini';
2
+ const gpt4o = 'gpt-4o';
3
+ export const PROFILES = {
4
+ cheap: {
5
+ plan: gpt4oMini,
6
+ chat: gpt4oMini,
7
+ edit: gpt4oMini,
8
+ review: gpt4oMini,
9
+ commit: gpt4oMini,
10
+ summarize: gpt4oMini,
11
+ },
12
+ balanced: {
13
+ plan: gpt4oMini,
14
+ chat: gpt4oMini,
15
+ edit: gpt4o,
16
+ review: gpt4o,
17
+ commit: gpt4oMini,
18
+ summarize: gpt4oMini,
19
+ },
20
+ strong: {
21
+ plan: gpt4oMini,
22
+ chat: gpt4o,
23
+ edit: gpt4o,
24
+ review: gpt4o,
25
+ commit: gpt4o,
26
+ summarize: gpt4oMini,
27
+ },
28
+ fixed: {
29
+ plan: '',
30
+ chat: '',
31
+ edit: '',
32
+ review: '',
33
+ commit: '',
34
+ summarize: '',
35
+ },
36
+ };
37
+ export function profileFor(name) {
38
+ return PROFILES[toProfile(name) ?? 'balanced'];
39
+ }
40
+ export function toProfile(name) {
41
+ return name === 'cheap' || name === 'balanced' || name === 'strong' || name === 'fixed'
42
+ ? name
43
+ : undefined;
44
+ }
@@ -0,0 +1,18 @@
1
+ import { PROFILES, toProfile } from './profiles.js';
2
+ const envProfile = toProfile(process.env.ICOPILOT_ROUTING);
3
+ const routingState = { profile: envProfile || 'fixed' };
4
+ export function setProfile(name) {
5
+ const profile = toProfile(name);
6
+ if (!profile) {
7
+ throw new Error(`Unknown routing profile: ${name}`);
8
+ }
9
+ routingState.profile = profile;
10
+ }
11
+ export function getProfile() {
12
+ return routingState.profile;
13
+ }
14
+ export function pickModel(sessionDefault, task) {
15
+ const profile = routingState.profile;
16
+ const routed = PROFILES[profile]?.[task];
17
+ return profile === 'fixed' || !routed ? sessionDefault : routed;
18
+ }
@@ -0,0 +1,151 @@
1
+ import path from 'node:path';
2
+ import { exec } from 'node:child_process';
3
+ const DEFAULT_IMAGE = process.env.ICOPILOT_SANDBOX_IMAGE?.trim() || 'node:20-alpine';
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const DEFAULT_WORKDIR = '/workspace';
6
+ const SANDBOX_LABEL = 'icopilot.sandbox=1';
7
+ const MAX_BUFFER = 10 * 1024 * 1024;
8
+ export class ContainerSandbox {
9
+ projectRoot;
10
+ containers = new Map();
11
+ constructor(projectRoot = process.cwd()) {
12
+ this.projectRoot = path.resolve(projectRoot);
13
+ }
14
+ getDefaultImage() {
15
+ return DEFAULT_IMAGE;
16
+ }
17
+ async isDockerAvailable() {
18
+ try {
19
+ const result = await this.runDocker(['docker', 'version', '--format', '{{.Server.Version}}']);
20
+ return result.code === 0 && result.stdout.trim().length > 0;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ async create(config) {
27
+ const normalized = this.normalizeConfig(config);
28
+ const args = [
29
+ 'docker',
30
+ 'run',
31
+ '-d',
32
+ '--rm',
33
+ '--init',
34
+ '--label',
35
+ SANDBOX_LABEL,
36
+ '--workdir',
37
+ normalized.workDir,
38
+ ];
39
+ for (const mount of normalized.mounts) {
40
+ const mode = mount.readonly ? 'ro' : 'rw';
41
+ args.push('-v', `${path.resolve(mount.source)}:${mount.target}:${mode}`);
42
+ }
43
+ for (const [key, value] of Object.entries(normalized.env)) {
44
+ args.push('-e', `${key}=${value}`);
45
+ }
46
+ if (normalized.memory) {
47
+ args.push('--memory', normalized.memory);
48
+ }
49
+ if (typeof normalized.cpus === 'number' &&
50
+ Number.isFinite(normalized.cpus) &&
51
+ normalized.cpus > 0) {
52
+ args.push('--cpus', String(normalized.cpus));
53
+ }
54
+ args.push(normalized.image, 'sh', '-lc', 'trap exit TERM INT; while :; do sleep 1000; done');
55
+ const result = await this.runDocker(args, normalized.timeout);
56
+ const containerId = result.stdout.trim();
57
+ if (!containerId) {
58
+ throw new Error(result.stderr.trim() || 'docker did not return a container id');
59
+ }
60
+ this.containers.set(containerId, normalized);
61
+ return containerId;
62
+ }
63
+ async exec(containerId, command) {
64
+ const normalizedId = containerId.trim();
65
+ if (!normalizedId) {
66
+ throw new Error('container id is required');
67
+ }
68
+ const knownConfig = this.containers.get(normalizedId);
69
+ return this.runDocker(['docker', 'exec', normalizedId, 'sh', '-lc', command], knownConfig?.timeout);
70
+ }
71
+ async destroy(containerId) {
72
+ const normalizedId = containerId.trim();
73
+ if (!normalizedId) {
74
+ return;
75
+ }
76
+ try {
77
+ await this.runDocker(['docker', 'rm', '-f', normalizedId]);
78
+ }
79
+ finally {
80
+ this.containers.delete(normalizedId);
81
+ }
82
+ }
83
+ async listRunning() {
84
+ const result = await this.runDocker([
85
+ 'docker',
86
+ 'ps',
87
+ '--filter',
88
+ `label=${SANDBOX_LABEL}`,
89
+ '--format',
90
+ '{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}',
91
+ ]);
92
+ return result.stdout
93
+ .split(/\r?\n/u)
94
+ .map((line) => line.trim())
95
+ .filter(Boolean)
96
+ .map((line) => {
97
+ const [id = '', image = '', status = '', name = ''] = line.split('\t');
98
+ return { id, image, status, name };
99
+ });
100
+ }
101
+ normalizeConfig(config) {
102
+ const workDir = config.workDir?.trim() || DEFAULT_WORKDIR;
103
+ const mounts = [...(config.mounts || [])];
104
+ const hasProjectMount = mounts.some((mount) => path.resolve(mount.source) === this.projectRoot || mount.target === workDir);
105
+ if (!hasProjectMount) {
106
+ mounts.unshift({
107
+ source: this.projectRoot,
108
+ target: workDir,
109
+ readonly: true,
110
+ });
111
+ }
112
+ return {
113
+ ...config,
114
+ image: config.image?.trim() || this.getDefaultImage(),
115
+ workDir,
116
+ mounts,
117
+ env: { ...(config.env || {}) },
118
+ timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
119
+ };
120
+ }
121
+ async runDocker(args, timeout = DEFAULT_TIMEOUT_MS) {
122
+ const command = args.map((arg) => quoteForShell(arg)).join(' ');
123
+ return new Promise((resolve, reject) => {
124
+ exec(command, {
125
+ timeout,
126
+ maxBuffer: MAX_BUFFER,
127
+ shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/sh',
128
+ }, (error, stdout, stderr) => {
129
+ const code = typeof error?.code === 'number'
130
+ ? Number(error.code)
131
+ : 0;
132
+ const outcome = { stdout, stderr, code };
133
+ if (error && code === 0) {
134
+ reject(error);
135
+ return;
136
+ }
137
+ if (error) {
138
+ reject(Object.assign(new Error(stderr.trim() || error.message), outcome));
139
+ return;
140
+ }
141
+ resolve(outcome);
142
+ });
143
+ });
144
+ }
145
+ }
146
+ function quoteForShell(value) {
147
+ if (process.platform === 'win32') {
148
+ return `'${value.replace(/'/g, "''")}'`;
149
+ }
150
+ return `'${value.replace(/'/g, `'\\''`)}'`;
151
+ }
@@ -0,0 +1,237 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
6
+ export function auditLogPath() {
7
+ return process.env.ICOPILOT_AUDIT_PATH || path.join(os.homedir(), '.icopilot', 'audit.log');
8
+ }
9
+ export class AuditLogger {
10
+ filePath;
11
+ constructor(filePath = auditLogPath()) {
12
+ this.filePath = filePath;
13
+ }
14
+ log(entry) {
15
+ const normalized = normalizeEntry(entry);
16
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
17
+ fs.appendFileSync(this.filePath, `${JSON.stringify(normalized)}\n`, 'utf8');
18
+ return normalized;
19
+ }
20
+ query(filter = {}) {
21
+ return this.readEntries().filter((entry) => matchesFilter(entry, filter));
22
+ }
23
+ getRecent(n = 20) {
24
+ const limit = normalizeLimit(n, 20);
25
+ return this.readEntries().slice(-limit).reverse();
26
+ }
27
+ export(targetPath, format = 'jsonl') {
28
+ const resolvedFormat = normalizeFormat(format, targetPath);
29
+ const resolvedPath = path.resolve(targetPath || defaultExportPath(resolvedFormat));
30
+ const entries = this.readEntries();
31
+ const body = resolvedFormat === 'json'
32
+ ? `${JSON.stringify(entries, null, 2)}\n`
33
+ : entries.map((entry) => JSON.stringify(entry)).join('\n') + (entries.length ? '\n' : '');
34
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
35
+ fs.writeFileSync(resolvedPath, body, 'utf8');
36
+ return resolvedPath;
37
+ }
38
+ rotate(maxAge = DEFAULT_MAX_AGE_MS) {
39
+ const entries = this.readEntries();
40
+ if (entries.length === 0)
41
+ return 0;
42
+ const cutoff = Date.now() - normalizeMaxAge(maxAge);
43
+ const kept = entries.filter((entry) => {
44
+ const timestamp = Date.parse(entry.timestamp);
45
+ return Number.isFinite(timestamp) && timestamp >= cutoff;
46
+ });
47
+ const removed = entries.length - kept.length;
48
+ if (removed === 0)
49
+ return 0;
50
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
51
+ const nextBody = kept.map((entry) => JSON.stringify(entry)).join('\n');
52
+ fs.writeFileSync(this.filePath, nextBody ? `${nextBody}\n` : '', 'utf8');
53
+ return removed;
54
+ }
55
+ getStats() {
56
+ const entries = this.readEntries();
57
+ const byAction = {};
58
+ const byTool = {};
59
+ let success = 0;
60
+ let failure = 0;
61
+ let denied = 0;
62
+ let durationTotal = 0;
63
+ let durationCount = 0;
64
+ for (const entry of entries) {
65
+ increment(byAction, entry.action);
66
+ if (entry.tool)
67
+ increment(byTool, entry.tool);
68
+ if (entry.result === 'success')
69
+ success += 1;
70
+ else if (entry.result === 'failure')
71
+ failure += 1;
72
+ else
73
+ denied += 1;
74
+ if (typeof entry.duration === 'number' &&
75
+ Number.isFinite(entry.duration) &&
76
+ entry.duration >= 0) {
77
+ durationTotal += entry.duration;
78
+ durationCount += 1;
79
+ }
80
+ }
81
+ return {
82
+ total: entries.length,
83
+ success,
84
+ failure,
85
+ denied,
86
+ firstEntry: entries[0]?.timestamp,
87
+ lastEntry: entries[entries.length - 1]?.timestamp,
88
+ avgDuration: durationCount ? Math.round(durationTotal / durationCount) : undefined,
89
+ byAction,
90
+ byTool,
91
+ };
92
+ }
93
+ readEntries() {
94
+ if (!fs.existsSync(this.filePath))
95
+ return [];
96
+ const raw = fs.readFileSync(this.filePath, 'utf8');
97
+ return raw
98
+ .split(/\r?\n/u)
99
+ .map((line) => line.trim())
100
+ .filter((line) => line.length > 0)
101
+ .flatMap((line) => {
102
+ try {
103
+ return [normalizeStoredEntry(JSON.parse(line))];
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ });
109
+ }
110
+ }
111
+ function normalizeEntry(entry) {
112
+ return {
113
+ id: typeof entry.id === 'string' && entry.id.trim().length > 0 ? entry.id : crypto.randomUUID(),
114
+ timestamp: normalizeTimestamp(entry.timestamp),
115
+ action: String(entry.action || 'tool.execute').trim() || 'tool.execute',
116
+ tool: normalizeOptionalString(entry.tool),
117
+ command: normalizeOptionalString(entry.command),
118
+ args: sanitizeArgs(entry.args),
119
+ result: normalizeResult(entry.result),
120
+ user: normalizeOptionalString(entry.user) || defaultUser(),
121
+ duration: normalizeDuration(entry.duration),
122
+ details: normalizeOptionalString(entry.details),
123
+ };
124
+ }
125
+ function normalizeStoredEntry(entry) {
126
+ const source = (entry && typeof entry === 'object' ? entry : {});
127
+ return normalizeEntry({
128
+ id: source.id,
129
+ timestamp: source.timestamp,
130
+ action: typeof source.action === 'string' ? source.action : 'tool.execute',
131
+ tool: source.tool,
132
+ command: source.command,
133
+ args: source.args,
134
+ result: normalizeResult(source.result),
135
+ user: source.user,
136
+ duration: source.duration,
137
+ details: source.details,
138
+ });
139
+ }
140
+ function normalizeTimestamp(value) {
141
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
142
+ return value.toISOString();
143
+ if (typeof value === 'string') {
144
+ const parsed = Date.parse(value);
145
+ if (Number.isFinite(parsed))
146
+ return new Date(parsed).toISOString();
147
+ }
148
+ return new Date().toISOString();
149
+ }
150
+ function normalizeResult(value) {
151
+ return value === 'success' || value === 'failure' || value === 'denied' ? value : 'failure';
152
+ }
153
+ function normalizeDuration(value) {
154
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
155
+ ? Math.round(value)
156
+ : undefined;
157
+ }
158
+ function normalizeOptionalString(value) {
159
+ if (typeof value !== 'string')
160
+ return undefined;
161
+ const trimmed = value.trim();
162
+ return trimmed.length > 0 ? trimmed : undefined;
163
+ }
164
+ function defaultUser() {
165
+ return normalizeOptionalString(process.env.ICOPILOT_AUDIT_USER || process.env.USERNAME || process.env.USER);
166
+ }
167
+ function sanitizeArgs(value, depth = 0) {
168
+ if (value === undefined || value === null)
169
+ return value;
170
+ if (depth >= 4)
171
+ return '[truncated depth]';
172
+ if (typeof value === 'string') {
173
+ return value.length > 4000
174
+ ? `${value.slice(0, 4000)}…[truncated ${value.length - 4000} chars]`
175
+ : value;
176
+ }
177
+ if (typeof value === 'number' || typeof value === 'boolean')
178
+ return value;
179
+ if (Array.isArray(value)) {
180
+ return value.slice(0, 50).map((item) => sanitizeArgs(item, depth + 1));
181
+ }
182
+ if (typeof value === 'object') {
183
+ return Object.fromEntries(Object.entries(value)
184
+ .slice(0, 50)
185
+ .map(([key, item]) => [key, sanitizeArgs(item, depth + 1)]));
186
+ }
187
+ return String(value);
188
+ }
189
+ function matchesFilter(entry, filter) {
190
+ const from = toTimestamp(filter.from);
191
+ const to = toTimestamp(filter.to);
192
+ const entryTime = Date.parse(entry.timestamp);
193
+ if (from !== undefined && (!Number.isFinite(entryTime) || entryTime < from))
194
+ return false;
195
+ if (to !== undefined && (!Number.isFinite(entryTime) || entryTime > to))
196
+ return false;
197
+ if (filter.result && entry.result !== filter.result)
198
+ return false;
199
+ if (filter.action && entry.action.toLowerCase() !== filter.action.toLowerCase())
200
+ return false;
201
+ if (filter.tool && (entry.tool || '').toLowerCase() !== filter.tool.toLowerCase())
202
+ return false;
203
+ return true;
204
+ }
205
+ function toTimestamp(value) {
206
+ if (value instanceof Date)
207
+ return value.getTime();
208
+ if (typeof value === 'string') {
209
+ const parsed = Date.parse(value);
210
+ return Number.isFinite(parsed) ? parsed : undefined;
211
+ }
212
+ return undefined;
213
+ }
214
+ function normalizeLimit(value, fallback) {
215
+ if (!Number.isFinite(value) || value <= 0)
216
+ return fallback;
217
+ return Math.max(1, Math.floor(value));
218
+ }
219
+ function normalizeMaxAge(value) {
220
+ if (!Number.isFinite(value) || value <= 0)
221
+ return DEFAULT_MAX_AGE_MS;
222
+ return Math.floor(value);
223
+ }
224
+ function normalizeFormat(format, targetPath) {
225
+ if (targetPath?.toLowerCase().endsWith('.json'))
226
+ return 'json';
227
+ return format === 'json' ? 'json' : 'jsonl';
228
+ }
229
+ function defaultExportPath(format) {
230
+ const stamp = new Date().toISOString().replace(/[:.]/gu, '-');
231
+ return path.join(process.cwd(), `icopilot-audit-${stamp}.${format === 'json' ? 'json' : 'log'}`);
232
+ }
233
+ function increment(target, key) {
234
+ if (!key)
235
+ return;
236
+ target[key] = (target[key] || 0) + 1;
237
+ }