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,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
const WINDOWS_FALLBACK_EDITOR = 'code --wait';
|
|
6
|
+
const UNIX_FALLBACK_EDITOR = 'vim';
|
|
7
|
+
function detectEditor() {
|
|
8
|
+
const visual = process.env.VISUAL?.trim();
|
|
9
|
+
if (visual)
|
|
10
|
+
return visual;
|
|
11
|
+
const editor = process.env.EDITOR?.trim();
|
|
12
|
+
if (editor)
|
|
13
|
+
return editor;
|
|
14
|
+
return process.platform === 'win32' ? WINDOWS_FALLBACK_EDITOR : UNIX_FALLBACK_EDITOR;
|
|
15
|
+
}
|
|
16
|
+
function resolveEditorWorkspace() {
|
|
17
|
+
const candidate = config.cwd || process.cwd();
|
|
18
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
return process.cwd();
|
|
22
|
+
}
|
|
23
|
+
function createTempFilePath(cwd) {
|
|
24
|
+
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
25
|
+
return path.join(cwd, `.icopilot-editor-${nonce}.md`);
|
|
26
|
+
}
|
|
27
|
+
export async function openEditor() {
|
|
28
|
+
const cwd = resolveEditorWorkspace();
|
|
29
|
+
const editor = detectEditor();
|
|
30
|
+
const tempFilePath = createTempFilePath(cwd);
|
|
31
|
+
fs.writeFileSync(tempFilePath, '', 'utf8');
|
|
32
|
+
try {
|
|
33
|
+
const escapedPath = `"${tempFilePath.replace(/"/g, '\\"')}"`;
|
|
34
|
+
const result = spawnSync(`${editor} ${escapedPath}`, {
|
|
35
|
+
cwd,
|
|
36
|
+
shell: true,
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
});
|
|
39
|
+
if (result.error)
|
|
40
|
+
throw result.error;
|
|
41
|
+
if (result.status !== 0 || result.signal)
|
|
42
|
+
return null;
|
|
43
|
+
const content = fs.readFileSync(tempFilePath, 'utf8').trim();
|
|
44
|
+
return content ? content : null;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
fs.rmSync(tempFilePath, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
const SECRET_MIN_LENGTH = 8;
|
|
5
|
+
export function maskSecret(value) {
|
|
6
|
+
if (value.length < SECRET_MIN_LENGTH)
|
|
7
|
+
return '***';
|
|
8
|
+
return `${value.slice(0, 4)}***${value.slice(-2)}`;
|
|
9
|
+
}
|
|
10
|
+
export function envCommand(args) {
|
|
11
|
+
if (args[0] === '--check') {
|
|
12
|
+
return checkVar(args[1]);
|
|
13
|
+
}
|
|
14
|
+
if (args[0] === '--full') {
|
|
15
|
+
return formatEnvInfo('iCopilot environment', getFullEnvInfo());
|
|
16
|
+
}
|
|
17
|
+
return formatEnvInfo('Environment context', getDefaultEnvInfo());
|
|
18
|
+
}
|
|
19
|
+
function checkVar(name) {
|
|
20
|
+
if (!name)
|
|
21
|
+
return theme.warn('usage: /env --check <VAR>\n');
|
|
22
|
+
const value = process.env[name];
|
|
23
|
+
const source = value === undefined ? 'unset' : 'env';
|
|
24
|
+
const rendered = value === undefined ? theme.dim('(not set)') : renderValue(name, value);
|
|
25
|
+
return formatEnvInfo('Environment check', [{ key: name, value: rendered, source }]);
|
|
26
|
+
}
|
|
27
|
+
function getDefaultEnvInfo() {
|
|
28
|
+
const shellValue = process.env.SHELL || process.env.ComSpec;
|
|
29
|
+
const homeValue = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
30
|
+
const info = [
|
|
31
|
+
envEntry('GITHUB_TOKEN', process.env.GITHUB_TOKEN, 'env'),
|
|
32
|
+
envEntry('ICOPILOT_MODEL', process.env.ICOPILOT_MODEL, 'env'),
|
|
33
|
+
envEntry('ICOPILOT_THEME', process.env.ICOPILOT_THEME, 'env'),
|
|
34
|
+
envEntry('ICOPILOT_SANDBOX', process.env.ICOPILOT_SANDBOX, 'env'),
|
|
35
|
+
envEntry('ICOPILOT_DEBUG', process.env.ICOPILOT_DEBUG, 'env'),
|
|
36
|
+
envEntry('SHELL / ComSpec', shellValue, shellValue ? 'env' : 'unset'),
|
|
37
|
+
runtimeEntry('NODE_VERSION', process.version),
|
|
38
|
+
runtimeEntry('CWD', process.cwd()),
|
|
39
|
+
runtimeEntry('HOME', homeValue),
|
|
40
|
+
];
|
|
41
|
+
const branch = gitBranch();
|
|
42
|
+
if (branch)
|
|
43
|
+
info.push({ key: 'Git branch', value: branch, source: 'git' });
|
|
44
|
+
return info;
|
|
45
|
+
}
|
|
46
|
+
function getFullEnvInfo() {
|
|
47
|
+
const matches = Object.entries(process.env)
|
|
48
|
+
.filter(([key]) => key.startsWith('ICOPILOT_'))
|
|
49
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
50
|
+
if (matches.length === 0) {
|
|
51
|
+
return [{ key: 'ICOPILOT_*', value: theme.dim('(none set)'), source: 'env' }];
|
|
52
|
+
}
|
|
53
|
+
return matches.map(([key, value]) => envEntry(key, value, 'env'));
|
|
54
|
+
}
|
|
55
|
+
function envEntry(key, value, source) {
|
|
56
|
+
if (value === undefined) {
|
|
57
|
+
return { key, value: theme.dim('(not set)'), source: 'unset' };
|
|
58
|
+
}
|
|
59
|
+
return { key, value: renderValue(key, value), source };
|
|
60
|
+
}
|
|
61
|
+
function runtimeEntry(key, value) {
|
|
62
|
+
return { key, value, source: 'runtime' };
|
|
63
|
+
}
|
|
64
|
+
function renderValue(key, value) {
|
|
65
|
+
return isSecretKey(key) ? maskSecret(value) : value;
|
|
66
|
+
}
|
|
67
|
+
function isSecretKey(key) {
|
|
68
|
+
return /(TOKEN|SECRET|PASSWORD|KEY)$/i.test(key);
|
|
69
|
+
}
|
|
70
|
+
function gitBranch() {
|
|
71
|
+
try {
|
|
72
|
+
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
76
|
+
}).trim();
|
|
77
|
+
return branch || null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function formatEnvInfo(title, items) {
|
|
84
|
+
const lines = items.map(({ key, value, source }) => ` ${theme.hl(key)} ${theme.dim(`[${source}]`)} ${value}`);
|
|
85
|
+
return `${theme.brand(title)}\n${lines.join('\n')}\n`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
const DEFAULT_MAX_BYTES = 32_000;
|
|
5
|
+
const DEFAULT_MAX_FILES = 25;
|
|
6
|
+
const SKIP_NAMES = new Set(['node_modules', '.git', 'dist']);
|
|
7
|
+
function readFilePreview(filePath, maxBytes) {
|
|
8
|
+
const fd = fs.openSync(filePath, 'r');
|
|
9
|
+
try {
|
|
10
|
+
const buffer = Buffer.alloc(Math.max(0, maxBytes));
|
|
11
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
12
|
+
return buffer.toString('utf8', 0, bytesRead);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
fs.closeSync(fd);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function buildDirPreview(dirPath, maxFiles) {
|
|
19
|
+
const entries = fs
|
|
20
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
21
|
+
.filter((entry) => !entry.name.startsWith('.') && !SKIP_NAMES.has(entry.name))
|
|
22
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
23
|
+
const immediate = entries.map((entry) => `- ${entry.name}${entry.isDirectory() ? '/' : ''}`);
|
|
24
|
+
const files = fg.sync('**/*', {
|
|
25
|
+
cwd: dirPath,
|
|
26
|
+
onlyFiles: true,
|
|
27
|
+
dot: false,
|
|
28
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
29
|
+
deep: 1,
|
|
30
|
+
unique: true,
|
|
31
|
+
});
|
|
32
|
+
const topFiles = files
|
|
33
|
+
.sort((a, b) => a.localeCompare(b))
|
|
34
|
+
.slice(0, Math.max(0, maxFiles))
|
|
35
|
+
.map((file) => ` - ${file}`);
|
|
36
|
+
return [`# Entries`, ...immediate, '', `# Top-level files`, ...topFiles].join('\n').trim();
|
|
37
|
+
}
|
|
38
|
+
export function buildExplain(target, cwd, opts = {}) {
|
|
39
|
+
const resolvedPath = path.resolve(cwd, target);
|
|
40
|
+
let stat;
|
|
41
|
+
try {
|
|
42
|
+
stat = fs.statSync(resolvedPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'missing',
|
|
47
|
+
path: resolvedPath,
|
|
48
|
+
preview: '',
|
|
49
|
+
prompt: `Explain request for ${resolvedPath}: the path doesn't exist. Suggest what to check next.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (stat.isFile()) {
|
|
53
|
+
const preview = readFilePreview(resolvedPath, opts.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
54
|
+
return {
|
|
55
|
+
kind: 'file',
|
|
56
|
+
path: resolvedPath,
|
|
57
|
+
preview,
|
|
58
|
+
prompt: `Give a concise structured overview of this file (${resolvedPath}) in <= 200 words. ` +
|
|
59
|
+
`Cover purpose, key exports, and notable risks.\n\n${preview}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
const preview = buildDirPreview(resolvedPath, opts.maxFiles ?? DEFAULT_MAX_FILES);
|
|
64
|
+
return {
|
|
65
|
+
kind: 'dir',
|
|
66
|
+
path: resolvedPath,
|
|
67
|
+
preview,
|
|
68
|
+
prompt: `Give an architectural summary of this folder (${resolvedPath}). ` +
|
|
69
|
+
`Explain the apparent responsibilities, important files, and likely extension points.\n\n${preview}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
kind: 'missing',
|
|
74
|
+
path: resolvedPath,
|
|
75
|
+
preview: '',
|
|
76
|
+
prompt: `Explain request for ${resolvedPath}: the path doesn't exist. Suggest what to check next.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function explainShellCommand(command) {
|
|
2
|
+
const displayCommand = command || '(empty command)';
|
|
3
|
+
const prompt = [
|
|
4
|
+
'You are explaining a shell command to a developer.',
|
|
5
|
+
'Explain the command in plain English without assuming the user already knows the tool.',
|
|
6
|
+
'Do not invent flags or behavior that are not present.',
|
|
7
|
+
'Structure the answer as:',
|
|
8
|
+
'1. A short summary of the command purpose',
|
|
9
|
+
'2. A step-by-step breakdown of each subcommand, flag, argument, and shell operator',
|
|
10
|
+
'3. Likely effects on files, history, network, or system state',
|
|
11
|
+
'4. Risks, destructive behavior, or irreversible consequences',
|
|
12
|
+
'5. Safer alternatives or precautions when relevant',
|
|
13
|
+
'If the command is empty, incomplete, or ambiguous, say so clearly and explain what is missing.',
|
|
14
|
+
'',
|
|
15
|
+
`Command: ${displayCommand}`,
|
|
16
|
+
`Raw command text: ${JSON.stringify(command)}`,
|
|
17
|
+
].join('\n');
|
|
18
|
+
return {
|
|
19
|
+
command,
|
|
20
|
+
prompt,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
5
|
+
const DEFAULT_MAX_FILES = 200;
|
|
6
|
+
const DEFAULT_IGNORES = new Set(['.git', 'node_modules', 'dist']);
|
|
7
|
+
export function buildExplorePrompt(query, cwd) {
|
|
8
|
+
const resolvedCwd = path.resolve(cwd);
|
|
9
|
+
const context = [
|
|
10
|
+
`Workspace: ${resolvedCwd}`,
|
|
11
|
+
'',
|
|
12
|
+
'File tree (depth <= 3, capped at 200 files):',
|
|
13
|
+
gatherProjectContext(resolvedCwd, DEFAULT_MAX_DEPTH),
|
|
14
|
+
'',
|
|
15
|
+
'Project metadata:',
|
|
16
|
+
readProjectMetadata(resolvedCwd),
|
|
17
|
+
'',
|
|
18
|
+
'README snippet:',
|
|
19
|
+
readReadmeSnippet(resolvedCwd),
|
|
20
|
+
'',
|
|
21
|
+
'Git status:',
|
|
22
|
+
readGitStatus(resolvedCwd),
|
|
23
|
+
].join('\n');
|
|
24
|
+
const prompt = [
|
|
25
|
+
'You are Explore, a lightweight codebase exploration agent.',
|
|
26
|
+
'Answer the user question using only the supplied project context.',
|
|
27
|
+
'Focus on architecture, likely file locations, relevant entry points, and practical next files to inspect.',
|
|
28
|
+
'When you mention files, use repo-relative paths.',
|
|
29
|
+
'If the context is incomplete, say what is missing instead of inventing details.',
|
|
30
|
+
'',
|
|
31
|
+
`User question: ${query.trim()}`,
|
|
32
|
+
'',
|
|
33
|
+
'Project context:',
|
|
34
|
+
context,
|
|
35
|
+
].join('\n');
|
|
36
|
+
return { prompt, context };
|
|
37
|
+
}
|
|
38
|
+
export function gatherProjectContext(cwd, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
39
|
+
const resolvedCwd = path.resolve(cwd);
|
|
40
|
+
if (!fs.existsSync(resolvedCwd) || !fs.statSync(resolvedCwd).isDirectory()) {
|
|
41
|
+
return '(workspace not found)';
|
|
42
|
+
}
|
|
43
|
+
const ignorePatterns = readGitignorePatterns(resolvedCwd);
|
|
44
|
+
const lines = ['.'];
|
|
45
|
+
let fileCount = 0;
|
|
46
|
+
let truncated = false;
|
|
47
|
+
const walk = (dirPath, depth, indent) => {
|
|
48
|
+
if (truncated || depth > maxDepth)
|
|
49
|
+
return;
|
|
50
|
+
const entries = fs
|
|
51
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
52
|
+
.filter((entry) => !shouldIgnore(path.relative(resolvedCwd, path.join(dirPath, entry.name)), entry.isDirectory(), ignorePatterns))
|
|
53
|
+
.sort((left, right) => {
|
|
54
|
+
if (left.isDirectory() !== right.isDirectory()) {
|
|
55
|
+
return left.isDirectory() ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
return left.name.localeCompare(right.name);
|
|
58
|
+
});
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (truncated)
|
|
61
|
+
break;
|
|
62
|
+
const childPath = path.join(dirPath, entry.name);
|
|
63
|
+
const label = `${indent}${entry.name}${entry.isDirectory() ? '/' : ''}`;
|
|
64
|
+
lines.push(label);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
if (depth < maxDepth) {
|
|
67
|
+
walk(childPath, depth + 1, `${indent} `);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
fileCount += 1;
|
|
72
|
+
if (fileCount >= DEFAULT_MAX_FILES) {
|
|
73
|
+
truncated = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(resolvedCwd, 1, ' ');
|
|
78
|
+
if (truncated) {
|
|
79
|
+
lines.push(` … truncated after ${DEFAULT_MAX_FILES} files`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
export function exploreCommand(args, cwd) {
|
|
84
|
+
const query = args.join(' ').trim();
|
|
85
|
+
if (!query) {
|
|
86
|
+
return 'usage: /explore <question>\n';
|
|
87
|
+
}
|
|
88
|
+
return buildExplorePrompt(query, cwd).prompt;
|
|
89
|
+
}
|
|
90
|
+
function readProjectMetadata(cwd) {
|
|
91
|
+
const sections = [];
|
|
92
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
93
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
94
|
+
sections.push(formatPackageJson(packageJsonPath));
|
|
95
|
+
}
|
|
96
|
+
const cargoTomlPath = path.join(cwd, 'Cargo.toml');
|
|
97
|
+
if (fs.existsSync(cargoTomlPath)) {
|
|
98
|
+
sections.push(formatSnippet('Cargo.toml', cargoTomlPath, 20));
|
|
99
|
+
}
|
|
100
|
+
const goModPath = path.join(cwd, 'go.mod');
|
|
101
|
+
if (fs.existsSync(goModPath)) {
|
|
102
|
+
sections.push(formatSnippet('go.mod', goModPath, 20));
|
|
103
|
+
}
|
|
104
|
+
if (!sections.length) {
|
|
105
|
+
return '(no package.json, Cargo.toml, or go.mod found)';
|
|
106
|
+
}
|
|
107
|
+
return sections.join('\n\n');
|
|
108
|
+
}
|
|
109
|
+
function formatPackageJson(packageJsonPath) {
|
|
110
|
+
try {
|
|
111
|
+
const raw = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
112
|
+
const scripts = Object.keys(raw.scripts ?? {}).sort((a, b) => a.localeCompare(b));
|
|
113
|
+
const deps = Object.keys(raw.dependencies ?? {}).sort((a, b) => a.localeCompare(b));
|
|
114
|
+
const devDeps = Object.keys(raw.devDependencies ?? {}).sort((a, b) => a.localeCompare(b));
|
|
115
|
+
return [
|
|
116
|
+
'package.json:',
|
|
117
|
+
` name: ${raw.name ?? '(unknown)'}`,
|
|
118
|
+
` type: ${raw.type ?? '(unspecified)'}`,
|
|
119
|
+
` scripts: ${scripts.length ? scripts.join(', ') : '(none)'}`,
|
|
120
|
+
` dependencies (${deps.length}): ${deps.slice(0, 12).join(', ') || '(none)'}`,
|
|
121
|
+
` devDependencies (${devDeps.length}): ${devDeps.slice(0, 12).join(', ') || '(none)'}`,
|
|
122
|
+
].join('\n');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return 'package.json:\n (failed to parse)';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function readReadmeSnippet(cwd) {
|
|
129
|
+
const readmeNames = ['README.md', 'README', 'readme.md', 'readme'];
|
|
130
|
+
const readmePath = readmeNames
|
|
131
|
+
.map((name) => path.join(cwd, name))
|
|
132
|
+
.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
133
|
+
if (!readmePath) {
|
|
134
|
+
return '(no README found)';
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const snippet = fs.readFileSync(readmePath, 'utf8').slice(0, 1_500).trim();
|
|
138
|
+
return snippet || '(README is empty)';
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return '(failed to read README)';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function readGitStatus(cwd) {
|
|
145
|
+
try {
|
|
146
|
+
const output = execFileSync('git', ['status', '--short', '--branch'], {
|
|
147
|
+
cwd,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
+
}).trim();
|
|
151
|
+
return output || '(clean working tree)';
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return '(not a git repository or git status unavailable)';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function formatSnippet(label, filePath, maxLines) {
|
|
158
|
+
try {
|
|
159
|
+
const lines = fs
|
|
160
|
+
.readFileSync(filePath, 'utf8')
|
|
161
|
+
.split(/\r?\n/)
|
|
162
|
+
.slice(0, maxLines)
|
|
163
|
+
.join('\n')
|
|
164
|
+
.trim();
|
|
165
|
+
return `${label}:\n${lines || '(empty)'}`;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return `${label}:\n(failed to read)`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function readGitignorePatterns(cwd) {
|
|
172
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
173
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return fs
|
|
178
|
+
.readFileSync(gitignorePath, 'utf8')
|
|
179
|
+
.split(/\r?\n/)
|
|
180
|
+
.map((line) => line.trim())
|
|
181
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!'));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function shouldIgnore(relativePath, isDirectory, patterns) {
|
|
188
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const normalized = normalizeSlashes(relativePath);
|
|
192
|
+
const segments = normalized.split('/');
|
|
193
|
+
if (segments.some((segment) => DEFAULT_IGNORES.has(segment))) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return patterns.some((pattern) => matchesGitignorePattern(normalized, segments, isDirectory, pattern));
|
|
197
|
+
}
|
|
198
|
+
function matchesGitignorePattern(normalizedPath, segments, isDirectory, pattern) {
|
|
199
|
+
const normalizedPattern = normalizeSlashes(pattern).replace(/^\/+/, '');
|
|
200
|
+
const directoryOnly = normalizedPattern.endsWith('/');
|
|
201
|
+
const barePattern = normalizedPattern.replace(/\/+$/, '');
|
|
202
|
+
if (directoryOnly && !isDirectory && barePattern === normalizedPath) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (!barePattern) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (!hasGlob(barePattern) && !barePattern.includes('/')) {
|
|
209
|
+
return segments.includes(barePattern);
|
|
210
|
+
}
|
|
211
|
+
if (!hasGlob(barePattern)) {
|
|
212
|
+
return normalizedPath === barePattern || normalizedPath.startsWith(`${barePattern}/`);
|
|
213
|
+
}
|
|
214
|
+
const regex = globToRegExp(barePattern);
|
|
215
|
+
return regex.test(normalizedPath);
|
|
216
|
+
}
|
|
217
|
+
function hasGlob(value) {
|
|
218
|
+
return /[*?[\]]/.test(value);
|
|
219
|
+
}
|
|
220
|
+
function globToRegExp(pattern) {
|
|
221
|
+
const source = pattern
|
|
222
|
+
.replace(/[|\\{}()[\]^$+.]/g, '\\$&')
|
|
223
|
+
.replace(/\*\*/g, '::DOUBLE_STAR::')
|
|
224
|
+
.replace(/\*/g, '[^/]*')
|
|
225
|
+
.replace(/\?/g, '[^/]')
|
|
226
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
227
|
+
return new RegExp(`^${source}(?:/.*)?$`);
|
|
228
|
+
}
|
|
229
|
+
function normalizeSlashes(value) {
|
|
230
|
+
return value.split(path.sep).join('/');
|
|
231
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
export function feedbackPath() {
|
|
7
|
+
const configured = process.env.ICOPILOT_FEEDBACK_PATH || path.join(os.homedir(), '.icopilot', 'feedback.json');
|
|
8
|
+
if (configured === '~')
|
|
9
|
+
return os.homedir();
|
|
10
|
+
if (/^~[\\/]/.test(configured))
|
|
11
|
+
return path.join(os.homedir(), configured.slice(2));
|
|
12
|
+
return path.resolve(configured);
|
|
13
|
+
}
|
|
14
|
+
export function submitFeedback(type, text, options = {}) {
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new Error('feedback text is required');
|
|
18
|
+
}
|
|
19
|
+
const entries = loadFeedback();
|
|
20
|
+
const repo = options.repo ?? getGitHubRepoSlug(options.cwd);
|
|
21
|
+
entries.push({
|
|
22
|
+
type,
|
|
23
|
+
text: trimmed,
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
repo: repo ?? undefined,
|
|
27
|
+
});
|
|
28
|
+
saveFeedback(entries);
|
|
29
|
+
const lines = [theme.ok('Thank you for your feedback!')];
|
|
30
|
+
if (repo) {
|
|
31
|
+
lines.push(theme.dim(`GitHub issues: ${buildGitHubIssuesUrl(repo)}`));
|
|
32
|
+
}
|
|
33
|
+
return `${lines.join('\n')}\n`;
|
|
34
|
+
}
|
|
35
|
+
export function loadFeedback() {
|
|
36
|
+
const file = feedbackPath();
|
|
37
|
+
if (!fs.existsSync(file))
|
|
38
|
+
return [];
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
41
|
+
if (!Array.isArray(parsed))
|
|
42
|
+
return [];
|
|
43
|
+
return parsed.flatMap((entry) => {
|
|
44
|
+
if (!entry || typeof entry !== 'object')
|
|
45
|
+
return [];
|
|
46
|
+
const record = entry;
|
|
47
|
+
if ((record.type !== 'bug' && record.type !== 'feature' && record.type !== 'praise') ||
|
|
48
|
+
typeof record.text !== 'string' ||
|
|
49
|
+
typeof record.timestamp !== 'string') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
type: record.type,
|
|
55
|
+
text: record.text,
|
|
56
|
+
timestamp: record.timestamp,
|
|
57
|
+
cwd: typeof record.cwd === 'string' ? record.cwd : undefined,
|
|
58
|
+
repo: typeof record.repo === 'string' ? record.repo : undefined,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function saveFeedback(entries) {
|
|
68
|
+
const file = feedbackPath();
|
|
69
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
|
+
fs.writeFileSync(file, `${JSON.stringify(entries, null, 2)}\n`, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
export function buildGitHubIssuesUrl(repo) {
|
|
73
|
+
return `https://github.com/${repo}/issues/new`;
|
|
74
|
+
}
|
|
75
|
+
export function getGitHubRepoSlug(cwd = process.cwd()) {
|
|
76
|
+
try {
|
|
77
|
+
const remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
78
|
+
cwd,
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
}).trim();
|
|
83
|
+
const match = remote.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?$/i);
|
|
84
|
+
return match?.[1] ?? null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function openGitHubIssues(repo) {
|
|
91
|
+
const url = buildGitHubIssuesUrl(repo);
|
|
92
|
+
const result = process.platform === 'win32'
|
|
93
|
+
? spawnSync('cmd', ['/c', 'start', '', url], { windowsHide: true })
|
|
94
|
+
: process.platform === 'darwin'
|
|
95
|
+
? spawnSync('open', [url], { windowsHide: true })
|
|
96
|
+
: spawnSync('xdg-open', [url], { windowsHide: true });
|
|
97
|
+
return result.status === 0;
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function buildFixPrompt(errorText) {
|
|
2
|
+
const error = errorText;
|
|
3
|
+
const trimmedError = errorText.trim();
|
|
4
|
+
const quotedError = trimmedError.length > 0 ? trimmedError : '[no error text provided]';
|
|
5
|
+
return {
|
|
6
|
+
error,
|
|
7
|
+
prompt: [
|
|
8
|
+
'You are helping diagnose a CLI or developer tooling error.',
|
|
9
|
+
'Identify the error shown below, explain the most likely root cause, and suggest 2-3 fixes ranked by likelihood.',
|
|
10
|
+
'For each fix, include the exact commands to run and a short note about what the command does.',
|
|
11
|
+
'If the error text is ambiguous, say so briefly and still provide the most plausible fixes first.',
|
|
12
|
+
'',
|
|
13
|
+
'Error text:',
|
|
14
|
+
quotedError,
|
|
15
|
+
].join('\n'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function detectShell() {
|
|
2
|
+
const candidates = [
|
|
3
|
+
process.env.SHELL,
|
|
4
|
+
process.env.ICOPILOT_SHELL,
|
|
5
|
+
process.env.TERM_PROGRAM,
|
|
6
|
+
process.env.ComSpec,
|
|
7
|
+
]
|
|
8
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
9
|
+
.map((value) => value.toLowerCase());
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
if (candidate.includes('pwsh') || candidate.includes('powershell'))
|
|
12
|
+
return 'pwsh';
|
|
13
|
+
if (candidate.includes('zsh'))
|
|
14
|
+
return 'zsh';
|
|
15
|
+
if (candidate.includes('fish'))
|
|
16
|
+
return 'fish';
|
|
17
|
+
if (candidate.includes('bash'))
|
|
18
|
+
return 'bash';
|
|
19
|
+
}
|
|
20
|
+
return 'bash';
|
|
21
|
+
}
|
|
22
|
+
export function buildGeneratePrompt(goal, shell = detectShell()) {
|
|
23
|
+
const prompt = [
|
|
24
|
+
`You are generating a complete, runnable ${shell} script for a developer.`,
|
|
25
|
+
'Return only the script text.',
|
|
26
|
+
'Use the requested shell dialect consistently.',
|
|
27
|
+
'Include helpful comments that explain the major steps.',
|
|
28
|
+
'Handle errors safely: enable set -e immediately when the shell supports it, and use the closest equivalent for other shells.',
|
|
29
|
+
'Make the script executable as written with sensible defaults and clear variable names.',
|
|
30
|
+
'',
|
|
31
|
+
`Goal: ${goal}`,
|
|
32
|
+
].join('\n');
|
|
33
|
+
return {
|
|
34
|
+
goal,
|
|
35
|
+
shell,
|
|
36
|
+
prompt,
|
|
37
|
+
};
|
|
38
|
+
}
|