agent-termo 0.0.0-stage → 1.0.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/.claude/agents/backend-architecture-designer.md +1250 -0
- package/.claude/agents/backend-expert.md +1731 -0
- package/.claude/agents/deployment-expert.md +1126 -0
- package/.claude/agents/dev-environment-initiator.md +788 -0
- package/.claude/agents/frontend-expert.md +1343 -0
- package/.claude/agents/frontend-planner.md +1044 -0
- package/.claude/agents/git-manager.md +811 -0
- package/.claude/agents/project-manager.md +843 -0
- package/.claude/agents/security-expert.md +835 -0
- package/.claude/agents/tech-stack-expert.md +1357 -0
- package/.claude/agents/test-agent.md +1372 -0
- package/.claude/agents/uiux-designer.md +955 -0
- package/.claude/commands/build.md +5 -0
- package/.claude/commands/deploy.md +3 -0
- package/.claude/commands/feature.md +3 -0
- package/.claude/commands/fix.md +3 -0
- package/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/README.md +169 -3
- package/dist/.claude/agents/backend-architecture-designer.md +1250 -0
- package/dist/.claude/agents/backend-expert.md +1731 -0
- package/dist/.claude/agents/deployment-expert.md +1126 -0
- package/dist/.claude/agents/dev-environment-initiator.md +788 -0
- package/dist/.claude/agents/frontend-expert.md +1343 -0
- package/dist/.claude/agents/frontend-planner.md +1044 -0
- package/dist/.claude/agents/git-manager.md +811 -0
- package/dist/.claude/agents/project-manager.md +843 -0
- package/dist/.claude/agents/security-expert.md +835 -0
- package/dist/.claude/agents/tech-stack-expert.md +1357 -0
- package/dist/.claude/agents/test-agent.md +1372 -0
- package/dist/.claude/agents/uiux-designer.md +955 -0
- package/dist/.claude/commands/build.md +5 -0
- package/dist/.claude/commands/deploy.md +3 -0
- package/dist/.claude/commands/feature.md +3 -0
- package/dist/.claude/commands/fix.md +3 -0
- package/dist/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/dist/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/dist/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +7 -0
- package/dist/main.js +173 -0
- package/dist/main.js.map +7 -0
- package/package.json +89 -5
- package/src/agents/index.ts +94 -0
- package/src/cli.ts +76 -0
- package/src/diff/index.ts +255 -0
- package/src/llm/client.ts +192 -0
- package/src/llm/tools.ts +206 -0
- package/src/main.ts +427 -0
- package/src/memory/index.ts +130 -0
- package/src/tools/index.ts +371 -0
- package/src/types/index.ts +151 -0
- package/src/ui/index.ts +519 -0
- package/src/utils/ansi.ts +134 -0
- package/src/utils/config.ts +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agent-termo",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-termo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal-based AI coding agent that replicates Claude Code experience",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-termo": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx watch src/cli.ts",
|
|
12
|
+
"build": "tsx scripts/build.mjs",
|
|
13
|
+
"build:tsc": "tsc",
|
|
14
|
+
"build:esbuild": "node scripts/build.mjs",
|
|
15
|
+
"build:pkg": "pkg --targets node20-win-x64,node20-macos-x64,node20-linux-x64 --output dist/",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"test:coverage": "vitest run --coverage",
|
|
21
|
+
"lint": "eslint src --ext .ts",
|
|
22
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
23
|
+
"cli": "tsx src/cli.ts"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"terminal",
|
|
28
|
+
"ai",
|
|
29
|
+
"agent",
|
|
30
|
+
"coding",
|
|
31
|
+
"llm",
|
|
32
|
+
"nvidia",
|
|
33
|
+
"nim"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/krish2312005/agent-termo.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/krish2312005/agent-termo#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/krish2312005/agent-termo/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"blessed": "^0.1.10",
|
|
49
|
+
"chalk": "^5.3.0",
|
|
50
|
+
"commander": "^12.1.0",
|
|
51
|
+
"fs-extra": "^11.2.0",
|
|
52
|
+
"marked": "^12.0.0",
|
|
53
|
+
"openai": "^4.50.0",
|
|
54
|
+
"simple-git": "^3.24.0",
|
|
55
|
+
"zod": "^3.23.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/glob": "^8.1.0",
|
|
59
|
+
"@types/node": "^20.12.0",
|
|
60
|
+
"@vitest/coverage-v8": "^1.5.0",
|
|
61
|
+
"esbuild": "^0.21.0",
|
|
62
|
+
"eslint": "^9.4.0",
|
|
63
|
+
"pkg": "^5.8.1",
|
|
64
|
+
"tsx": "^4.10.0",
|
|
65
|
+
"typescript": "^5.4.0",
|
|
66
|
+
"typescript-eslint": "^8.0.0",
|
|
67
|
+
"vitest": "^1.5.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=20.0.0"
|
|
71
|
+
},
|
|
72
|
+
"files": [
|
|
73
|
+
"dist",
|
|
74
|
+
"src",
|
|
75
|
+
".claude"
|
|
76
|
+
],
|
|
77
|
+
"pkg": {
|
|
78
|
+
"targets": [
|
|
79
|
+
"node20-win-x64",
|
|
80
|
+
"node20-macos-x64",
|
|
81
|
+
"node20-linux-x64"
|
|
82
|
+
],
|
|
83
|
+
"assets": [
|
|
84
|
+
".claude/agents/**/*",
|
|
85
|
+
".claude/commands/**/*",
|
|
86
|
+
".claude/skills/**/*"
|
|
87
|
+
],
|
|
88
|
+
"outputPath": "dist"
|
|
89
|
+
}
|
|
6
90
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Agent Loader + System Prompt Builder
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import type { Agent } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
function parseAgentMeta(content: string): { description: string; tools: string[] } {
|
|
10
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
11
|
+
const toolsMatch = content.match(/^tools:\s*\[(.+)\]/m);
|
|
12
|
+
const toolsStr = toolsMatch?.[1] ?? '';
|
|
13
|
+
const tools = toolsStr
|
|
14
|
+
? toolsStr.split(',').map((t) => t.trim().replace(/^["']|["']$/g, ''))
|
|
15
|
+
: ['file:read', 'file:write', 'file:edit', 'file:glob', 'grep', 'bash:execute', 'git:status', 'git:commit', 'git:push', 'git:log'];
|
|
16
|
+
return { description: descMatch?.[1]?.trim() ?? '', tools };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadAgentPrompt(agentName: string, agentDir: string): string | null {
|
|
20
|
+
const candidate = resolve(agentDir, 'agents', agentName + '.md');
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(candidate, 'utf-8');
|
|
23
|
+
const withoutFrontmatter = content.replace(/^---[\s\S]+?---\n/, '');
|
|
24
|
+
return withoutFrontmatter.trim();
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadAllAgents(agentDir: string): Agent[] {
|
|
31
|
+
const agentsDir = resolve(agentDir, 'agents');
|
|
32
|
+
try {
|
|
33
|
+
const files = readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
|
|
34
|
+
const agents: Agent[] = files.map((file) => {
|
|
35
|
+
const agentId = file.replace(/\.md$/, '');
|
|
36
|
+
const content = readFileSync(resolve(agentsDir, file), 'utf-8');
|
|
37
|
+
const meta = parseAgentMeta(content);
|
|
38
|
+
const withoutFrontmatter = content.replace(/^---[\s\S]+?---\n/, '').trim();
|
|
39
|
+
return { name: agentId, description: meta.description, systemPrompt: withoutFrontmatter, tools: meta.tools };
|
|
40
|
+
});
|
|
41
|
+
return agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Agent System Prompt Builder
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
const SYSTEM_BASE = `You are Agent-Termo, a helpful AI assistant in a terminal.
|
|
52
|
+
|
|
53
|
+
RESPONSE RULES (follow these strictly):
|
|
54
|
+
- Be concise and direct. 1-3 sentences for simple questions.
|
|
55
|
+
- Never output verbose chain-of-thought or reasoning — just the answer.
|
|
56
|
+
- For coding tasks: briefly explain what you did, show key code.
|
|
57
|
+
- Never write "null" or placeholders in your response.
|
|
58
|
+
- When using function calls, respond with the function only — no preamble.
|
|
59
|
+
|
|
60
|
+
CONVERSATION MODE (default):
|
|
61
|
+
Just chat. Say things like "I'm doing well!" or "2+2 is 4." — no tools needed.
|
|
62
|
+
|
|
63
|
+
TOOL MODE (when asked to code/build/fix files):
|
|
64
|
+
Use these functions (JSON tool_calls format):
|
|
65
|
+
{"tool_calls":[{"id":"call_1","name":"function_name","arguments":{"key":"value"}}]}
|
|
66
|
+
|
|
67
|
+
Available functions:
|
|
68
|
+
- file:read(path) → read a file
|
|
69
|
+
- file:write(path, content) → overwrite a file completely
|
|
70
|
+
- file:edit(path, old_string, new_string) → edit a specific line section
|
|
71
|
+
- file:glob(pattern) → list files matching a glob (e.g. "src/**/*.ts")
|
|
72
|
+
- grep(pattern, path, glob?) → search for text in files
|
|
73
|
+
- bash:execute(command) → run a shell command
|
|
74
|
+
- git:status() / git:commit(message) / git:push() → version control
|
|
75
|
+
|
|
76
|
+
When done, say "Done: <brief summary>."
|
|
77
|
+
|
|
78
|
+
AVAILABLE FILES:
|
|
79
|
+
- docs/MEMORY.md — project context
|
|
80
|
+
- docs/TIMELINE.md — recent activity log`;
|
|
81
|
+
|
|
82
|
+
export function buildSystemPrompt(memory: string, timeline: string, _agentDir: string): string {
|
|
83
|
+
let prompt = SYSTEM_BASE;
|
|
84
|
+
|
|
85
|
+
if (memory) {
|
|
86
|
+
prompt += '\n\n## Project Memory\n' + memory;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (timeline) {
|
|
90
|
+
prompt += '\n\n## Recent Timeline (last entries)\n' + timeline.slice(-1500);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return prompt;
|
|
94
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// CLI Entry Point — Agent-Termo
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { runRepl } from './main.js';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('agent-termo')
|
|
13
|
+
.description('Terminal AI coding agent with Nvidia NIM APIs')
|
|
14
|
+
.version('1.0.0')
|
|
15
|
+
.argument('[project]', 'Project directory to operate in', process.cwd())
|
|
16
|
+
.option('-k, --api-key <key>', 'Nvidia NIM API key')
|
|
17
|
+
.option('-m, --model <model>', 'Model name to use')
|
|
18
|
+
.option('--debug', 'Enable debug logging')
|
|
19
|
+
.action(async (project: string, options: { apiKey?: string; model?: string; debug?: boolean }) => {
|
|
20
|
+
// Apply CLI overrides to env before main initializes
|
|
21
|
+
if (options.apiKey) process.env.NIM_API_KEY = options.apiKey;
|
|
22
|
+
if (options.model) process.env.NIM_MODEL = options.model;
|
|
23
|
+
if (options.debug) process.env.LOG_LEVEL = 'debug';
|
|
24
|
+
|
|
25
|
+
console.log(`Starting Agent-Termo in: ${project}`);
|
|
26
|
+
await runRepl(project);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('init')
|
|
31
|
+
.description('Initialize Agent-Termo in a project directory')
|
|
32
|
+
.argument('[dir]', 'Directory to initialize', '.')
|
|
33
|
+
.action(async (dir: string) => {
|
|
34
|
+
const { mkdirSync, writeFileSync, existsSync } = await import('node:fs');
|
|
35
|
+
const { resolve } = await import('node:path');
|
|
36
|
+
|
|
37
|
+
const dirsToCreate = [resolve(dir, 'docs'), resolve(dir, '.claude', 'agents'), resolve(dir, 'src')];
|
|
38
|
+
for (const d of dirsToCreate) {
|
|
39
|
+
mkdirSync(d, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create stub docs
|
|
43
|
+
const memoryPath = resolve(dir, 'docs', 'MEMORY.md');
|
|
44
|
+
if (!existsSync(memoryPath)) {
|
|
45
|
+
writeFileSync(memoryPath, '# Project Memory\n\n## Project Context\n(Add project context here)\n', 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const timelinePath = resolve(dir, 'docs', 'TIMELINE.md');
|
|
49
|
+
if (!existsSync(timelinePath)) {
|
|
50
|
+
writeFileSync(
|
|
51
|
+
timelinePath,
|
|
52
|
+
'# Project Timeline\n\n## Initialization\n- [date] Agent-Termo initialized\n',
|
|
53
|
+
'utf-8',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n✓ Initialized Agent-Termo in ${resolve(dir)}`);
|
|
58
|
+
console.log(' Created: docs/MEMORY.md, docs/TIMELINE.md');
|
|
59
|
+
console.log(' Create a .env file with your NIM_API_KEY to get started.');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
// Check for unparsed arguments — if none, run repl mode
|
|
64
|
+
const args = process.argv.slice(2);
|
|
65
|
+
if (args.length === 0) {
|
|
66
|
+
await runRepl(process.cwd());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await program.parseAsync(process.argv);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error(`\nFatal error: ${err instanceof Error ? err.message : err}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Diff Engine — line-level diff computation for file change review
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Uses LCS (Longest Common Subsequence) to compute minimal line-level diffs.
|
|
5
|
+
|
|
6
|
+
import type { DiffLine, DiffResult } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
// ── LCS-based Diff Algorithm ────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export function computeDiff(original: string, proposed: string): DiffResult {
|
|
11
|
+
const originalLines = original.split('\n');
|
|
12
|
+
const proposedLines = proposed.split('\n');
|
|
13
|
+
|
|
14
|
+
if (originalLines.length === 1 && originalLines[0] === '') {
|
|
15
|
+
const lines: DiffLine[] = proposedLines.map((content, i) => ({
|
|
16
|
+
type: 'add',
|
|
17
|
+
lineNumber: i + 1,
|
|
18
|
+
content,
|
|
19
|
+
}));
|
|
20
|
+
return { file: '', lines, additions: proposedLines.length, deletions: 0 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (original === proposed) {
|
|
24
|
+
return { file: '', lines: [], additions: 0, deletions: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const m = originalLines.length;
|
|
28
|
+
const n = proposedLines.length;
|
|
29
|
+
const stride = n + 1;
|
|
30
|
+
const table = new Int32Array((m + 1) * (n + 1));
|
|
31
|
+
|
|
32
|
+
const get = (i: number, j: number): number => table[i * stride + j]!;
|
|
33
|
+
const set = (i: number, j: number, val: number) => { table[i * stride + j] = val; };
|
|
34
|
+
|
|
35
|
+
for (let i = 1; i <= m; i++) {
|
|
36
|
+
for (let j = 1; j <= n; j++) {
|
|
37
|
+
if (originalLines[i - 1] === proposedLines[j - 1]) {
|
|
38
|
+
set(i, j, get(i - 1, j - 1) + 1);
|
|
39
|
+
} else {
|
|
40
|
+
set(i, j, Math.max(get(i - 1, j), get(i, j - 1)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ops = backtrackLCS(get, originalLines, proposedLines);
|
|
46
|
+
const diffLines = opsToDiffLines(ops);
|
|
47
|
+
|
|
48
|
+
let additions = 0;
|
|
49
|
+
let deletions = 0;
|
|
50
|
+
for (const line of diffLines) {
|
|
51
|
+
if (line.type === 'add') additions++;
|
|
52
|
+
if (line.type === 'remove') deletions++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { file: '', lines: diffLines, additions, deletions };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface DiffOp {
|
|
59
|
+
type: 'same' | 'add' | 'remove';
|
|
60
|
+
originalIndex: number;
|
|
61
|
+
proposedIndex: number;
|
|
62
|
+
originalLine?: string;
|
|
63
|
+
proposedLine?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function backtrackLCS(
|
|
67
|
+
get: (i: number, j: number) => number,
|
|
68
|
+
a: string[],
|
|
69
|
+
b: string[],
|
|
70
|
+
): DiffOp[] {
|
|
71
|
+
const ops: DiffOp[] = [];
|
|
72
|
+
const m = a.length;
|
|
73
|
+
const n = b.length;
|
|
74
|
+
let i = m;
|
|
75
|
+
let j = n;
|
|
76
|
+
|
|
77
|
+
while (i > 0 || j > 0) {
|
|
78
|
+
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
|
79
|
+
ops.unshift({
|
|
80
|
+
type: 'same',
|
|
81
|
+
originalIndex: i,
|
|
82
|
+
proposedIndex: j,
|
|
83
|
+
originalLine: a[i - 1],
|
|
84
|
+
proposedLine: b[j - 1],
|
|
85
|
+
});
|
|
86
|
+
i--;
|
|
87
|
+
j--;
|
|
88
|
+
} else if (j > 0 && (i === 0 || get(i, j - 1) >= get(i - 1, j))) {
|
|
89
|
+
ops.unshift({
|
|
90
|
+
type: 'add',
|
|
91
|
+
proposedIndex: j,
|
|
92
|
+
originalIndex: i,
|
|
93
|
+
proposedLine: b[j - 1],
|
|
94
|
+
});
|
|
95
|
+
j--;
|
|
96
|
+
} else {
|
|
97
|
+
ops.unshift({
|
|
98
|
+
type: 'remove',
|
|
99
|
+
originalIndex: i,
|
|
100
|
+
proposedIndex: j,
|
|
101
|
+
originalLine: a[i - 1],
|
|
102
|
+
});
|
|
103
|
+
i--;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return ops;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function opsToDiffLines(ops: DiffOp[]): DiffLine[] {
|
|
111
|
+
const lines: DiffLine[] = [];
|
|
112
|
+
let originalLineNum = 0;
|
|
113
|
+
let proposedLineNum = 0;
|
|
114
|
+
|
|
115
|
+
for (const op of ops) {
|
|
116
|
+
switch (op.type) {
|
|
117
|
+
case 'same': {
|
|
118
|
+
originalLineNum++;
|
|
119
|
+
proposedLineNum++;
|
|
120
|
+
lines.push({
|
|
121
|
+
type: 'context',
|
|
122
|
+
lineNumber: originalLineNum,
|
|
123
|
+
content: op.originalLine ?? '',
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'remove': {
|
|
128
|
+
originalLineNum++;
|
|
129
|
+
lines.push({
|
|
130
|
+
type: 'remove',
|
|
131
|
+
lineNumber: originalLineNum,
|
|
132
|
+
content: op.originalLine ?? '',
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'add': {
|
|
137
|
+
proposedLineNum++;
|
|
138
|
+
lines.push({
|
|
139
|
+
type: 'add',
|
|
140
|
+
lineNumber: proposedLineNum,
|
|
141
|
+
content: op.proposedLine ?? '',
|
|
142
|
+
});
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Context Extraction for large files ──────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export interface DiffOptions {
|
|
154
|
+
contextLines?: number;
|
|
155
|
+
maxLines?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function applyDiffContext(diff: DiffResult, options: DiffOptions = {}): DiffResult {
|
|
159
|
+
const { contextLines = 3, maxLines = 0 } = options;
|
|
160
|
+
|
|
161
|
+
if (contextLines <= 0 && maxLines <= 0) {
|
|
162
|
+
return diff;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Identify contiguous change regions
|
|
166
|
+
const changeRegions: { start: number; end: number }[] = [];
|
|
167
|
+
let inChange = false;
|
|
168
|
+
let regionStart = 0;
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < diff.lines.length; i++) {
|
|
171
|
+
const line = diff.lines[i]!;
|
|
172
|
+
if (line.type === 'add' || line.type === 'remove') {
|
|
173
|
+
if (!inChange) {
|
|
174
|
+
regionStart = i;
|
|
175
|
+
inChange = true;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
if (inChange) {
|
|
179
|
+
changeRegions.push({ start: regionStart, end: i - 1 });
|
|
180
|
+
inChange = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (inChange) {
|
|
185
|
+
changeRegions.push({ start: regionStart, end: diff.lines.length - 1 });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build merged ranges — use parallel arrays to avoid tuple strict-null issues
|
|
189
|
+
// All accesses use ! because arrays are never sparse at these indices
|
|
190
|
+
const mergeStarts: number[] = [];
|
|
191
|
+
const mergeEnds: number[] = [];
|
|
192
|
+
|
|
193
|
+
for (const region of changeRegions) {
|
|
194
|
+
const start = Math.max(0, region.start - contextLines);
|
|
195
|
+
const end = Math.min(diff.lines.length - 1, region.end + contextLines);
|
|
196
|
+
|
|
197
|
+
const lastIdx = mergeStarts.length - 1;
|
|
198
|
+
if (mergeStarts.length > 0 && start <= mergeEnds[lastIdx]! + 1) {
|
|
199
|
+
mergeEnds[lastIdx] = Math.max(mergeEnds[lastIdx]!, end);
|
|
200
|
+
} else {
|
|
201
|
+
mergeStarts.push(start);
|
|
202
|
+
mergeEnds.push(end);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build filtered diff lines with ellipsis between non-adjacent ranges
|
|
207
|
+
const filteredLines: DiffLine[] = [];
|
|
208
|
+
|
|
209
|
+
for (let r = 0; r < mergeStarts.length; r++) {
|
|
210
|
+
const rangeStart = mergeStarts[r]!;
|
|
211
|
+
const rangeEnd = mergeEnds[r]!;
|
|
212
|
+
const prevEnd = r > 0 ? mergeEnds[r - 1]! : -1;
|
|
213
|
+
|
|
214
|
+
if (r > 0 && rangeStart > prevEnd + 1) {
|
|
215
|
+
const omitted = rangeStart - prevEnd - 1;
|
|
216
|
+
filteredLines.push({
|
|
217
|
+
type: 'context',
|
|
218
|
+
lineNumber: 0,
|
|
219
|
+
content: '... ' + omitted + ' unchanged lines omitted ...',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
224
|
+
const line = diff.lines[i]!;
|
|
225
|
+
filteredLines.push(line);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let finalLines = filteredLines;
|
|
230
|
+
if (maxLines > 0 && finalLines.length > maxLines) {
|
|
231
|
+
const halfMax = Math.floor(maxLines / 2);
|
|
232
|
+
finalLines = [
|
|
233
|
+
...finalLines.slice(0, halfMax),
|
|
234
|
+
{
|
|
235
|
+
type: 'context',
|
|
236
|
+
lineNumber: 0,
|
|
237
|
+
content: '... ' + (finalLines.length - maxLines) + ' more lines ...',
|
|
238
|
+
},
|
|
239
|
+
...finalLines.slice(finalLines.length - halfMax),
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let additions = 0;
|
|
244
|
+
let deletions = 0;
|
|
245
|
+
for (const line of finalLines) {
|
|
246
|
+
if (line.type === 'add') additions++;
|
|
247
|
+
if (line.type === 'remove') deletions++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { ...diff, lines: finalLines, additions, deletions };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function isNoOpDiff(diff: DiffResult): boolean {
|
|
254
|
+
return diff.lines.length === 0 && diff.additions === 0 && diff.deletions === 0;
|
|
255
|
+
}
|