agdi 2.2.3 → 2.4.1
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/README.md +321 -93
- package/dist/index.js +1341 -75
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
11
|
+
import chalk13 from "chalk";
|
|
12
|
+
import ora5 from "ora";
|
|
7
13
|
|
|
8
14
|
// src/core/llm/index.ts
|
|
9
15
|
var PuterProvider = class {
|
|
@@ -46,6 +52,38 @@ var PuterProvider = class {
|
|
|
46
52
|
usage: typeof data !== "string" ? data.usage : void 0
|
|
47
53
|
};
|
|
48
54
|
}
|
|
55
|
+
async chat(messages) {
|
|
56
|
+
const response = await fetch("https://api.puter.com/ai/chat", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json"
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
model: this.model,
|
|
63
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content }))
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Puter API error: ${response.status} ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
let text = "";
|
|
71
|
+
if (typeof data === "string") {
|
|
72
|
+
text = data;
|
|
73
|
+
} else if (data.message?.content) {
|
|
74
|
+
if (Array.isArray(data.message.content)) {
|
|
75
|
+
text = data.message.content.map((c) => c.text || "").join("");
|
|
76
|
+
} else {
|
|
77
|
+
text = data.message.content;
|
|
78
|
+
}
|
|
79
|
+
} else if (data.choices?.[0]?.message?.content) {
|
|
80
|
+
text = data.choices[0].message.content;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
usage: typeof data !== "string" ? data.usage : void 0
|
|
85
|
+
};
|
|
86
|
+
}
|
|
49
87
|
};
|
|
50
88
|
var GeminiProvider = class {
|
|
51
89
|
config;
|
|
@@ -71,6 +109,28 @@ var GeminiProvider = class {
|
|
|
71
109
|
usage: void 0
|
|
72
110
|
};
|
|
73
111
|
}
|
|
112
|
+
async chat(messages) {
|
|
113
|
+
const { GoogleGenAI } = await import("@google/genai");
|
|
114
|
+
const ai = new GoogleGenAI({ apiKey: this.config.apiKey });
|
|
115
|
+
const contents = messages.map((m) => {
|
|
116
|
+
if (m.role === "system") {
|
|
117
|
+
return { role: "user", parts: [{ text: m.content }] };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
121
|
+
parts: [{ text: m.content }]
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const response = await ai.models.generateContent({
|
|
125
|
+
model: this.config.model || "gemini-2.5-flash",
|
|
126
|
+
contents
|
|
127
|
+
});
|
|
128
|
+
const text = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
129
|
+
return {
|
|
130
|
+
text,
|
|
131
|
+
usage: void 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
74
134
|
};
|
|
75
135
|
var OpenRouterProvider = class {
|
|
76
136
|
config;
|
|
@@ -107,6 +167,33 @@ var OpenRouterProvider = class {
|
|
|
107
167
|
} : void 0
|
|
108
168
|
};
|
|
109
169
|
}
|
|
170
|
+
async chat(messages) {
|
|
171
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
176
|
+
"HTTP-Referer": "https://agdi.dev",
|
|
177
|
+
"X-Title": "Agdi CLI"
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
model: this.config.model || "anthropic/claude-3.5-sonnet",
|
|
181
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content }))
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const error = await response.text();
|
|
186
|
+
throw new Error(`OpenRouter API error: ${response.status} - ${error}`);
|
|
187
|
+
}
|
|
188
|
+
const data = await response.json();
|
|
189
|
+
return {
|
|
190
|
+
text: data.choices?.[0]?.message?.content || "",
|
|
191
|
+
usage: data.usage ? {
|
|
192
|
+
inputTokens: data.usage.prompt_tokens,
|
|
193
|
+
outputTokens: data.usage.completion_tokens
|
|
194
|
+
} : void 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
110
197
|
};
|
|
111
198
|
function createLLMProvider(provider, config) {
|
|
112
199
|
switch (provider) {
|
|
@@ -364,6 +451,18 @@ var MALICIOUS_PATTERNS = [
|
|
|
364
451
|
description: "Private key detected",
|
|
365
452
|
severity: "critical"
|
|
366
453
|
},
|
|
454
|
+
{
|
|
455
|
+
pattern: /sk-ant-[A-Za-z0-9-_]{95,}/g,
|
|
456
|
+
category: "secret",
|
|
457
|
+
description: "Anthropic API key detected",
|
|
458
|
+
severity: "critical"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
pattern: /sk-ant-api[A-Za-z0-9-_]{90,}/g,
|
|
462
|
+
category: "secret",
|
|
463
|
+
description: "Anthropic API key (alt format) detected",
|
|
464
|
+
severity: "critical"
|
|
465
|
+
},
|
|
367
466
|
// ==================== DANGEROUS CODE PATTERNS ====================
|
|
368
467
|
{
|
|
369
468
|
pattern: /\beval\s*\(/g,
|
|
@@ -475,6 +574,50 @@ var MALICIOUS_PATTERNS = [
|
|
|
475
574
|
category: "dangerous",
|
|
476
575
|
description: "Template literal with shell commands",
|
|
477
576
|
severity: "medium"
|
|
577
|
+
},
|
|
578
|
+
// ==================== PROMPT INJECTION DEFENSE ====================
|
|
579
|
+
{
|
|
580
|
+
pattern: /ignore\s+(all\s+)?(previous|above|prior)\s+(instructions?|prompts?|rules?)/gi,
|
|
581
|
+
category: "suspicious",
|
|
582
|
+
description: "Prompt injection: instruction override attempt",
|
|
583
|
+
severity: "critical"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
pattern: /\[SYSTEM\]|\[INST\]|<\|im_start\|>|<\|endoftext\|>/gi,
|
|
587
|
+
category: "suspicious",
|
|
588
|
+
description: "Prompt injection: model control tokens",
|
|
589
|
+
severity: "critical"
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
pattern: /you\s+are\s+(now\s+)?(a|an|the)\s+\w+\s+(that|who|which)/gi,
|
|
593
|
+
category: "suspicious",
|
|
594
|
+
description: "Prompt injection: role override attempt",
|
|
595
|
+
severity: "high"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
pattern: /disregard\s+(all\s+)?safety|bypass\s+(all\s+)?security|disable\s+(all\s+)?restrictions/gi,
|
|
599
|
+
category: "suspicious",
|
|
600
|
+
description: "Prompt injection: safety bypass attempt",
|
|
601
|
+
severity: "critical"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
pattern: /\bexec\s*\(\s*[`'"].*?\$\(.*?\)/g,
|
|
605
|
+
category: "dangerous",
|
|
606
|
+
description: "Command injection via exec with substitution",
|
|
607
|
+
severity: "critical"
|
|
608
|
+
},
|
|
609
|
+
// ==================== NETWORK EXFILTRATION ====================
|
|
610
|
+
{
|
|
611
|
+
pattern: /\bfetch\s*\(\s*['"`]https?:\/\/[^'"`]*\.(ru|cn|tk|ml|ga)\//gi,
|
|
612
|
+
category: "suspicious",
|
|
613
|
+
description: "Request to suspicious TLD",
|
|
614
|
+
severity: "high"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
pattern: /\bnew\s+WebSocket\s*\(\s*['"`]wss?:\/\/(?!localhost|127\.0\.0\.1)/g,
|
|
618
|
+
category: "suspicious",
|
|
619
|
+
description: "WebSocket to external server",
|
|
620
|
+
severity: "medium"
|
|
478
621
|
}
|
|
479
622
|
];
|
|
480
623
|
function scanCode(code, filename) {
|
|
@@ -1270,10 +1413,10 @@ async function selectModel() {
|
|
|
1270
1413
|
`));
|
|
1271
1414
|
}
|
|
1272
1415
|
|
|
1273
|
-
// src/commands/
|
|
1274
|
-
import { input as
|
|
1275
|
-
import
|
|
1276
|
-
import
|
|
1416
|
+
// src/commands/agdi-dev.ts
|
|
1417
|
+
import { input as input5, confirm as confirm3 } from "@inquirer/prompts";
|
|
1418
|
+
import chalk12 from "chalk";
|
|
1419
|
+
import ora4 from "ora";
|
|
1277
1420
|
|
|
1278
1421
|
// src/actions/plan-executor.ts
|
|
1279
1422
|
import { select as select4, confirm } from "@inquirer/prompts";
|
|
@@ -1331,6 +1474,40 @@ function summarizePlan(plan) {
|
|
|
1331
1474
|
riskTier: maxRiskTier
|
|
1332
1475
|
};
|
|
1333
1476
|
}
|
|
1477
|
+
function validateAction(action) {
|
|
1478
|
+
if (!action || typeof action !== "object") return null;
|
|
1479
|
+
const obj = action;
|
|
1480
|
+
const type = obj.type;
|
|
1481
|
+
if (type === "mkdir") {
|
|
1482
|
+
if (typeof obj.path !== "string" || !obj.path) return null;
|
|
1483
|
+
if (obj.path.includes("..") || obj.path.startsWith("/") || /^[A-Z]:/i.test(obj.path)) return null;
|
|
1484
|
+
return { type: "mkdir", path: obj.path };
|
|
1485
|
+
}
|
|
1486
|
+
if (type === "writeFile") {
|
|
1487
|
+
if (typeof obj.path !== "string" || !obj.path) return null;
|
|
1488
|
+
if (typeof obj.content !== "string") return null;
|
|
1489
|
+
if (obj.path.includes("..") || obj.path.startsWith("/") || /^[A-Z]:/i.test(obj.path)) return null;
|
|
1490
|
+
return { type: "writeFile", path: obj.path, content: obj.content };
|
|
1491
|
+
}
|
|
1492
|
+
if (type === "deleteFile") {
|
|
1493
|
+
if (typeof obj.path !== "string" || !obj.path) return null;
|
|
1494
|
+
if (obj.path.includes("..") || obj.path.startsWith("/") || /^[A-Z]:/i.test(obj.path)) return null;
|
|
1495
|
+
return { type: "deleteFile", path: obj.path };
|
|
1496
|
+
}
|
|
1497
|
+
if (type === "exec") {
|
|
1498
|
+
if (!Array.isArray(obj.argv) || obj.argv.length === 0) return null;
|
|
1499
|
+
if (!obj.argv.every((a) => typeof a === "string")) return null;
|
|
1500
|
+
const dangerous = ["sudo", "su", "rm -rf /", "format", "mkfs", "dd", ":(){"];
|
|
1501
|
+
const cmdStr = obj.argv.join(" ").toLowerCase();
|
|
1502
|
+
if (dangerous.some((d) => cmdStr.includes(d))) return null;
|
|
1503
|
+
return {
|
|
1504
|
+
type: "exec",
|
|
1505
|
+
argv: obj.argv,
|
|
1506
|
+
cwd: typeof obj.cwd === "string" ? obj.cwd : void 0
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1334
1511
|
function parseActionPlan(response) {
|
|
1335
1512
|
try {
|
|
1336
1513
|
const jsonMatch = response.match(/\{[\s\S]*"actions"[\s\S]*\}/);
|
|
@@ -1341,10 +1518,23 @@ function parseActionPlan(response) {
|
|
|
1341
1518
|
if (!parsed.actions || !Array.isArray(parsed.actions)) {
|
|
1342
1519
|
return null;
|
|
1343
1520
|
}
|
|
1521
|
+
const validatedActions = [];
|
|
1522
|
+
for (const action of parsed.actions) {
|
|
1523
|
+
const validated = validateAction(action);
|
|
1524
|
+
if (!validated) {
|
|
1525
|
+
console.warn(`Invalid action rejected: ${JSON.stringify(action).slice(0, 100)}`);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
validatedActions.push(validated);
|
|
1529
|
+
}
|
|
1530
|
+
if (validatedActions.length === 0) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
const projectName = typeof parsed.projectName === "string" ? parsed.projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 50) : "generated-app";
|
|
1344
1534
|
return {
|
|
1345
|
-
projectName
|
|
1346
|
-
actions:
|
|
1347
|
-
nextSteps: parsed.nextSteps
|
|
1535
|
+
projectName,
|
|
1536
|
+
actions: validatedActions,
|
|
1537
|
+
nextSteps: typeof parsed.nextSteps === "string" ? parsed.nextSteps : void 0
|
|
1348
1538
|
};
|
|
1349
1539
|
} catch {
|
|
1350
1540
|
return null;
|
|
@@ -1661,6 +1851,68 @@ async function deleteFileTool(path4) {
|
|
|
1661
1851
|
return { success: false, error: msg };
|
|
1662
1852
|
}
|
|
1663
1853
|
}
|
|
1854
|
+
async function applyPatchTool(path4, unifiedDiff) {
|
|
1855
|
+
const validation = validatePath(path4);
|
|
1856
|
+
if (!validation.valid) {
|
|
1857
|
+
return { success: false, error: validation.error };
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
const current = existsSync3(validation.resolved) ? await readFile(validation.resolved, "utf-8") : "";
|
|
1861
|
+
const patched = applySimplePatch(current, unifiedDiff);
|
|
1862
|
+
if (patched === null) {
|
|
1863
|
+
return { success: false, error: "Failed to apply patch" };
|
|
1864
|
+
}
|
|
1865
|
+
await writeFile(validation.resolved, patched, "utf-8");
|
|
1866
|
+
logEvent({
|
|
1867
|
+
eventType: "command_result",
|
|
1868
|
+
command: `applyPatch ${path4}`,
|
|
1869
|
+
result: { exitCode: 0 },
|
|
1870
|
+
metadata: {
|
|
1871
|
+
tool: "applyPatchTool",
|
|
1872
|
+
path: validation.resolved,
|
|
1873
|
+
patchLength: unifiedDiff.length
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
return { success: true };
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1879
|
+
return { success: false, error: msg };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function applySimplePatch(content, diff) {
|
|
1883
|
+
const lines = content.split("\n");
|
|
1884
|
+
const diffLines = diff.split("\n");
|
|
1885
|
+
let lineIndex = 0;
|
|
1886
|
+
const result = [];
|
|
1887
|
+
for (const diffLine of diffLines) {
|
|
1888
|
+
if (diffLine.startsWith("---") || diffLine.startsWith("+++") || diffLine.startsWith("@@")) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (diffLine.startsWith("-")) {
|
|
1892
|
+
lineIndex++;
|
|
1893
|
+
} else if (diffLine.startsWith("+")) {
|
|
1894
|
+
result.push(diffLine.slice(1));
|
|
1895
|
+
} else if (diffLine.startsWith(" ") || diffLine === "") {
|
|
1896
|
+
if (lineIndex < lines.length) {
|
|
1897
|
+
result.push(lines[lineIndex]);
|
|
1898
|
+
lineIndex++;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
while (lineIndex < lines.length) {
|
|
1903
|
+
result.push(lines[lineIndex]);
|
|
1904
|
+
lineIndex++;
|
|
1905
|
+
}
|
|
1906
|
+
return result.join("\n");
|
|
1907
|
+
}
|
|
1908
|
+
function resolvePath(path4) {
|
|
1909
|
+
const env = getEnvironment();
|
|
1910
|
+
return resolve(env.workspaceRoot, path4);
|
|
1911
|
+
}
|
|
1912
|
+
function fileExists(path4) {
|
|
1913
|
+
const resolved = resolvePath(path4);
|
|
1914
|
+
return existsSync3(resolved);
|
|
1915
|
+
}
|
|
1664
1916
|
|
|
1665
1917
|
// src/security/permission-gate.ts
|
|
1666
1918
|
import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2 } from "path";
|
|
@@ -2455,6 +2707,11 @@ function saveTrustConfig(config) {
|
|
|
2455
2707
|
function normalizePath(path4) {
|
|
2456
2708
|
return resolve3(path4).toLowerCase().replace(/\\/g, "/");
|
|
2457
2709
|
}
|
|
2710
|
+
function isWorkspaceTrusted(workspacePath) {
|
|
2711
|
+
const config = loadTrustConfig();
|
|
2712
|
+
const normalized = normalizePath(workspacePath);
|
|
2713
|
+
return config.trustedWorkspaces.some((w) => w.normalizedPath === normalized);
|
|
2714
|
+
}
|
|
2458
2715
|
function trustWorkspace(workspacePath) {
|
|
2459
2716
|
const config = loadTrustConfig();
|
|
2460
2717
|
const normalized = normalizePath(workspacePath);
|
|
@@ -2468,6 +2725,63 @@ function trustWorkspace(workspacePath) {
|
|
|
2468
2725
|
});
|
|
2469
2726
|
saveTrustConfig(config);
|
|
2470
2727
|
}
|
|
2728
|
+
async function promptWorkspaceTrust(workspacePath) {
|
|
2729
|
+
if (isWorkspaceTrusted(workspacePath)) {
|
|
2730
|
+
console.log(chalk9.green("\u2713 Workspace is trusted\n"));
|
|
2731
|
+
return "persistent";
|
|
2732
|
+
}
|
|
2733
|
+
console.log(chalk9.yellow("\n\u26A0\uFE0F Untrusted Workspace"));
|
|
2734
|
+
console.log(chalk9.gray(` ${workspacePath}
|
|
2735
|
+
`));
|
|
2736
|
+
console.log(chalk9.gray("Agdi can run commands in this workspace."));
|
|
2737
|
+
console.log(chalk9.gray("Do you trust the contents of this folder?\n"));
|
|
2738
|
+
const choice = await select3({
|
|
2739
|
+
message: "Trust this workspace?",
|
|
2740
|
+
choices: [
|
|
2741
|
+
{
|
|
2742
|
+
name: "Trust for this session only",
|
|
2743
|
+
value: "session",
|
|
2744
|
+
description: "Allow commands for this session, ask again next time"
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
name: "Trust and remember",
|
|
2748
|
+
value: "persistent",
|
|
2749
|
+
description: "Always trust this workspace"
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: "Exit (don't trust)",
|
|
2753
|
+
value: "exit",
|
|
2754
|
+
description: "Exit without granting trust"
|
|
2755
|
+
}
|
|
2756
|
+
]
|
|
2757
|
+
});
|
|
2758
|
+
return choice;
|
|
2759
|
+
}
|
|
2760
|
+
async function handleTrustFlow(workspacePath) {
|
|
2761
|
+
const choice = await promptWorkspaceTrust(workspacePath);
|
|
2762
|
+
switch (choice) {
|
|
2763
|
+
case "session":
|
|
2764
|
+
updateEnvironment({ trustLevel: "session" });
|
|
2765
|
+
console.log(chalk9.green("\u2713 Trusted for this session\n"));
|
|
2766
|
+
return "session";
|
|
2767
|
+
case "persistent":
|
|
2768
|
+
trustWorkspace(workspacePath);
|
|
2769
|
+
updateEnvironment({ trustLevel: "persistent" });
|
|
2770
|
+
console.log(chalk9.green("\u2713 Workspace trusted and remembered\n"));
|
|
2771
|
+
return "persistent";
|
|
2772
|
+
case "exit":
|
|
2773
|
+
console.log(chalk9.yellow("\n\u{1F44B} Exiting. Workspace not trusted.\n"));
|
|
2774
|
+
return null;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
async function ensureTrusted(workspacePath) {
|
|
2778
|
+
if (isWorkspaceTrusted(workspacePath)) {
|
|
2779
|
+
updateEnvironment({ trustLevel: "persistent" });
|
|
2780
|
+
return true;
|
|
2781
|
+
}
|
|
2782
|
+
const result = await handleTrustFlow(workspacePath);
|
|
2783
|
+
return result !== null;
|
|
2784
|
+
}
|
|
2471
2785
|
|
|
2472
2786
|
// src/actions/plan-executor.ts
|
|
2473
2787
|
function displayPlanSummary(plan) {
|
|
@@ -2733,14 +3047,775 @@ async function parseAndExecutePlan(response) {
|
|
|
2733
3047
|
return executePlan(plan);
|
|
2734
3048
|
}
|
|
2735
3049
|
|
|
2736
|
-
// src/
|
|
2737
|
-
|
|
3050
|
+
// src/core/context-manager.ts
|
|
3051
|
+
import { readdirSync, readFileSync as readFileSync5, statSync, existsSync as existsSync6 } from "fs";
|
|
3052
|
+
import { join as join4, basename } from "path";
|
|
3053
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
3054
|
+
"node_modules",
|
|
3055
|
+
".git",
|
|
3056
|
+
"dist",
|
|
3057
|
+
"build",
|
|
3058
|
+
".next",
|
|
3059
|
+
".nuxt",
|
|
3060
|
+
".cache",
|
|
3061
|
+
"coverage",
|
|
3062
|
+
"__pycache__",
|
|
3063
|
+
".venv",
|
|
3064
|
+
"venv",
|
|
3065
|
+
".idea",
|
|
3066
|
+
".vscode",
|
|
3067
|
+
".turbo"
|
|
3068
|
+
]);
|
|
3069
|
+
var IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
3070
|
+
".DS_Store",
|
|
3071
|
+
"Thumbs.db",
|
|
3072
|
+
".env",
|
|
3073
|
+
".env.local",
|
|
3074
|
+
"package-lock.json",
|
|
3075
|
+
"yarn.lock",
|
|
3076
|
+
"pnpm-lock.yaml"
|
|
3077
|
+
]);
|
|
3078
|
+
var MAX_TREE_DEPTH = 4;
|
|
3079
|
+
var MAX_FILES_PER_DIR = 20;
|
|
3080
|
+
function getWorkspaceTree(rootDir, maxDepth = MAX_TREE_DEPTH) {
|
|
3081
|
+
const env = getEnvironment();
|
|
3082
|
+
const root = rootDir || env.workspaceRoot;
|
|
3083
|
+
if (!existsSync6(root)) {
|
|
3084
|
+
return "(workspace not found)";
|
|
3085
|
+
}
|
|
3086
|
+
const lines = [];
|
|
3087
|
+
lines.push(basename(root) + "/");
|
|
3088
|
+
buildTree(root, "", lines, 0, maxDepth);
|
|
3089
|
+
return lines.join("\n");
|
|
3090
|
+
}
|
|
3091
|
+
function buildTree(dir, prefix, lines, depth, maxDepth) {
|
|
3092
|
+
if (depth >= maxDepth) {
|
|
3093
|
+
lines.push(prefix + "\u2514\u2500\u2500 ...");
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
let entries;
|
|
3097
|
+
try {
|
|
3098
|
+
entries = readdirSync(dir);
|
|
3099
|
+
} catch {
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
const filtered = entries.filter((e) => {
|
|
3103
|
+
if (IGNORED_DIRS.has(e) || IGNORED_FILES.has(e)) return false;
|
|
3104
|
+
if (e.startsWith(".") && e !== ".github") return false;
|
|
3105
|
+
return true;
|
|
3106
|
+
});
|
|
3107
|
+
const dirs = [];
|
|
3108
|
+
const files = [];
|
|
3109
|
+
for (const entry of filtered) {
|
|
3110
|
+
const fullPath = join4(dir, entry);
|
|
3111
|
+
try {
|
|
3112
|
+
const stat = statSync(fullPath);
|
|
3113
|
+
if (stat.isDirectory()) {
|
|
3114
|
+
dirs.push(entry);
|
|
3115
|
+
} else {
|
|
3116
|
+
files.push(entry);
|
|
3117
|
+
}
|
|
3118
|
+
} catch {
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
dirs.sort();
|
|
3122
|
+
files.sort();
|
|
3123
|
+
const allEntries = [...dirs.map((d) => ({ name: d, isDir: true })), ...files.map((f) => ({ name: f, isDir: false }))];
|
|
3124
|
+
const truncated = allEntries.length > MAX_FILES_PER_DIR;
|
|
3125
|
+
const toShow = truncated ? allEntries.slice(0, MAX_FILES_PER_DIR) : allEntries;
|
|
3126
|
+
toShow.forEach((entry, index) => {
|
|
3127
|
+
const isLast = index === toShow.length - 1 && !truncated;
|
|
3128
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
3129
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
3130
|
+
if (entry.isDir) {
|
|
3131
|
+
lines.push(prefix + connector + entry.name + "/");
|
|
3132
|
+
buildTree(join4(dir, entry.name), prefix + childPrefix, lines, depth + 1, maxDepth);
|
|
3133
|
+
} else {
|
|
3134
|
+
lines.push(prefix + connector + entry.name);
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
if (truncated) {
|
|
3138
|
+
lines.push(prefix + "\u2514\u2500\u2500 ... (" + (allEntries.length - MAX_FILES_PER_DIR) + " more)");
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
function getProjectConfig(rootDir) {
|
|
3142
|
+
const env = getEnvironment();
|
|
3143
|
+
const root = rootDir || env.workspaceRoot;
|
|
3144
|
+
const packageJsonPath = join4(root, "package.json");
|
|
3145
|
+
if (existsSync6(packageJsonPath)) {
|
|
3146
|
+
try {
|
|
3147
|
+
const content = readFileSync5(packageJsonPath, "utf-8");
|
|
3148
|
+
const pkg = JSON.parse(content);
|
|
3149
|
+
return {
|
|
3150
|
+
name: pkg.name || basename(root),
|
|
3151
|
+
version: pkg.version,
|
|
3152
|
+
description: pkg.description,
|
|
3153
|
+
dependencies: pkg.dependencies,
|
|
3154
|
+
devDependencies: pkg.devDependencies,
|
|
3155
|
+
scripts: pkg.scripts
|
|
3156
|
+
};
|
|
3157
|
+
} catch {
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
function buildWorkspaceContext(rootDir) {
|
|
3164
|
+
const env = getEnvironment();
|
|
3165
|
+
const root = rootDir || env.workspaceRoot;
|
|
3166
|
+
return {
|
|
3167
|
+
workspaceRoot: root,
|
|
3168
|
+
fileTree: getWorkspaceTree(root),
|
|
3169
|
+
projectConfig: getProjectConfig(root),
|
|
3170
|
+
gitStatus: null
|
|
3171
|
+
// Filled by GitManager
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
function formatContextForPrompt(context) {
|
|
3175
|
+
const sections = [];
|
|
3176
|
+
sections.push("## Current Workspace");
|
|
3177
|
+
sections.push(`Path: ${context.workspaceRoot}`);
|
|
3178
|
+
if (context.projectConfig) {
|
|
3179
|
+
const cfg = context.projectConfig;
|
|
3180
|
+
sections.push(`
|
|
3181
|
+
Project: ${cfg.name}${cfg.version ? " v" + cfg.version : ""}`);
|
|
3182
|
+
if (cfg.description) {
|
|
3183
|
+
sections.push(`Description: ${cfg.description}`);
|
|
3184
|
+
}
|
|
3185
|
+
if (cfg.scripts) {
|
|
3186
|
+
const scriptList = Object.keys(cfg.scripts).slice(0, 5).join(", ");
|
|
3187
|
+
sections.push(`Scripts: ${scriptList}`);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
sections.push("\n## File Structure");
|
|
3191
|
+
sections.push("```");
|
|
3192
|
+
sections.push(context.fileTree);
|
|
3193
|
+
sections.push("```");
|
|
3194
|
+
if (context.gitStatus) {
|
|
3195
|
+
sections.push("\n## Git Status");
|
|
3196
|
+
sections.push(context.gitStatus);
|
|
3197
|
+
}
|
|
3198
|
+
return sections.join("\n");
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// src/core/git-manager.ts
|
|
3202
|
+
import { spawnSync } from "child_process";
|
|
3203
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3204
|
+
import { join as join5 } from "path";
|
|
3205
|
+
function execGit(args, cwd) {
|
|
3206
|
+
const env = getEnvironment();
|
|
3207
|
+
const workDir = cwd || env.workspaceRoot;
|
|
3208
|
+
try {
|
|
3209
|
+
const result = spawnSync("git", args, {
|
|
3210
|
+
cwd: workDir,
|
|
3211
|
+
encoding: "utf-8",
|
|
3212
|
+
timeout: 1e4
|
|
3213
|
+
});
|
|
3214
|
+
if (result.error) {
|
|
3215
|
+
throw result.error;
|
|
3216
|
+
}
|
|
3217
|
+
return result.stdout?.trim() || "";
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
return "";
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
function isGitRepo(dir) {
|
|
3223
|
+
const env = getEnvironment();
|
|
3224
|
+
const checkDir = dir || env.workspaceRoot;
|
|
3225
|
+
return existsSync7(join5(checkDir, ".git"));
|
|
3226
|
+
}
|
|
3227
|
+
function getStatus(cwd) {
|
|
3228
|
+
if (!isGitRepo(cwd)) {
|
|
3229
|
+
return {
|
|
3230
|
+
isGitRepo: false,
|
|
3231
|
+
branch: "",
|
|
3232
|
+
ahead: 0,
|
|
3233
|
+
behind: 0,
|
|
3234
|
+
staged: [],
|
|
3235
|
+
unstaged: [],
|
|
3236
|
+
untracked: [],
|
|
3237
|
+
hasConflicts: false
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
const branch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd) || "main";
|
|
3241
|
+
let ahead = 0;
|
|
3242
|
+
let behind = 0;
|
|
3243
|
+
const trackingInfo = execGit(["rev-list", "--left-right", "--count", `origin/${branch}...HEAD`], cwd);
|
|
3244
|
+
if (trackingInfo) {
|
|
3245
|
+
const [behindStr, aheadStr] = trackingInfo.split(/\s+/);
|
|
3246
|
+
behind = parseInt(behindStr, 10) || 0;
|
|
3247
|
+
ahead = parseInt(aheadStr, 10) || 0;
|
|
3248
|
+
}
|
|
3249
|
+
const statusOutput = execGit(["status", "--porcelain=v1"], cwd);
|
|
3250
|
+
const staged = [];
|
|
3251
|
+
const unstaged = [];
|
|
3252
|
+
const untracked = [];
|
|
3253
|
+
let hasConflicts = false;
|
|
3254
|
+
for (const line of statusOutput.split("\n")) {
|
|
3255
|
+
if (!line) continue;
|
|
3256
|
+
const indexStatus = line[0];
|
|
3257
|
+
const workTreeStatus = line[1];
|
|
3258
|
+
const filePath = line.slice(3).trim();
|
|
3259
|
+
if (indexStatus === "U" || workTreeStatus === "U" || indexStatus === "A" && workTreeStatus === "A" || indexStatus === "D" && workTreeStatus === "D") {
|
|
3260
|
+
hasConflicts = true;
|
|
3261
|
+
}
|
|
3262
|
+
if (indexStatus === "?" && workTreeStatus === "?") {
|
|
3263
|
+
untracked.push(filePath);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
if (indexStatus !== " " && indexStatus !== "?") {
|
|
3267
|
+
staged.push({
|
|
3268
|
+
path: filePath,
|
|
3269
|
+
status: parseStatus(indexStatus)
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
if (workTreeStatus !== " " && workTreeStatus !== "?") {
|
|
3273
|
+
unstaged.push({
|
|
3274
|
+
path: filePath,
|
|
3275
|
+
status: parseStatus(workTreeStatus)
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
return {
|
|
3280
|
+
isGitRepo: true,
|
|
3281
|
+
branch,
|
|
3282
|
+
ahead,
|
|
3283
|
+
behind,
|
|
3284
|
+
staged,
|
|
3285
|
+
unstaged,
|
|
3286
|
+
untracked,
|
|
3287
|
+
hasConflicts
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function parseStatus(code) {
|
|
3291
|
+
switch (code) {
|
|
3292
|
+
case "A":
|
|
3293
|
+
return "added";
|
|
3294
|
+
case "M":
|
|
3295
|
+
return "modified";
|
|
3296
|
+
case "D":
|
|
3297
|
+
return "deleted";
|
|
3298
|
+
case "R":
|
|
3299
|
+
return "renamed";
|
|
3300
|
+
case "C":
|
|
3301
|
+
return "copied";
|
|
3302
|
+
default:
|
|
3303
|
+
return "modified";
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
function getDiff(staged = false, cwd) {
|
|
3307
|
+
if (!isGitRepo(cwd)) {
|
|
3308
|
+
return { files: [], summary: "(not a git repository)" };
|
|
3309
|
+
}
|
|
3310
|
+
const args = staged ? ["diff", "--cached", "--stat"] : ["diff", "--stat"];
|
|
3311
|
+
const statOutput = execGit(args, cwd);
|
|
3312
|
+
const patchArgs = staged ? ["diff", "--cached"] : ["diff"];
|
|
3313
|
+
const patchOutput = execGit(patchArgs, cwd);
|
|
3314
|
+
const files = [];
|
|
3315
|
+
const statLines = statOutput.split("\n");
|
|
3316
|
+
for (const line of statLines) {
|
|
3317
|
+
const match = line.match(/^\s*(.+?)\s+\|\s+(\d+)\s*([+-]*)/);
|
|
3318
|
+
if (match) {
|
|
3319
|
+
const [, path4, changes, plusMinus] = match;
|
|
3320
|
+
const additions = (plusMinus.match(/\+/g) || []).length;
|
|
3321
|
+
const deletions = (plusMinus.match(/-/g) || []).length;
|
|
3322
|
+
const filePatches = patchOutput.split(/^diff --git/m);
|
|
3323
|
+
const filePatch = filePatches.find((p) => p.includes(path4.trim())) || "";
|
|
3324
|
+
files.push({
|
|
3325
|
+
path: path4.trim(),
|
|
3326
|
+
additions,
|
|
3327
|
+
deletions,
|
|
3328
|
+
patch: filePatch.slice(0, 2e3)
|
|
3329
|
+
// Limit patch size
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
3334
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
3335
|
+
const summary = `${files.length} file(s) changed, ${totalAdditions} insertion(s), ${totalDeletions} deletion(s)`;
|
|
3336
|
+
return { files, summary };
|
|
3337
|
+
}
|
|
3338
|
+
function formatStatusForPrompt(status) {
|
|
3339
|
+
if (!status.isGitRepo) {
|
|
3340
|
+
return "(not a git repository)";
|
|
3341
|
+
}
|
|
3342
|
+
const lines = [];
|
|
3343
|
+
lines.push(`Branch: ${status.branch}`);
|
|
3344
|
+
if (status.ahead > 0 || status.behind > 0) {
|
|
3345
|
+
lines.push(`Tracking: ${status.ahead} ahead, ${status.behind} behind origin`);
|
|
3346
|
+
}
|
|
3347
|
+
if (status.hasConflicts) {
|
|
3348
|
+
lines.push("\u26A0\uFE0F MERGE CONFLICTS DETECTED");
|
|
3349
|
+
}
|
|
3350
|
+
if (status.staged.length > 0) {
|
|
3351
|
+
lines.push(`
|
|
3352
|
+
Staged (${status.staged.length}):`);
|
|
3353
|
+
for (const f of status.staged.slice(0, 10)) {
|
|
3354
|
+
lines.push(` ${f.status[0].toUpperCase()} ${f.path}`);
|
|
3355
|
+
}
|
|
3356
|
+
if (status.staged.length > 10) {
|
|
3357
|
+
lines.push(` ... and ${status.staged.length - 10} more`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
if (status.unstaged.length > 0) {
|
|
3361
|
+
lines.push(`
|
|
3362
|
+
Unstaged (${status.unstaged.length}):`);
|
|
3363
|
+
for (const f of status.unstaged.slice(0, 10)) {
|
|
3364
|
+
lines.push(` ${f.status[0].toUpperCase()} ${f.path}`);
|
|
3365
|
+
}
|
|
3366
|
+
if (status.unstaged.length > 10) {
|
|
3367
|
+
lines.push(` ... and ${status.unstaged.length - 10} more`);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
if (status.untracked.length > 0) {
|
|
3371
|
+
lines.push(`
|
|
3372
|
+
Untracked (${status.untracked.length}):`);
|
|
3373
|
+
for (const f of status.untracked.slice(0, 5)) {
|
|
3374
|
+
lines.push(` ? ${f}`);
|
|
3375
|
+
}
|
|
3376
|
+
if (status.untracked.length > 5) {
|
|
3377
|
+
lines.push(` ... and ${status.untracked.length - 5} more`);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0) {
|
|
3381
|
+
lines.push("\n\u2713 Working tree clean");
|
|
3382
|
+
}
|
|
3383
|
+
return lines.join("\n");
|
|
3384
|
+
}
|
|
3385
|
+
function formatDiffForPrompt(diff) {
|
|
3386
|
+
if (diff.files.length === 0) {
|
|
3387
|
+
return "(no changes)";
|
|
3388
|
+
}
|
|
3389
|
+
const lines = [];
|
|
3390
|
+
lines.push(diff.summary);
|
|
3391
|
+
lines.push("");
|
|
3392
|
+
for (const file of diff.files.slice(0, 5)) {
|
|
3393
|
+
lines.push(`### ${file.path}`);
|
|
3394
|
+
lines.push(`+${file.additions} -${file.deletions}`);
|
|
3395
|
+
if (file.patch) {
|
|
3396
|
+
lines.push("```diff");
|
|
3397
|
+
lines.push(file.patch.slice(0, 1e3));
|
|
3398
|
+
if (file.patch.length > 1e3) lines.push("... (truncated)");
|
|
3399
|
+
lines.push("```");
|
|
3400
|
+
}
|
|
3401
|
+
lines.push("");
|
|
3402
|
+
}
|
|
3403
|
+
if (diff.files.length > 5) {
|
|
3404
|
+
lines.push(`... and ${diff.files.length - 5} more files`);
|
|
3405
|
+
}
|
|
3406
|
+
return lines.join("\n");
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/core/conversation-manager.ts
|
|
3410
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
3411
|
+
import { join as join6 } from "path";
|
|
3412
|
+
import { homedir as homedir5 } from "os";
|
|
3413
|
+
var AGDI_DIR = join6(homedir5(), ".agdi");
|
|
3414
|
+
var SESSIONS_DIR = join6(AGDI_DIR, "sessions");
|
|
3415
|
+
var MAX_CONTEXT_TOKENS = 32e3;
|
|
3416
|
+
var CHARS_PER_TOKEN = 4;
|
|
3417
|
+
var ConversationManager = class _ConversationManager {
|
|
3418
|
+
messages = [];
|
|
3419
|
+
sessionId;
|
|
3420
|
+
systemPrompt = "";
|
|
3421
|
+
constructor(sessionId) {
|
|
3422
|
+
this.sessionId = sessionId || this.generateSessionId();
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Set the system prompt (persists across conversation)
|
|
3426
|
+
*/
|
|
3427
|
+
setSystemPrompt(prompt) {
|
|
3428
|
+
this.systemPrompt = prompt;
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Get the system prompt
|
|
3432
|
+
*/
|
|
3433
|
+
getSystemPrompt() {
|
|
3434
|
+
return this.systemPrompt;
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Add a user message
|
|
3438
|
+
*/
|
|
3439
|
+
addUserMessage(content) {
|
|
3440
|
+
this.messages.push({
|
|
3441
|
+
role: "user",
|
|
3442
|
+
content,
|
|
3443
|
+
timestamp: Date.now()
|
|
3444
|
+
});
|
|
3445
|
+
this.trimToContextLimit();
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Add an assistant response
|
|
3449
|
+
*/
|
|
3450
|
+
addAssistantMessage(content) {
|
|
3451
|
+
this.messages.push({
|
|
3452
|
+
role: "assistant",
|
|
3453
|
+
content,
|
|
3454
|
+
timestamp: Date.now()
|
|
3455
|
+
});
|
|
3456
|
+
this.trimToContextLimit();
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Get all messages for LLM context
|
|
3460
|
+
*/
|
|
3461
|
+
getMessages() {
|
|
3462
|
+
return [...this.messages];
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Get messages formatted for API calls
|
|
3466
|
+
*/
|
|
3467
|
+
getMessagesForAPI() {
|
|
3468
|
+
const result = [];
|
|
3469
|
+
if (this.systemPrompt) {
|
|
3470
|
+
result.push({ role: "system", content: this.systemPrompt });
|
|
3471
|
+
}
|
|
3472
|
+
for (const msg of this.messages) {
|
|
3473
|
+
result.push({ role: msg.role, content: msg.content });
|
|
3474
|
+
}
|
|
3475
|
+
return result;
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Get message count
|
|
3479
|
+
*/
|
|
3480
|
+
getMessageCount() {
|
|
3481
|
+
return this.messages.length;
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Get turn count (user messages)
|
|
3485
|
+
*/
|
|
3486
|
+
getTurnCount() {
|
|
3487
|
+
return this.messages.filter((m) => m.role === "user").length;
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Clear conversation history
|
|
3491
|
+
*/
|
|
3492
|
+
clear() {
|
|
3493
|
+
this.messages = [];
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Get session ID
|
|
3497
|
+
*/
|
|
3498
|
+
getSessionId() {
|
|
3499
|
+
return this.sessionId;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Get last N messages
|
|
3503
|
+
*/
|
|
3504
|
+
getLastMessages(n) {
|
|
3505
|
+
return this.messages.slice(-n);
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Get conversation summary for display
|
|
3509
|
+
*/
|
|
3510
|
+
getSummary() {
|
|
3511
|
+
const turns = this.getTurnCount();
|
|
3512
|
+
const tokens = this.estimateTokens();
|
|
3513
|
+
return `Session: ${this.sessionId} | ${turns} turns | ~${tokens} tokens`;
|
|
3514
|
+
}
|
|
3515
|
+
/**
|
|
3516
|
+
* Estimate token count
|
|
3517
|
+
*/
|
|
3518
|
+
estimateTokens() {
|
|
3519
|
+
let chars = this.systemPrompt.length;
|
|
3520
|
+
for (const msg of this.messages) {
|
|
3521
|
+
chars += msg.content.length;
|
|
3522
|
+
}
|
|
3523
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Trim messages to stay within context limit
|
|
3527
|
+
*/
|
|
3528
|
+
trimToContextLimit() {
|
|
3529
|
+
while (this.estimateTokens() > MAX_CONTEXT_TOKENS && this.messages.length > 2) {
|
|
3530
|
+
this.messages.shift();
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Generate a unique session ID
|
|
3535
|
+
*/
|
|
3536
|
+
generateSessionId() {
|
|
3537
|
+
const now = /* @__PURE__ */ new Date();
|
|
3538
|
+
const date = now.toISOString().split("T")[0];
|
|
3539
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
3540
|
+
return `${date}-${random}`;
|
|
3541
|
+
}
|
|
3542
|
+
// ==================== PERSISTENCE ====================
|
|
3543
|
+
/**
|
|
3544
|
+
* Save session to disk
|
|
3545
|
+
*/
|
|
3546
|
+
save() {
|
|
3547
|
+
this.ensureDirectories();
|
|
3548
|
+
const session = {
|
|
3549
|
+
id: this.sessionId,
|
|
3550
|
+
messages: this.messages,
|
|
3551
|
+
createdAt: this.messages[0]?.timestamp || Date.now(),
|
|
3552
|
+
updatedAt: Date.now()
|
|
3553
|
+
};
|
|
3554
|
+
const filePath = join6(SESSIONS_DIR, `${this.sessionId}.json`);
|
|
3555
|
+
writeFileSync4(filePath, JSON.stringify(session, null, 2));
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Load session from disk
|
|
3559
|
+
*/
|
|
3560
|
+
static load(sessionId) {
|
|
3561
|
+
const filePath = join6(SESSIONS_DIR, `${sessionId}.json`);
|
|
3562
|
+
if (!existsSync8(filePath)) {
|
|
3563
|
+
return null;
|
|
3564
|
+
}
|
|
3565
|
+
try {
|
|
3566
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
3567
|
+
const session = JSON.parse(content);
|
|
3568
|
+
const manager = new _ConversationManager(session.id);
|
|
3569
|
+
manager.messages = session.messages;
|
|
3570
|
+
return manager;
|
|
3571
|
+
} catch {
|
|
3572
|
+
return null;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* List available sessions
|
|
3577
|
+
*/
|
|
3578
|
+
static listSessions() {
|
|
3579
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
3580
|
+
return [];
|
|
3581
|
+
}
|
|
3582
|
+
const { readdirSync: readdirSync2 } = __require("fs");
|
|
3583
|
+
const files = readdirSync2(SESSIONS_DIR);
|
|
3584
|
+
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", "")).sort().reverse();
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Ensure directories exist
|
|
3588
|
+
*/
|
|
3589
|
+
ensureDirectories() {
|
|
3590
|
+
if (!existsSync8(AGDI_DIR)) {
|
|
3591
|
+
mkdirSync4(AGDI_DIR, { recursive: true });
|
|
3592
|
+
}
|
|
3593
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
3594
|
+
mkdirSync4(SESSIONS_DIR, { recursive: true });
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
var currentConversation = null;
|
|
3599
|
+
function getConversation() {
|
|
3600
|
+
if (!currentConversation) {
|
|
3601
|
+
currentConversation = new ConversationManager();
|
|
3602
|
+
}
|
|
3603
|
+
return currentConversation;
|
|
3604
|
+
}
|
|
3605
|
+
function clearConversation() {
|
|
3606
|
+
if (currentConversation) {
|
|
3607
|
+
currentConversation.clear();
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/core/file-editor.ts
|
|
3612
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3613
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3614
|
+
import chalk11 from "chalk";
|
|
3615
|
+
import ora3 from "ora";
|
|
3616
|
+
import { input as input4, confirm as confirm2 } from "@inquirer/prompts";
|
|
3617
|
+
var EDIT_SYSTEM_PROMPT = `You are a surgical code editor. Given a file's content and an edit instruction, output ONLY a unified diff patch.
|
|
3618
|
+
|
|
3619
|
+
## Output Format (STRICT)
|
|
3620
|
+
\`\`\`diff
|
|
3621
|
+
--- a/filename
|
|
3622
|
+
+++ b/filename
|
|
3623
|
+
@@ -start,count +start,count @@
|
|
3624
|
+
context line (unchanged)
|
|
3625
|
+
-removed line
|
|
3626
|
+
+added line
|
|
3627
|
+
context line (unchanged)
|
|
3628
|
+
\`\`\`
|
|
3629
|
+
|
|
3630
|
+
## Rules
|
|
3631
|
+
1. Output ONLY the diff block, no explanation before or after
|
|
3632
|
+
2. Include 3 context lines around each change
|
|
3633
|
+
3. Use proper unified diff format with @@ hunk headers
|
|
3634
|
+
4. Preserve exact indentation (spaces/tabs)
|
|
3635
|
+
5. Multiple changes = multiple @@ hunks
|
|
3636
|
+
6. Line numbers in @@ must be accurate
|
|
3637
|
+
|
|
3638
|
+
## Example
|
|
3639
|
+
Input: "Add a console.log at line 5"
|
|
3640
|
+
Output:
|
|
3641
|
+
\`\`\`diff
|
|
3642
|
+
--- a/index.ts
|
|
3643
|
+
+++ b/index.ts
|
|
3644
|
+
@@ -3,6 +3,7 @@
|
|
3645
|
+
import { foo } from './foo';
|
|
3646
|
+
|
|
3647
|
+
function main() {
|
|
3648
|
+
+ console.log('Debug point');
|
|
3649
|
+
const result = foo();
|
|
3650
|
+
return result;
|
|
3651
|
+
}
|
|
3652
|
+
\`\`\``;
|
|
3653
|
+
async function readFileForEdit(filePath) {
|
|
3654
|
+
const resolved = resolvePath(filePath);
|
|
3655
|
+
if (!existsSync9(resolved)) {
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
try {
|
|
3659
|
+
const content = await readFile2(resolved, "utf-8");
|
|
3660
|
+
const lines = content.split("\n");
|
|
3661
|
+
const numbered = lines.map((line, i) => `${String(i + 1).padStart(4, " ")} \u2502 ${line}`).join("\n");
|
|
3662
|
+
return { content, numbered };
|
|
3663
|
+
} catch {
|
|
3664
|
+
return null;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
function extractDiff(response) {
|
|
3668
|
+
const diffMatch = response.match(/```diff\n([\s\S]*?)```/);
|
|
3669
|
+
if (diffMatch) {
|
|
3670
|
+
return diffMatch[1].trim();
|
|
3671
|
+
}
|
|
3672
|
+
const codeMatch = response.match(/```\n([\s\S]*?)```/);
|
|
3673
|
+
if (codeMatch && codeMatch[1].includes("@@")) {
|
|
3674
|
+
return codeMatch[1].trim();
|
|
3675
|
+
}
|
|
3676
|
+
if (response.includes("@@") && (response.includes("---") || response.includes("+++"))) {
|
|
3677
|
+
return response.trim();
|
|
3678
|
+
}
|
|
3679
|
+
return null;
|
|
3680
|
+
}
|
|
3681
|
+
function previewDiff(diff) {
|
|
3682
|
+
console.log(chalk11.cyan.bold("\n\u{1F4DD} Proposed Changes:\n"));
|
|
3683
|
+
const lines = diff.split("\n");
|
|
3684
|
+
for (const line of lines) {
|
|
3685
|
+
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
3686
|
+
console.log(chalk11.gray(line));
|
|
3687
|
+
} else if (line.startsWith("@@")) {
|
|
3688
|
+
console.log(chalk11.cyan(line));
|
|
3689
|
+
} else if (line.startsWith("+")) {
|
|
3690
|
+
console.log(chalk11.green(line));
|
|
3691
|
+
} else if (line.startsWith("-")) {
|
|
3692
|
+
console.log(chalk11.red(line));
|
|
3693
|
+
} else {
|
|
3694
|
+
console.log(chalk11.gray(line));
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
console.log("");
|
|
3698
|
+
}
|
|
3699
|
+
function countChanges(diff) {
|
|
3700
|
+
const lines = diff.split("\n");
|
|
3701
|
+
let added = 0;
|
|
3702
|
+
let removed = 0;
|
|
3703
|
+
for (const line of lines) {
|
|
3704
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
3705
|
+
added++;
|
|
3706
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
3707
|
+
removed++;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return { added, removed };
|
|
3711
|
+
}
|
|
3712
|
+
async function handleFileEdit(filePath, llm) {
|
|
3713
|
+
const env = getEnvironment();
|
|
3714
|
+
if (!fileExists(filePath)) {
|
|
3715
|
+
console.log(chalk11.red(`
|
|
3716
|
+
\u2717 File not found: ${filePath}
|
|
3717
|
+
`));
|
|
3718
|
+
return { success: false, error: "File not found" };
|
|
3719
|
+
}
|
|
3720
|
+
const fileData = await readFileForEdit(filePath);
|
|
3721
|
+
if (!fileData) {
|
|
3722
|
+
console.log(chalk11.red(`
|
|
3723
|
+
\u2717 Could not read file: ${filePath}
|
|
3724
|
+
`));
|
|
3725
|
+
return { success: false, error: "Could not read file" };
|
|
3726
|
+
}
|
|
3727
|
+
const previewLines = fileData.numbered.split("\n").slice(0, 20);
|
|
3728
|
+
console.log(chalk11.cyan.bold(`
|
|
3729
|
+
\u{1F4C4} ${filePath}
|
|
3730
|
+
`));
|
|
3731
|
+
console.log(chalk11.gray(previewLines.join("\n")));
|
|
3732
|
+
if (fileData.content.split("\n").length > 20) {
|
|
3733
|
+
console.log(chalk11.gray(` ... (${fileData.content.split("\n").length - 20} more lines)`));
|
|
3734
|
+
}
|
|
3735
|
+
console.log("");
|
|
3736
|
+
const instruction = await input4({
|
|
3737
|
+
message: chalk11.yellow("Describe the edit:")
|
|
3738
|
+
});
|
|
3739
|
+
if (!instruction.trim()) {
|
|
3740
|
+
console.log(chalk11.gray("\n(no instruction provided)\n"));
|
|
3741
|
+
return { success: false, error: "No instruction" };
|
|
3742
|
+
}
|
|
3743
|
+
const spinner = ora3("Generating edit...").start();
|
|
3744
|
+
try {
|
|
3745
|
+
const prompt = `File: ${filePath}
|
|
3746
|
+
|
|
3747
|
+
Content:
|
|
3748
|
+
\`\`\`
|
|
3749
|
+
${fileData.content}
|
|
3750
|
+
\`\`\`
|
|
3751
|
+
|
|
3752
|
+
Edit instruction: ${instruction}
|
|
3753
|
+
|
|
3754
|
+
Generate the unified diff to make this change.`;
|
|
3755
|
+
const response = await llm.generate(prompt, EDIT_SYSTEM_PROMPT);
|
|
3756
|
+
spinner.stop();
|
|
3757
|
+
const diff = extractDiff(response.text);
|
|
3758
|
+
if (!diff) {
|
|
3759
|
+
console.log(chalk11.yellow("\n\u26A0\uFE0F Could not generate a valid diff.\n"));
|
|
3760
|
+
console.log(chalk11.gray("AI response:\n" + response.text.slice(0, 500)));
|
|
3761
|
+
return { success: false, error: "Invalid diff generated" };
|
|
3762
|
+
}
|
|
3763
|
+
previewDiff(diff);
|
|
3764
|
+
const changes = countChanges(diff);
|
|
3765
|
+
console.log(chalk11.gray(` ${chalk11.green(`+${changes.added}`)} additions, ${chalk11.red(`-${changes.removed}`)} deletions
|
|
3766
|
+
`));
|
|
3767
|
+
const shouldApply = await confirm2({
|
|
3768
|
+
message: "Apply these changes?",
|
|
3769
|
+
default: true
|
|
3770
|
+
});
|
|
3771
|
+
if (!shouldApply) {
|
|
3772
|
+
console.log(chalk11.gray("\n\u{1F44B} Edit cancelled.\n"));
|
|
3773
|
+
return { success: false, error: "Cancelled by user" };
|
|
3774
|
+
}
|
|
3775
|
+
const applySpinner = ora3("Applying changes...").start();
|
|
3776
|
+
const result = await applyPatchTool(filePath, diff);
|
|
3777
|
+
applySpinner.stop();
|
|
3778
|
+
if (result.success) {
|
|
3779
|
+
console.log(chalk11.green(`
|
|
3780
|
+
\u2713 Successfully edited ${filePath}
|
|
3781
|
+
`));
|
|
3782
|
+
logEvent({
|
|
3783
|
+
eventType: "command_result",
|
|
3784
|
+
command: `/edit ${filePath}`,
|
|
3785
|
+
result: { exitCode: 0 },
|
|
3786
|
+
metadata: {
|
|
3787
|
+
instruction,
|
|
3788
|
+
added: changes.added,
|
|
3789
|
+
removed: changes.removed
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
return { success: true, linesChanged: changes.added + changes.removed };
|
|
3793
|
+
} else {
|
|
3794
|
+
console.log(chalk11.red(`
|
|
3795
|
+
\u2717 Failed to apply changes: ${result.error}
|
|
3796
|
+
`));
|
|
3797
|
+
return { success: false, error: result.error };
|
|
3798
|
+
}
|
|
3799
|
+
} catch (error) {
|
|
3800
|
+
spinner.stop();
|
|
3801
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3802
|
+
console.log(chalk11.red(`
|
|
3803
|
+
\u2717 Error: ${msg}
|
|
3804
|
+
`));
|
|
3805
|
+
return { success: false, error: msg };
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
// src/commands/agdi-dev.ts
|
|
3810
|
+
var BASE_CHAT_PROMPT = `You are Agdi dev, an elite AI coding assistant. You help developers write code, debug issues, and build applications.
|
|
2738
3811
|
|
|
2739
3812
|
## Your Capabilities
|
|
2740
3813
|
- Write complete, production-ready code
|
|
2741
3814
|
- Debug and fix code issues
|
|
2742
3815
|
- Explain code and architecture
|
|
2743
3816
|
- Answer coding questions
|
|
3817
|
+
- Analyze git status and diffs
|
|
3818
|
+
- Generate meaningful commit messages
|
|
2744
3819
|
|
|
2745
3820
|
## Response Style
|
|
2746
3821
|
- Be concise and direct
|
|
@@ -2753,6 +3828,17 @@ var CHAT_SYSTEM_PROMPT = `You are Agdi dev, an elite AI coding assistant. You he
|
|
|
2753
3828
|
- Follow modern best practices
|
|
2754
3829
|
- Include proper error handling
|
|
2755
3830
|
- Write self-documenting code`;
|
|
3831
|
+
function buildContextAwarePrompt() {
|
|
3832
|
+
const context = buildWorkspaceContext();
|
|
3833
|
+
const gitStatus = getStatus();
|
|
3834
|
+
context.gitStatus = formatStatusForPrompt(gitStatus);
|
|
3835
|
+
const contextSection = formatContextForPrompt(context);
|
|
3836
|
+
return `${BASE_CHAT_PROMPT}
|
|
3837
|
+
|
|
3838
|
+
---
|
|
3839
|
+
|
|
3840
|
+
${contextSection}`;
|
|
3841
|
+
}
|
|
2756
3842
|
var BUILD_SYSTEM_PROMPT = `You are Agdi dev, an AI coding assistant that generates applications.
|
|
2757
3843
|
|
|
2758
3844
|
## CRITICAL: Output Format
|
|
@@ -2789,29 +3875,36 @@ Instead, output a single JSON object with this exact structure:
|
|
|
2789
3875
|
async function startCodingMode() {
|
|
2790
3876
|
const activeConfig = getActiveProvider();
|
|
2791
3877
|
if (!activeConfig) {
|
|
2792
|
-
console.log(
|
|
3878
|
+
console.log(chalk12.red("\u274C No API key configured. Run: agdi"));
|
|
2793
3879
|
return;
|
|
2794
3880
|
}
|
|
2795
3881
|
const { provider, apiKey, model } = activeConfig;
|
|
2796
3882
|
const config = loadConfig();
|
|
2797
3883
|
const env = initSession();
|
|
2798
3884
|
displaySessionHeader(env);
|
|
3885
|
+
const isTrusted = await ensureTrusted(env.workspaceRoot);
|
|
3886
|
+
if (!isTrusted) {
|
|
3887
|
+
process.exit(0);
|
|
3888
|
+
}
|
|
2799
3889
|
logSessionStart(env.workspaceRoot, env.trustLevel);
|
|
2800
|
-
console.log(
|
|
2801
|
-
console.log(
|
|
2802
|
-
console.log(
|
|
2803
|
-
console.log(
|
|
3890
|
+
console.log(chalk12.cyan.bold("\u26A1 Agdi dev\n"));
|
|
3891
|
+
console.log(chalk12.gray(`Model: ${chalk12.cyan(model)}`));
|
|
3892
|
+
console.log(chalk12.gray(`Workspace: ${chalk12.cyan(env.workspaceRoot)}`));
|
|
3893
|
+
console.log(chalk12.gray("Commands: /status, /diff, /commit, /build, /clear, /history, /model, /help, /exit\n"));
|
|
3894
|
+
console.log(chalk12.gray("\u2500".repeat(50) + "\n"));
|
|
2804
3895
|
const pm = new ProjectManager();
|
|
2805
3896
|
let llm = createLLMProvider(provider, { apiKey, model });
|
|
3897
|
+
const conversation = getConversation();
|
|
3898
|
+
conversation.setSystemPrompt(buildContextAwarePrompt());
|
|
2806
3899
|
while (true) {
|
|
2807
3900
|
try {
|
|
2808
|
-
const userInput = await
|
|
2809
|
-
message:
|
|
3901
|
+
const userInput = await input5({
|
|
3902
|
+
message: chalk12.green("\u2192")
|
|
2810
3903
|
});
|
|
2811
3904
|
const trimmed = userInput.trim().toLowerCase();
|
|
2812
3905
|
if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit") {
|
|
2813
3906
|
logSessionEnd();
|
|
2814
|
-
console.log(
|
|
3907
|
+
console.log(chalk12.gray("\n\u{1F44B} Goodbye!\n"));
|
|
2815
3908
|
break;
|
|
2816
3909
|
}
|
|
2817
3910
|
if (trimmed === "/help") {
|
|
@@ -2826,22 +3919,66 @@ async function startCodingMode() {
|
|
|
2826
3919
|
apiKey: newConfig.apiKey,
|
|
2827
3920
|
model: newConfig.model
|
|
2828
3921
|
});
|
|
2829
|
-
console.log(
|
|
3922
|
+
console.log(chalk12.gray(`Now using: ${chalk12.cyan(newConfig.model)}
|
|
2830
3923
|
`));
|
|
2831
3924
|
}
|
|
2832
3925
|
continue;
|
|
2833
3926
|
}
|
|
2834
3927
|
if (trimmed === "/chat") {
|
|
2835
|
-
console.log(
|
|
3928
|
+
console.log(chalk12.gray("\nSwitching to chat mode. Type /code to return.\n"));
|
|
2836
3929
|
await chatMode(llm);
|
|
2837
3930
|
continue;
|
|
2838
3931
|
}
|
|
3932
|
+
if (trimmed === "/clear") {
|
|
3933
|
+
clearConversation();
|
|
3934
|
+
conversation.setSystemPrompt(buildContextAwarePrompt());
|
|
3935
|
+
console.log(chalk12.green("\n\u2713 Conversation cleared.\n"));
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
if (trimmed === "/history") {
|
|
3939
|
+
const messages = conversation.getMessages();
|
|
3940
|
+
if (messages.length === 0) {
|
|
3941
|
+
console.log(chalk12.gray("\n(no conversation history)\n"));
|
|
3942
|
+
} else {
|
|
3943
|
+
console.log(chalk12.cyan.bold("\n\u{1F4DC} Conversation History\n"));
|
|
3944
|
+
console.log(chalk12.gray(conversation.getSummary()));
|
|
3945
|
+
console.log("");
|
|
3946
|
+
for (const msg of messages.slice(-6)) {
|
|
3947
|
+
const role = msg.role === "user" ? chalk12.green("You") : chalk12.cyan("AI");
|
|
3948
|
+
const preview = msg.content.slice(0, 80) + (msg.content.length > 80 ? "..." : "");
|
|
3949
|
+
console.log(` ${role}: ${chalk12.gray(preview)}`);
|
|
3950
|
+
}
|
|
3951
|
+
console.log("");
|
|
3952
|
+
}
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
if (trimmed === "/status") {
|
|
3956
|
+
await handleGitStatus(llm);
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
if (trimmed === "/diff") {
|
|
3960
|
+
await handleGitDiff(llm);
|
|
3961
|
+
continue;
|
|
3962
|
+
}
|
|
3963
|
+
if (trimmed === "/commit") {
|
|
3964
|
+
await handleGitCommit(llm);
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3967
|
+
if (trimmed.startsWith("/edit ")) {
|
|
3968
|
+
const filePath = userInput.slice(6).trim();
|
|
3969
|
+
if (filePath) {
|
|
3970
|
+
await handleFileEdit(filePath, llm);
|
|
3971
|
+
} else {
|
|
3972
|
+
console.log(chalk12.yellow("\nUsage: /edit <file>\n"));
|
|
3973
|
+
}
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
2839
3976
|
if (trimmed.startsWith("/build ") || trimmed.startsWith("build ")) {
|
|
2840
3977
|
const prompt = userInput.replace(/^\/?build\s+/i, "").trim();
|
|
2841
3978
|
if (prompt) {
|
|
2842
3979
|
await buildAppWithPlan(prompt, llm);
|
|
2843
3980
|
} else {
|
|
2844
|
-
console.log(
|
|
3981
|
+
console.log(chalk12.yellow("\nUsage: /build <description>\n"));
|
|
2845
3982
|
}
|
|
2846
3983
|
continue;
|
|
2847
3984
|
}
|
|
@@ -2853,9 +3990,17 @@ async function startCodingMode() {
|
|
|
2853
3990
|
await buildAppWithPlan(userInput, llm);
|
|
2854
3991
|
continue;
|
|
2855
3992
|
}
|
|
2856
|
-
const spinner =
|
|
3993
|
+
const spinner = ora4("Thinking...").start();
|
|
2857
3994
|
try {
|
|
2858
|
-
|
|
3995
|
+
conversation.addUserMessage(userInput);
|
|
3996
|
+
let response;
|
|
3997
|
+
if (llm.chat) {
|
|
3998
|
+
response = await llm.chat(conversation.getMessagesForAPI());
|
|
3999
|
+
} else {
|
|
4000
|
+
const contextPrompt = buildContextAwarePrompt();
|
|
4001
|
+
response = await llm.generate(userInput, contextPrompt);
|
|
4002
|
+
}
|
|
4003
|
+
conversation.addAssistantMessage(response.text);
|
|
2859
4004
|
spinner.stop();
|
|
2860
4005
|
console.log("\n" + formatResponse(response.text) + "\n");
|
|
2861
4006
|
} catch (error) {
|
|
@@ -2865,7 +4010,7 @@ async function startCodingMode() {
|
|
|
2865
4010
|
} catch (error) {
|
|
2866
4011
|
if (error.name === "ExitPromptError") {
|
|
2867
4012
|
logSessionEnd();
|
|
2868
|
-
console.log(
|
|
4013
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
2869
4014
|
process.exit(0);
|
|
2870
4015
|
}
|
|
2871
4016
|
throw error;
|
|
@@ -2873,13 +4018,13 @@ async function startCodingMode() {
|
|
|
2873
4018
|
}
|
|
2874
4019
|
}
|
|
2875
4020
|
async function buildAppWithPlan(prompt, llm) {
|
|
2876
|
-
const spinner =
|
|
4021
|
+
const spinner = ora4("Generating action plan...").start();
|
|
2877
4022
|
try {
|
|
2878
4023
|
const response = await llm.generate(prompt, BUILD_SYSTEM_PROMPT);
|
|
2879
4024
|
spinner.stop();
|
|
2880
4025
|
const result = await parseAndExecutePlan(response.text);
|
|
2881
4026
|
if (!result) {
|
|
2882
|
-
console.log(
|
|
4027
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Model did not return an action plan. Showing response:\n"));
|
|
2883
4028
|
console.log(formatResponse(response.text) + "\n");
|
|
2884
4029
|
}
|
|
2885
4030
|
} catch (error) {
|
|
@@ -2890,15 +4035,15 @@ async function buildAppWithPlan(prompt, llm) {
|
|
|
2890
4035
|
async function chatMode(llm) {
|
|
2891
4036
|
while (true) {
|
|
2892
4037
|
try {
|
|
2893
|
-
const userInput = await
|
|
2894
|
-
message:
|
|
4038
|
+
const userInput = await input5({
|
|
4039
|
+
message: chalk12.blue("\u{1F4AC}")
|
|
2895
4040
|
});
|
|
2896
4041
|
if (userInput.toLowerCase() === "/code" || userInput.toLowerCase() === "/exit") {
|
|
2897
|
-
console.log(
|
|
4042
|
+
console.log(chalk12.gray("\nBack to Agdi dev mode.\n"));
|
|
2898
4043
|
return;
|
|
2899
4044
|
}
|
|
2900
4045
|
if (!userInput.trim()) continue;
|
|
2901
|
-
const spinner =
|
|
4046
|
+
const spinner = ora4("...").start();
|
|
2902
4047
|
const response = await llm.generate(userInput, "You are a helpful assistant. Be friendly and concise.");
|
|
2903
4048
|
spinner.stop();
|
|
2904
4049
|
console.log("\n" + response.text + "\n");
|
|
@@ -2910,51 +4055,172 @@ async function chatMode(llm) {
|
|
|
2910
4055
|
}
|
|
2911
4056
|
}
|
|
2912
4057
|
}
|
|
4058
|
+
async function handleGitStatus(llm) {
|
|
4059
|
+
if (!isGitRepo()) {
|
|
4060
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Not a git repository\n"));
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
const spinner = ora4("Analyzing git status...").start();
|
|
4064
|
+
try {
|
|
4065
|
+
const status = getStatus();
|
|
4066
|
+
const statusText = formatStatusForPrompt(status);
|
|
4067
|
+
const prompt = `Analyze this git status and provide a brief summary of the current state of the repository. What's staged, unstaged, and what should be done next?
|
|
4068
|
+
|
|
4069
|
+
${statusText}`;
|
|
4070
|
+
const response = await llm.generate(prompt, BASE_CHAT_PROMPT);
|
|
4071
|
+
spinner.stop();
|
|
4072
|
+
console.log(chalk12.cyan.bold("\n\u{1F4CA} Git Status Analysis\n"));
|
|
4073
|
+
console.log(chalk12.gray(statusText));
|
|
4074
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 AI Analysis \u2500\u2500\u2500\n"));
|
|
4075
|
+
console.log(formatResponse(response.text) + "\n");
|
|
4076
|
+
} catch (error) {
|
|
4077
|
+
spinner.fail("Error analyzing status");
|
|
4078
|
+
handleError(error);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
async function handleGitDiff(llm) {
|
|
4082
|
+
if (!isGitRepo()) {
|
|
4083
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Not a git repository\n"));
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
const spinner = ora4("Analyzing changes...").start();
|
|
4087
|
+
try {
|
|
4088
|
+
const stagedDiff = getDiff(true);
|
|
4089
|
+
const unstagedDiff = getDiff(false);
|
|
4090
|
+
if (stagedDiff.files.length === 0 && unstagedDiff.files.length === 0) {
|
|
4091
|
+
spinner.stop();
|
|
4092
|
+
console.log(chalk12.gray("\n(no changes to analyze)\n"));
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
let diffContext = "";
|
|
4096
|
+
if (stagedDiff.files.length > 0) {
|
|
4097
|
+
diffContext += "## Staged Changes\n" + formatDiffForPrompt(stagedDiff) + "\n\n";
|
|
4098
|
+
}
|
|
4099
|
+
if (unstagedDiff.files.length > 0) {
|
|
4100
|
+
diffContext += "## Unstaged Changes\n" + formatDiffForPrompt(unstagedDiff);
|
|
4101
|
+
}
|
|
4102
|
+
const prompt = `Explain these code changes. What are the key modifications and their purpose?
|
|
4103
|
+
|
|
4104
|
+
${diffContext}`;
|
|
4105
|
+
const response = await llm.generate(prompt, BASE_CHAT_PROMPT);
|
|
4106
|
+
spinner.stop();
|
|
4107
|
+
console.log(chalk12.cyan.bold("\n\u{1F50D} Diff Analysis\n"));
|
|
4108
|
+
console.log(formatResponse(response.text) + "\n");
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
spinner.fail("Error analyzing diff");
|
|
4111
|
+
handleError(error);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
async function handleGitCommit(llm) {
|
|
4115
|
+
if (!isGitRepo()) {
|
|
4116
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Not a git repository\n"));
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
const status = getStatus();
|
|
4120
|
+
if (status.staged.length === 0) {
|
|
4121
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F No staged changes. Stage some changes first with `git add`.\n"));
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
const spinner = ora4("Generating commit message...").start();
|
|
4125
|
+
try {
|
|
4126
|
+
const stagedDiff = getDiff(true);
|
|
4127
|
+
const diffText = formatDiffForPrompt(stagedDiff);
|
|
4128
|
+
const prompt = `Generate a concise, conventional commit message for these staged changes. Use the format: type(scope): description
|
|
4129
|
+
|
|
4130
|
+
Types: feat, fix, docs, style, refactor, test, chore
|
|
4131
|
+
Keep the message under 72 characters.
|
|
4132
|
+
Return ONLY the commit message, nothing else.
|
|
4133
|
+
|
|
4134
|
+
Changes:
|
|
4135
|
+
${diffText}`;
|
|
4136
|
+
const response = await llm.generate(prompt, "You are a git commit message generator. Output ONLY the commit message, no explanation.");
|
|
4137
|
+
spinner.stop();
|
|
4138
|
+
const commitMessage = response.text.trim().split("\n")[0];
|
|
4139
|
+
console.log(chalk12.cyan.bold("\n\u{1F4AC} Generated Commit Message\n"));
|
|
4140
|
+
console.log(chalk12.white(` ${commitMessage}
|
|
4141
|
+
`));
|
|
4142
|
+
const shouldCommit = await confirm3({
|
|
4143
|
+
message: "Commit with this message?",
|
|
4144
|
+
default: true
|
|
4145
|
+
});
|
|
4146
|
+
if (shouldCommit) {
|
|
4147
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
4148
|
+
const env = getEnvironment();
|
|
4149
|
+
try {
|
|
4150
|
+
execSync2(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
4151
|
+
cwd: env.workspaceRoot,
|
|
4152
|
+
stdio: "inherit"
|
|
4153
|
+
});
|
|
4154
|
+
console.log(chalk12.green("\n\u2713 Committed successfully!\n"));
|
|
4155
|
+
} catch (gitError) {
|
|
4156
|
+
console.log(chalk12.red("\n\u2717 Commit failed. Check git output above.\n"));
|
|
4157
|
+
}
|
|
4158
|
+
} else {
|
|
4159
|
+
console.log(chalk12.gray("\n\u{1F44B} Commit cancelled.\n"));
|
|
4160
|
+
}
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
spinner.fail("Error generating commit");
|
|
4163
|
+
handleError(error);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
2913
4166
|
function formatResponse(text) {
|
|
2914
4167
|
return text.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
2915
|
-
const header = lang ?
|
|
4168
|
+
const header = lang ? chalk12.gray(`\u2500\u2500 ${lang} \u2500\u2500`) : chalk12.gray("\u2500\u2500 code \u2500\u2500");
|
|
2916
4169
|
return `
|
|
2917
4170
|
${header}
|
|
2918
|
-
${
|
|
2919
|
-
${
|
|
4171
|
+
${chalk12.white(code.trim())}
|
|
4172
|
+
${chalk12.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
2920
4173
|
`;
|
|
2921
4174
|
});
|
|
2922
4175
|
}
|
|
2923
4176
|
function showHelp() {
|
|
2924
|
-
console.log(
|
|
2925
|
-
console.log(
|
|
2926
|
-
console.log(
|
|
2927
|
-
console.log(
|
|
2928
|
-
console.log(
|
|
2929
|
-
console.log(
|
|
2930
|
-
console.log(
|
|
2931
|
-
console.log(
|
|
4177
|
+
console.log(chalk12.cyan.bold("\n\u{1F4D6} Commands\n"));
|
|
4178
|
+
console.log(chalk12.cyan(" Git Commands:"));
|
|
4179
|
+
console.log(chalk12.gray(" /status ") + "AI analysis of git status");
|
|
4180
|
+
console.log(chalk12.gray(" /diff ") + "AI explanation of current changes");
|
|
4181
|
+
console.log(chalk12.gray(" /commit ") + "Generate and run git commit");
|
|
4182
|
+
console.log("");
|
|
4183
|
+
console.log(chalk12.cyan(" Build Commands:"));
|
|
4184
|
+
console.log(chalk12.gray(" /build ") + "Generate and execute an application");
|
|
4185
|
+
console.log(chalk12.gray(" /edit ") + "AI-powered surgical file editing");
|
|
4186
|
+
console.log("");
|
|
4187
|
+
console.log(chalk12.cyan(" Conversation:"));
|
|
4188
|
+
console.log(chalk12.gray(" /clear ") + "Clear conversation history");
|
|
4189
|
+
console.log(chalk12.gray(" /history ") + "Show recent conversation");
|
|
4190
|
+
console.log("");
|
|
4191
|
+
console.log(chalk12.cyan(" General:"));
|
|
4192
|
+
console.log(chalk12.gray(" /model ") + "Change AI model");
|
|
4193
|
+
console.log(chalk12.gray(" /chat ") + "Switch to chat mode");
|
|
4194
|
+
console.log(chalk12.gray(" /help ") + "Show this help");
|
|
4195
|
+
console.log(chalk12.gray(" /exit ") + "Exit Agdi");
|
|
4196
|
+
console.log(chalk12.gray("\n Or just type your coding question!\n"));
|
|
4197
|
+
console.log(chalk12.gray('Tip: "Create a todo app" will generate & write files.\n'));
|
|
2932
4198
|
}
|
|
2933
4199
|
function handleError(error) {
|
|
2934
4200
|
const msg = error instanceof Error ? error.message : String(error);
|
|
2935
4201
|
if (msg.includes("429") || msg.includes("quota")) {
|
|
2936
|
-
console.log(
|
|
4202
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Quota exceeded. Run /model to switch.\n"));
|
|
2937
4203
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
2938
|
-
console.log(
|
|
4204
|
+
console.log(chalk12.red("\n\u{1F511} Invalid API key. Run: agdi auth\n"));
|
|
2939
4205
|
} else {
|
|
2940
|
-
console.log(
|
|
4206
|
+
console.log(chalk12.red("\n" + msg + "\n"));
|
|
2941
4207
|
}
|
|
2942
4208
|
}
|
|
2943
4209
|
|
|
2944
4210
|
// src/index.ts
|
|
2945
4211
|
var BANNER = `
|
|
2946
|
-
${
|
|
2947
|
-
${
|
|
2948
|
-
${
|
|
2949
|
-
${
|
|
2950
|
-
${
|
|
2951
|
-
${
|
|
4212
|
+
${chalk13.cyan(` ___ __ _ `)}
|
|
4213
|
+
${chalk13.cyan(` / | ____ _____/ /(_) `)}
|
|
4214
|
+
${chalk13.cyan(` / /| | / __ \`/ __ // / `)}
|
|
4215
|
+
${chalk13.cyan(` / ___ |/ /_/ / /_/ // / `)}
|
|
4216
|
+
${chalk13.cyan(`/_/ |_|\\_, /\\__,_//_/ `)}
|
|
4217
|
+
${chalk13.cyan(` /____/ `)}
|
|
2952
4218
|
`;
|
|
2953
4219
|
var program = new Command();
|
|
2954
|
-
program.name("agdi").description(
|
|
4220
|
+
program.name("agdi").description(chalk13.cyan("\u{1F680} AI-powered coding assistant")).version("2.2.2").configureHelp({
|
|
2955
4221
|
// Show banner only when help is requested
|
|
2956
4222
|
formatHelp: (cmd, helper) => {
|
|
2957
|
-
return BANNER + "\n" +
|
|
4223
|
+
return BANNER + "\n" + chalk13.gray(" The Open Source AI Architect") + "\n" + chalk13.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n") + "\n" + helper.formatHelp(cmd, helper);
|
|
2958
4224
|
}
|
|
2959
4225
|
});
|
|
2960
4226
|
program.action(async () => {
|
|
@@ -2965,7 +4231,7 @@ program.action(async () => {
|
|
|
2965
4231
|
await startCodingMode();
|
|
2966
4232
|
} catch (error) {
|
|
2967
4233
|
if (error.name === "ExitPromptError") {
|
|
2968
|
-
console.log(
|
|
4234
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
2969
4235
|
process.exit(0);
|
|
2970
4236
|
}
|
|
2971
4237
|
throw error;
|
|
@@ -2980,7 +4246,7 @@ program.command("auth").description("Configure API keys").option("--status", "Sh
|
|
|
2980
4246
|
}
|
|
2981
4247
|
} catch (error) {
|
|
2982
4248
|
if (error.name === "ExitPromptError") {
|
|
2983
|
-
console.log(
|
|
4249
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
2984
4250
|
process.exit(0);
|
|
2985
4251
|
}
|
|
2986
4252
|
throw error;
|
|
@@ -2991,7 +4257,7 @@ program.command("model").alias("models").description("Change AI model").action(a
|
|
|
2991
4257
|
await selectModel();
|
|
2992
4258
|
} catch (error) {
|
|
2993
4259
|
if (error.name === "ExitPromptError") {
|
|
2994
|
-
console.log(
|
|
4260
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
2995
4261
|
process.exit(0);
|
|
2996
4262
|
}
|
|
2997
4263
|
throw error;
|
|
@@ -3005,7 +4271,7 @@ program.command("chat").description("Start a chat session").action(async () => {
|
|
|
3005
4271
|
await startChat();
|
|
3006
4272
|
} catch (error) {
|
|
3007
4273
|
if (error.name === "ExitPromptError") {
|
|
3008
|
-
console.log(
|
|
4274
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
3009
4275
|
process.exit(0);
|
|
3010
4276
|
}
|
|
3011
4277
|
throw error;
|
|
@@ -3016,7 +4282,7 @@ program.command("run [directory]").description("Run a generated project").action
|
|
|
3016
4282
|
await runProject(directory);
|
|
3017
4283
|
} catch (error) {
|
|
3018
4284
|
if (error.name === "ExitPromptError") {
|
|
3019
|
-
console.log(
|
|
4285
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
3020
4286
|
process.exit(0);
|
|
3021
4287
|
}
|
|
3022
4288
|
throw error;
|
|
@@ -3029,10 +4295,10 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
3029
4295
|
}
|
|
3030
4296
|
const activeConfig = getActiveProvider();
|
|
3031
4297
|
if (!activeConfig) {
|
|
3032
|
-
console.log(
|
|
4298
|
+
console.log(chalk13.red("\u274C No API key configured. Run: agdi auth"));
|
|
3033
4299
|
return;
|
|
3034
4300
|
}
|
|
3035
|
-
const spinner =
|
|
4301
|
+
const spinner = ora5("Generating application...").start();
|
|
3036
4302
|
try {
|
|
3037
4303
|
const llm = createLLMProvider(activeConfig.provider, {
|
|
3038
4304
|
apiKey: activeConfig.apiKey,
|
|
@@ -3041,30 +4307,30 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
3041
4307
|
const pm = new ProjectManager();
|
|
3042
4308
|
pm.create(options.output.replace("./", ""), prompt);
|
|
3043
4309
|
const { plan, files } = await generateApp(prompt, llm, (step, file) => {
|
|
3044
|
-
spinner.text = file ? `${step} ${
|
|
4310
|
+
spinner.text = file ? `${step} ${chalk13.gray(file)}` : step;
|
|
3045
4311
|
});
|
|
3046
4312
|
pm.updateFiles(files);
|
|
3047
4313
|
pm.updateDependencies(plan.dependencies);
|
|
3048
4314
|
await writeProject(pm.get(), options.output);
|
|
3049
|
-
spinner.succeed(
|
|
3050
|
-
console.log(
|
|
3051
|
-
\u{1F4C1} Created ${files.length} files in ${
|
|
3052
|
-
console.log(
|
|
4315
|
+
spinner.succeed(chalk13.green("App generated!"));
|
|
4316
|
+
console.log(chalk13.gray(`
|
|
4317
|
+
\u{1F4C1} Created ${files.length} files in ${chalk13.cyan(options.output)}`));
|
|
4318
|
+
console.log(chalk13.gray("\nNext: cd " + options.output + " && npm install && npm run dev\n"));
|
|
3053
4319
|
} catch (error) {
|
|
3054
4320
|
spinner.fail("Generation failed");
|
|
3055
4321
|
const msg = error instanceof Error ? error.message : String(error);
|
|
3056
4322
|
if (msg.includes("429") || msg.includes("quota")) {
|
|
3057
|
-
console.log(
|
|
4323
|
+
console.log(chalk13.yellow("\n\u26A0\uFE0F Quota exceeded. Run: agdi model\n"));
|
|
3058
4324
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
3059
|
-
console.log(
|
|
4325
|
+
console.log(chalk13.red("\n\u{1F511} Invalid API key. Run: agdi auth\n"));
|
|
3060
4326
|
} else {
|
|
3061
|
-
console.error(
|
|
4327
|
+
console.error(chalk13.red("\n" + msg + "\n"));
|
|
3062
4328
|
}
|
|
3063
4329
|
process.exit(1);
|
|
3064
4330
|
}
|
|
3065
4331
|
} catch (error) {
|
|
3066
4332
|
if (error.name === "ExitPromptError") {
|
|
3067
|
-
console.log(
|
|
4333
|
+
console.log(chalk13.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
3068
4334
|
process.exit(0);
|
|
3069
4335
|
}
|
|
3070
4336
|
throw error;
|
|
@@ -3073,11 +4339,11 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
3073
4339
|
program.command("config").description("Show configuration").action(async () => {
|
|
3074
4340
|
const config = loadConfig();
|
|
3075
4341
|
const active = getActiveProvider();
|
|
3076
|
-
console.log(
|
|
3077
|
-
console.log(
|
|
3078
|
-
console.log(
|
|
3079
|
-
console.log(
|
|
3080
|
-
console.log(
|
|
4342
|
+
console.log(chalk13.cyan.bold("\n\u2699\uFE0F Configuration\n"));
|
|
4343
|
+
console.log(chalk13.gray(" Provider: ") + chalk13.cyan(config.defaultProvider || "not set"));
|
|
4344
|
+
console.log(chalk13.gray(" Model: ") + chalk13.cyan(config.defaultModel || "not set"));
|
|
4345
|
+
console.log(chalk13.gray(" Config: ") + chalk13.gray("~/.agdi/config.json"));
|
|
4346
|
+
console.log(chalk13.cyan.bold("\n\u{1F510} API Keys\n"));
|
|
3081
4347
|
const keys = [
|
|
3082
4348
|
["Gemini", config.geminiApiKey],
|
|
3083
4349
|
["OpenRouter", config.openrouterApiKey],
|
|
@@ -3086,7 +4352,7 @@ program.command("config").description("Show configuration").action(async () => {
|
|
|
3086
4352
|
["DeepSeek", config.deepseekApiKey]
|
|
3087
4353
|
];
|
|
3088
4354
|
for (const [name, key] of keys) {
|
|
3089
|
-
const status = key ?
|
|
4355
|
+
const status = key ? chalk13.green("\u2713") : chalk13.gray("\u2717");
|
|
3090
4356
|
console.log(` ${status} ${name}`);
|
|
3091
4357
|
}
|
|
3092
4358
|
console.log("");
|