agdi 2.1.0 → 2.2.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/dist/index.js +1571 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk12 from "chalk";
|
|
6
6
|
import ora4 from "ora";
|
|
7
7
|
|
|
8
8
|
// src/core/llm/index.ts
|
|
@@ -1020,7 +1020,7 @@ async function runProject(targetDir) {
|
|
|
1020
1020
|
if (!fs3.existsSync(nodeModulesPath)) {
|
|
1021
1021
|
console.log(chalk6.yellow("\u{1F4E6} Installing dependencies...\n"));
|
|
1022
1022
|
const installSpinner = ora2("Running npm install...").start();
|
|
1023
|
-
await new Promise((
|
|
1023
|
+
await new Promise((resolve5, reject) => {
|
|
1024
1024
|
const install = spawn("npm", ["install"], {
|
|
1025
1025
|
cwd: absoluteDir,
|
|
1026
1026
|
stdio: "inherit",
|
|
@@ -1029,7 +1029,7 @@ async function runProject(targetDir) {
|
|
|
1029
1029
|
install.on("close", (code) => {
|
|
1030
1030
|
if (code === 0) {
|
|
1031
1031
|
installSpinner.succeed("Dependencies installed!");
|
|
1032
|
-
|
|
1032
|
+
resolve5();
|
|
1033
1033
|
} else {
|
|
1034
1034
|
installSpinner.fail("npm install failed");
|
|
1035
1035
|
reject(new Error(`npm install exited with code ${code}`));
|
|
@@ -1272,16 +1272,1475 @@ async function selectModel() {
|
|
|
1272
1272
|
|
|
1273
1273
|
// src/commands/codex.ts
|
|
1274
1274
|
import { input as input4 } from "@inquirer/prompts";
|
|
1275
|
-
import
|
|
1275
|
+
import chalk11 from "chalk";
|
|
1276
1276
|
import ora3 from "ora";
|
|
1277
|
-
|
|
1277
|
+
|
|
1278
|
+
// src/actions/plan-executor.ts
|
|
1279
|
+
import { select as select4, confirm } from "@inquirer/prompts";
|
|
1280
|
+
import chalk10 from "chalk";
|
|
1281
|
+
import { spawn as spawn2 } from "child_process";
|
|
1282
|
+
import { resolve as resolve4 } from "path";
|
|
1283
|
+
|
|
1284
|
+
// src/actions/types.ts
|
|
1285
|
+
function summarizePlan(plan) {
|
|
1286
|
+
let filesCreated = 0;
|
|
1287
|
+
let filesDeleted = 0;
|
|
1288
|
+
let dirsCreated = 0;
|
|
1289
|
+
let commandsToRun = 0;
|
|
1290
|
+
const domains = [];
|
|
1291
|
+
const ports = [];
|
|
1292
|
+
let maxRiskTier = 0;
|
|
1293
|
+
for (const action of plan.actions) {
|
|
1294
|
+
switch (action.type) {
|
|
1295
|
+
case "mkdir":
|
|
1296
|
+
dirsCreated++;
|
|
1297
|
+
maxRiskTier = Math.max(maxRiskTier, 1);
|
|
1298
|
+
break;
|
|
1299
|
+
case "writeFile":
|
|
1300
|
+
filesCreated++;
|
|
1301
|
+
maxRiskTier = Math.max(maxRiskTier, 1);
|
|
1302
|
+
break;
|
|
1303
|
+
case "deleteFile":
|
|
1304
|
+
filesDeleted++;
|
|
1305
|
+
maxRiskTier = Math.max(maxRiskTier, 1);
|
|
1306
|
+
break;
|
|
1307
|
+
case "exec":
|
|
1308
|
+
commandsToRun++;
|
|
1309
|
+
if (["npm", "yarn", "pnpm", "pip"].includes(action.argv[0])) {
|
|
1310
|
+
maxRiskTier = Math.max(maxRiskTier, 2);
|
|
1311
|
+
if (["npm", "yarn", "pnpm"].includes(action.argv[0])) {
|
|
1312
|
+
domains.push("registry.npmjs.org");
|
|
1313
|
+
}
|
|
1314
|
+
if (action.argv[0] === "pip") {
|
|
1315
|
+
domains.push("pypi.org");
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (action.argv.includes("dev") || action.argv.includes("start")) {
|
|
1319
|
+
ports.push(3e3);
|
|
1320
|
+
}
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
filesCreated,
|
|
1326
|
+
filesDeleted,
|
|
1327
|
+
dirsCreated,
|
|
1328
|
+
commandsToRun,
|
|
1329
|
+
domains: [...new Set(domains)],
|
|
1330
|
+
ports: [...new Set(ports)],
|
|
1331
|
+
riskTier: maxRiskTier
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function parseActionPlan(response) {
|
|
1335
|
+
try {
|
|
1336
|
+
const jsonMatch = response.match(/\{[\s\S]*"actions"[\s\S]*\}/);
|
|
1337
|
+
if (!jsonMatch) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1341
|
+
if (!parsed.actions || !Array.isArray(parsed.actions)) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
projectName: parsed.projectName || "generated-app",
|
|
1346
|
+
actions: parsed.actions,
|
|
1347
|
+
nextSteps: parsed.nextSteps
|
|
1348
|
+
};
|
|
1349
|
+
} catch {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// src/actions/fs-tools.ts
|
|
1355
|
+
import { mkdir, writeFile, rm, readFile } from "fs/promises";
|
|
1356
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1357
|
+
import { resolve, dirname, relative, isAbsolute } from "path";
|
|
1358
|
+
|
|
1359
|
+
// src/security/execution-env.ts
|
|
1360
|
+
import chalk8 from "chalk";
|
|
1361
|
+
import { platform, cwd as processCwd } from "os";
|
|
1362
|
+
import { existsSync, readFileSync } from "fs";
|
|
1363
|
+
function detectWSL() {
|
|
1364
|
+
if (platform() !== "linux") return false;
|
|
1365
|
+
try {
|
|
1366
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
if (existsSync("/proc/version")) {
|
|
1370
|
+
const version = readFileSync("/proc/version", "utf-8");
|
|
1371
|
+
return version.toLowerCase().includes("microsoft");
|
|
1372
|
+
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
function detectContainer() {
|
|
1378
|
+
try {
|
|
1379
|
+
if (existsSync("/.dockerenv")) {
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
if (existsSync("/proc/1/cgroup")) {
|
|
1383
|
+
const cgroup = readFileSync("/proc/1/cgroup", "utf-8");
|
|
1384
|
+
if (cgroup.includes("docker") || cgroup.includes("lxc") || cgroup.includes("kubepods")) {
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (process.env.KUBERNETES_SERVICE_HOST) {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
}
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
function detectBackend() {
|
|
1396
|
+
const os2 = platform();
|
|
1397
|
+
if (detectContainer()) {
|
|
1398
|
+
return "container";
|
|
1399
|
+
}
|
|
1400
|
+
if (detectWSL()) {
|
|
1401
|
+
return "wsl";
|
|
1402
|
+
}
|
|
1403
|
+
switch (os2) {
|
|
1404
|
+
case "win32":
|
|
1405
|
+
return "windows-host";
|
|
1406
|
+
case "linux":
|
|
1407
|
+
return "linux-host";
|
|
1408
|
+
case "darwin":
|
|
1409
|
+
return "macos-host";
|
|
1410
|
+
default:
|
|
1411
|
+
return "linux-host";
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function getWorkspaceRoot() {
|
|
1415
|
+
return processCwd();
|
|
1416
|
+
}
|
|
1417
|
+
function detectEnvironment(overrides) {
|
|
1418
|
+
const os2 = platform() === "win32" ? "windows" : platform() === "darwin" ? "darwin" : "linux";
|
|
1419
|
+
const backend = detectBackend();
|
|
1420
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
1421
|
+
return {
|
|
1422
|
+
os: os2,
|
|
1423
|
+
backend,
|
|
1424
|
+
workspaceRoot,
|
|
1425
|
+
cwd: workspaceRoot,
|
|
1426
|
+
// Start cwd at workspace root
|
|
1427
|
+
networkPolicy: "off",
|
|
1428
|
+
// Default: network off
|
|
1429
|
+
allowedDomains: [
|
|
1430
|
+
"registry.npmjs.org",
|
|
1431
|
+
"pypi.org",
|
|
1432
|
+
"github.com",
|
|
1433
|
+
"api.github.com"
|
|
1434
|
+
],
|
|
1435
|
+
trustLevel: "untrusted",
|
|
1436
|
+
// Default: untrusted
|
|
1437
|
+
isContainer: backend === "container",
|
|
1438
|
+
isWSL: backend === "wsl",
|
|
1439
|
+
...overrides
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function formatBackend(backend) {
|
|
1443
|
+
switch (backend) {
|
|
1444
|
+
case "windows-host":
|
|
1445
|
+
return "Windows host";
|
|
1446
|
+
case "linux-host":
|
|
1447
|
+
return "Linux host";
|
|
1448
|
+
case "macos-host":
|
|
1449
|
+
return "macOS host";
|
|
1450
|
+
case "wsl":
|
|
1451
|
+
return "WSL (Windows Subsystem for Linux)";
|
|
1452
|
+
case "container":
|
|
1453
|
+
return "Container sandbox";
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function formatNetwork(policy, domains) {
|
|
1457
|
+
switch (policy) {
|
|
1458
|
+
case "off":
|
|
1459
|
+
return "off";
|
|
1460
|
+
case "allowlist":
|
|
1461
|
+
return `allowlist (${domains.slice(0, 2).join(", ")}${domains.length > 2 ? "..." : ""})`;
|
|
1462
|
+
case "on":
|
|
1463
|
+
return "unrestricted";
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
function formatTrust(trust) {
|
|
1467
|
+
switch (trust) {
|
|
1468
|
+
case "untrusted":
|
|
1469
|
+
return chalk8.red("untrusted (read-only mode)");
|
|
1470
|
+
case "session":
|
|
1471
|
+
return chalk8.yellow("session trusted");
|
|
1472
|
+
case "persistent":
|
|
1473
|
+
return chalk8.green("trusted");
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function displaySessionHeader(env) {
|
|
1477
|
+
const boxWidth = 54;
|
|
1478
|
+
const topBorder = "\u256D\u2500 Agdi Terminal Session " + "\u2500".repeat(boxWidth - 25) + "\u256E";
|
|
1479
|
+
const bottomBorder = "\u2570" + "\u2500".repeat(boxWidth) + "\u256F";
|
|
1480
|
+
const lines = [
|
|
1481
|
+
`Exec env: ${formatBackend(env.backend)}`,
|
|
1482
|
+
`Workspace: ${truncatePath(env.workspaceRoot, 40)}`,
|
|
1483
|
+
`cwd: ${truncatePath(env.cwd, 40)}`,
|
|
1484
|
+
`Network: ${formatNetwork(env.networkPolicy, env.allowedDomains)}`,
|
|
1485
|
+
`Trust: ${formatTrust(env.trustLevel)}`
|
|
1486
|
+
];
|
|
1487
|
+
console.log(chalk8.cyan(topBorder));
|
|
1488
|
+
for (const line of lines) {
|
|
1489
|
+
const padding = " ".repeat(Math.max(0, boxWidth - stripAnsi(line).length - 2));
|
|
1490
|
+
console.log(chalk8.cyan("\u2502 ") + line + padding + chalk8.cyan(" \u2502"));
|
|
1491
|
+
}
|
|
1492
|
+
console.log(chalk8.cyan(bottomBorder));
|
|
1493
|
+
console.log("");
|
|
1494
|
+
}
|
|
1495
|
+
function truncatePath(path4, maxLen) {
|
|
1496
|
+
if (path4.length <= maxLen) return path4;
|
|
1497
|
+
return "..." + path4.slice(-(maxLen - 3));
|
|
1498
|
+
}
|
|
1499
|
+
function stripAnsi(str) {
|
|
1500
|
+
return str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
1501
|
+
}
|
|
1502
|
+
var currentEnvironment = null;
|
|
1503
|
+
function initSession(overrides) {
|
|
1504
|
+
currentEnvironment = detectEnvironment(overrides);
|
|
1505
|
+
return currentEnvironment;
|
|
1506
|
+
}
|
|
1507
|
+
function getEnvironment() {
|
|
1508
|
+
if (!currentEnvironment) {
|
|
1509
|
+
return initSession();
|
|
1510
|
+
}
|
|
1511
|
+
return currentEnvironment;
|
|
1512
|
+
}
|
|
1513
|
+
function updateEnvironment(updates) {
|
|
1514
|
+
currentEnvironment = { ...getEnvironment(), ...updates };
|
|
1515
|
+
return currentEnvironment;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/security/audit-logger.ts
|
|
1519
|
+
import { existsSync as existsSync2, appendFileSync, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
|
|
1520
|
+
import { join } from "path";
|
|
1521
|
+
import { homedir as homedir2 } from "os";
|
|
1522
|
+
var LOG_DIR = join(homedir2(), ".agdi");
|
|
1523
|
+
var LOG_FILE = join(LOG_DIR, "audit.jsonl");
|
|
1524
|
+
var MAX_OUTPUT_LENGTH = 1e3;
|
|
1525
|
+
var currentSessionId = null;
|
|
1526
|
+
function generateSessionId() {
|
|
1527
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
1528
|
+
}
|
|
1529
|
+
function getSessionId() {
|
|
1530
|
+
if (!currentSessionId) {
|
|
1531
|
+
currentSessionId = generateSessionId();
|
|
1532
|
+
}
|
|
1533
|
+
return currentSessionId;
|
|
1534
|
+
}
|
|
1535
|
+
function ensureLogDir() {
|
|
1536
|
+
if (!existsSync2(LOG_DIR)) {
|
|
1537
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function truncateOutput(output) {
|
|
1541
|
+
if (!output) return output;
|
|
1542
|
+
if (output.length <= MAX_OUTPUT_LENGTH) return output;
|
|
1543
|
+
return output.substring(0, MAX_OUTPUT_LENGTH) + `... (truncated, ${output.length} total chars)`;
|
|
1544
|
+
}
|
|
1545
|
+
function logEvent(event) {
|
|
1546
|
+
ensureLogDir();
|
|
1547
|
+
const fullEvent = {
|
|
1548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1549
|
+
sessionId: getSessionId(),
|
|
1550
|
+
...event
|
|
1551
|
+
};
|
|
1552
|
+
if (fullEvent.result?.output) {
|
|
1553
|
+
fullEvent.result.output = truncateOutput(fullEvent.result.output);
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
appendFileSync(LOG_FILE, JSON.stringify(fullEvent) + "\n");
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
console.error("Failed to write audit log:", error);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function logSessionStart(workspacePath, trustLevel) {
|
|
1562
|
+
currentSessionId = generateSessionId();
|
|
1563
|
+
logEvent({
|
|
1564
|
+
eventType: "session_start",
|
|
1565
|
+
metadata: {
|
|
1566
|
+
workspacePath,
|
|
1567
|
+
trustLevel,
|
|
1568
|
+
platform: process.platform,
|
|
1569
|
+
nodeVersion: process.version
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function logSessionEnd() {
|
|
1574
|
+
logEvent({
|
|
1575
|
+
eventType: "session_end"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/actions/fs-tools.ts
|
|
1580
|
+
function isWithinWorkspace(p, workspaceRoot) {
|
|
1581
|
+
const resolved = resolve(workspaceRoot, p);
|
|
1582
|
+
const root = resolve(workspaceRoot);
|
|
1583
|
+
const rel = relative(root, resolved);
|
|
1584
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1585
|
+
}
|
|
1586
|
+
function validatePath(path4) {
|
|
1587
|
+
const env = getEnvironment();
|
|
1588
|
+
const resolved = resolve(env.workspaceRoot, path4);
|
|
1589
|
+
if (!isWithinWorkspace(path4, env.workspaceRoot)) {
|
|
1590
|
+
return {
|
|
1591
|
+
valid: false,
|
|
1592
|
+
resolved,
|
|
1593
|
+
error: `Path outside workspace: ${path4}`
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
return { valid: true, resolved };
|
|
1597
|
+
}
|
|
1598
|
+
async function mkdirTool(path4) {
|
|
1599
|
+
const validation = validatePath(path4);
|
|
1600
|
+
if (!validation.valid) {
|
|
1601
|
+
return { success: false, error: validation.error };
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
await mkdir(validation.resolved, { recursive: true });
|
|
1605
|
+
logEvent({
|
|
1606
|
+
eventType: "command_result",
|
|
1607
|
+
command: `mkdir ${path4}`,
|
|
1608
|
+
result: { exitCode: 0 },
|
|
1609
|
+
metadata: { tool: "mkdirTool", path: validation.resolved }
|
|
1610
|
+
});
|
|
1611
|
+
return { success: true };
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1614
|
+
return { success: false, error: msg };
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function writeFileTool(path4, content) {
|
|
1618
|
+
const validation = validatePath(path4);
|
|
1619
|
+
if (!validation.valid) {
|
|
1620
|
+
return { success: false, error: validation.error };
|
|
1621
|
+
}
|
|
1622
|
+
try {
|
|
1623
|
+
const dir = dirname(validation.resolved);
|
|
1624
|
+
await mkdir(dir, { recursive: true });
|
|
1625
|
+
await writeFile(validation.resolved, content, "utf-8");
|
|
1626
|
+
logEvent({
|
|
1627
|
+
eventType: "command_result",
|
|
1628
|
+
command: `writeFile ${path4}`,
|
|
1629
|
+
result: { exitCode: 0 },
|
|
1630
|
+
metadata: {
|
|
1631
|
+
tool: "writeFileTool",
|
|
1632
|
+
path: validation.resolved,
|
|
1633
|
+
contentLength: content.length
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
return { success: true };
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1639
|
+
return { success: false, error: msg };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
async function deleteFileTool(path4) {
|
|
1643
|
+
const validation = validatePath(path4);
|
|
1644
|
+
if (!validation.valid) {
|
|
1645
|
+
return { success: false, error: validation.error };
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
if (!existsSync3(validation.resolved)) {
|
|
1649
|
+
return { success: true };
|
|
1650
|
+
}
|
|
1651
|
+
await rm(validation.resolved);
|
|
1652
|
+
logEvent({
|
|
1653
|
+
eventType: "command_result",
|
|
1654
|
+
command: `deleteFile ${path4}`,
|
|
1655
|
+
result: { exitCode: 0 },
|
|
1656
|
+
metadata: { tool: "deleteFileTool", path: validation.resolved }
|
|
1657
|
+
});
|
|
1658
|
+
return { success: true };
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1661
|
+
return { success: false, error: msg };
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/security/permission-gate.ts
|
|
1666
|
+
import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2 } from "path";
|
|
1667
|
+
|
|
1668
|
+
// src/security/argv-parser.ts
|
|
1669
|
+
var WINDOWS_FLAG_PATTERNS = [
|
|
1670
|
+
// Single letter flags: /i, /s, /r, /q, etc.
|
|
1671
|
+
/^\/[aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ]$/,
|
|
1672
|
+
// Numbered flags: /1, /2, etc.
|
|
1673
|
+
/^\/[0-9]+$/,
|
|
1674
|
+
// Common multi-char flags
|
|
1675
|
+
/^\/(?:all|help|version|quiet|verbose|force|recursive)$/i,
|
|
1676
|
+
// findstr flags
|
|
1677
|
+
/^\/(?:b|e|l|r|s|i|x|v|n|m|o|p|offline|c|g|f|d|a)$/i,
|
|
1678
|
+
// xcopy/robocopy flags
|
|
1679
|
+
/^\/(?:e|s|h|k|y|q|f|l|c|v|w|d|u|m|a|t|n|o|x|exclude|copy|move|purge)$/i,
|
|
1680
|
+
// dir flags
|
|
1681
|
+
/^\/(?:a|b|c|d|l|n|o|p|q|r|s|t|w|x|4)$/i
|
|
1682
|
+
];
|
|
1683
|
+
var TOOL_ARG_SCHEMAS = {
|
|
1684
|
+
findstr: { flags: [], notPaths: [] },
|
|
1685
|
+
// All /x args are flags
|
|
1686
|
+
xcopy: { flags: [], notPaths: [] },
|
|
1687
|
+
robocopy: { flags: [], notPaths: [] },
|
|
1688
|
+
dir: { flags: [], notPaths: [] },
|
|
1689
|
+
find: { flags: [], notPaths: [0] },
|
|
1690
|
+
// First arg is search string
|
|
1691
|
+
grep: { flags: [], notPaths: [0] },
|
|
1692
|
+
// First arg is pattern
|
|
1693
|
+
sed: { flags: [], notPaths: [0] },
|
|
1694
|
+
// First arg is expression
|
|
1695
|
+
awk: { flags: [], notPaths: [0] }
|
|
1696
|
+
// First arg is program
|
|
1697
|
+
};
|
|
1698
|
+
function parseArgv(command) {
|
|
1699
|
+
const argv = [];
|
|
1700
|
+
let current = "";
|
|
1701
|
+
let inSingleQuote = false;
|
|
1702
|
+
let inDoubleQuote = false;
|
|
1703
|
+
let escaped = false;
|
|
1704
|
+
for (let i = 0; i < command.length; i++) {
|
|
1705
|
+
const char = command[i];
|
|
1706
|
+
if (escaped) {
|
|
1707
|
+
current += char;
|
|
1708
|
+
escaped = false;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (char === "\\" && !inSingleQuote) {
|
|
1712
|
+
escaped = true;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (char === "'" && !inDoubleQuote) {
|
|
1716
|
+
inSingleQuote = !inSingleQuote;
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (char === '"' && !inSingleQuote) {
|
|
1720
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if ((char === " " || char === " ") && !inSingleQuote && !inDoubleQuote) {
|
|
1724
|
+
if (current) {
|
|
1725
|
+
argv.push(current);
|
|
1726
|
+
current = "";
|
|
1727
|
+
}
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
current += char;
|
|
1731
|
+
}
|
|
1732
|
+
if (current) {
|
|
1733
|
+
argv.push(current);
|
|
1734
|
+
}
|
|
1735
|
+
return {
|
|
1736
|
+
argv,
|
|
1737
|
+
command: argv[0] || "",
|
|
1738
|
+
args: argv.slice(1),
|
|
1739
|
+
rawCommand: command
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function isWindowsFlag(token, commandName) {
|
|
1743
|
+
for (const pattern of WINDOWS_FLAG_PATTERNS) {
|
|
1744
|
+
if (pattern.test(token)) {
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (commandName && TOOL_ARG_SCHEMAS[commandName.toLowerCase()]) {
|
|
1749
|
+
if (/^\/[a-zA-Z]/.test(token)) {
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
function isLikelyPath(token, commandName) {
|
|
1756
|
+
if (isWindowsFlag(token, commandName)) {
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
if (token.startsWith("/") && token.length > 1 && token.includes("/")) {
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
if (/^[A-Za-z]:[\\/]/.test(token)) {
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
if (/\.[a-zA-Z0-9]{1,6}$/.test(token) && !token.startsWith("-") && !token.startsWith("/")) {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
if (token === "." || token === ".." || token.startsWith("./") || token.startsWith("../")) {
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
var WRITE_COMMANDS = {
|
|
1777
|
+
// Position of write path(s) in args (0-indexed)
|
|
1778
|
+
cp: [1],
|
|
1779
|
+
mv: [1],
|
|
1780
|
+
touch: [0, 1, 2, 3, 4],
|
|
1781
|
+
// All args
|
|
1782
|
+
mkdir: [0, 1, 2, 3, 4],
|
|
1783
|
+
rm: [0, 1, 2, 3, 4],
|
|
1784
|
+
echo: [],
|
|
1785
|
+
// Only writes with >
|
|
1786
|
+
tee: [0],
|
|
1787
|
+
sed: [1],
|
|
1788
|
+
// With -i
|
|
1789
|
+
tar: [1],
|
|
1790
|
+
// -cf archive
|
|
1791
|
+
zip: [0],
|
|
1792
|
+
unzip: [],
|
|
1793
|
+
// Uses -d flag
|
|
1794
|
+
git: [],
|
|
1795
|
+
// Complex
|
|
1796
|
+
npm: [],
|
|
1797
|
+
// Writes to node_modules
|
|
1798
|
+
yarn: [],
|
|
1799
|
+
pnpm: []
|
|
1800
|
+
};
|
|
1801
|
+
var READ_COMMANDS = {
|
|
1802
|
+
cat: [0, 1, 2, 3, 4],
|
|
1803
|
+
head: [0],
|
|
1804
|
+
tail: [0],
|
|
1805
|
+
less: [0],
|
|
1806
|
+
more: [0],
|
|
1807
|
+
grep: [1, 2, 3, 4],
|
|
1808
|
+
find: [0],
|
|
1809
|
+
ls: [0],
|
|
1810
|
+
dir: [0],
|
|
1811
|
+
type: [0]
|
|
1812
|
+
};
|
|
1813
|
+
function extractPaths(parsed) {
|
|
1814
|
+
const paths = [];
|
|
1815
|
+
const cmd = parsed.command.toLowerCase();
|
|
1816
|
+
const rawCmd = parsed.rawCommand;
|
|
1817
|
+
const redirectMatch = rawCmd.match(/>\s*(\S+)/);
|
|
1818
|
+
if (redirectMatch) {
|
|
1819
|
+
paths.push({
|
|
1820
|
+
path: redirectMatch[1],
|
|
1821
|
+
operation: "write",
|
|
1822
|
+
position: -1
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
for (let i = 0; i < parsed.args.length; i++) {
|
|
1826
|
+
const arg = parsed.args[i];
|
|
1827
|
+
if (!isLikelyPath(arg, cmd)) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
let operation = "unknown";
|
|
1831
|
+
if (WRITE_COMMANDS[cmd]?.includes(i)) {
|
|
1832
|
+
operation = "write";
|
|
1833
|
+
} else if (READ_COMMANDS[cmd]?.includes(i)) {
|
|
1834
|
+
operation = "read";
|
|
1835
|
+
}
|
|
1836
|
+
paths.push({
|
|
1837
|
+
path: arg,
|
|
1838
|
+
operation,
|
|
1839
|
+
position: i
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
return paths;
|
|
1843
|
+
}
|
|
1844
|
+
function extractDomains(command) {
|
|
1845
|
+
const domains = [];
|
|
1846
|
+
const urlPattern = /https?:\/\/([a-zA-Z0-9.-]+)/gi;
|
|
1847
|
+
let match;
|
|
1848
|
+
while ((match = urlPattern.exec(command)) !== null) {
|
|
1849
|
+
domains.push(match[1]);
|
|
1850
|
+
}
|
|
1851
|
+
const networkCommands = ["curl", "wget", "fetch", "npm", "yarn", "pnpm", "pip", "git"];
|
|
1852
|
+
const parsed = parseArgv(command);
|
|
1853
|
+
if (networkCommands.includes(parsed.command.toLowerCase())) {
|
|
1854
|
+
if (["npm", "yarn", "pnpm"].includes(parsed.command.toLowerCase())) {
|
|
1855
|
+
domains.push("registry.npmjs.org");
|
|
1856
|
+
}
|
|
1857
|
+
if (parsed.command.toLowerCase() === "pip") {
|
|
1858
|
+
domains.push("pypi.org");
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return [...new Set(domains)];
|
|
1862
|
+
}
|
|
1863
|
+
function extractPorts(command) {
|
|
1864
|
+
const ports = [];
|
|
1865
|
+
const patterns = [
|
|
1866
|
+
/-p\s*(\d+)/gi,
|
|
1867
|
+
// -p 3000
|
|
1868
|
+
/--port[=\s]+(\d+)/gi,
|
|
1869
|
+
// --port 3000 or --port=3000
|
|
1870
|
+
/:(\d{2,5})(?:\s|$|\/)/g,
|
|
1871
|
+
// :3000 or localhost:3000
|
|
1872
|
+
/PORT[=:]\s*(\d+)/gi
|
|
1873
|
+
// PORT=3000
|
|
1874
|
+
];
|
|
1875
|
+
for (const pattern of patterns) {
|
|
1876
|
+
let match;
|
|
1877
|
+
while ((match = pattern.exec(command)) !== null) {
|
|
1878
|
+
const port = parseInt(match[1], 10);
|
|
1879
|
+
if (port > 0 && port <= 65535) {
|
|
1880
|
+
ports.push(port);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return [...new Set(ports)];
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/security/rules-engine.ts
|
|
1888
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1889
|
+
import { join as join2 } from "path";
|
|
1890
|
+
import { homedir as homedir3 } from "os";
|
|
1891
|
+
var DEFAULT_RULES = [
|
|
1892
|
+
// Tier 0: Read-only (allow)
|
|
1893
|
+
{ id: "default-ls", pattern: ["ls"], action: "allow", source: "default", createdAt: "", description: "List directory" },
|
|
1894
|
+
{ id: "default-dir", pattern: ["dir"], action: "allow", source: "default", createdAt: "", description: "List directory (Windows)" },
|
|
1895
|
+
{ id: "default-cat", pattern: ["cat"], action: "allow", source: "default", createdAt: "", description: "View file" },
|
|
1896
|
+
{ id: "default-type", pattern: ["type"], action: "allow", source: "default", createdAt: "", description: "View file (Windows)" },
|
|
1897
|
+
{ id: "default-pwd", pattern: ["pwd"], action: "allow", source: "default", createdAt: "", description: "Print working directory" },
|
|
1898
|
+
{ id: "default-echo", pattern: ["echo"], action: "allow", source: "default", createdAt: "", description: "Print text" },
|
|
1899
|
+
{ id: "default-head", pattern: ["head"], action: "allow", source: "default", createdAt: "", description: "View file head" },
|
|
1900
|
+
{ id: "default-tail", pattern: ["tail"], action: "allow", source: "default", createdAt: "", description: "View file tail" },
|
|
1901
|
+
{ id: "default-find", pattern: ["find"], action: "allow", source: "default", createdAt: "", description: "Find files" },
|
|
1902
|
+
{ id: "default-grep", pattern: ["grep"], action: "allow", source: "default", createdAt: "", description: "Search in files" },
|
|
1903
|
+
{ id: "default-findstr", pattern: ["findstr"], action: "allow", source: "default", createdAt: "", description: "Search in files (Windows)" },
|
|
1904
|
+
// Git read commands (allow)
|
|
1905
|
+
{ id: "default-git-status", pattern: ["git", "status"], action: "allow", source: "default", createdAt: "", description: "Git status" },
|
|
1906
|
+
{ id: "default-git-diff", pattern: ["git", "diff"], action: "allow", source: "default", createdAt: "", description: "Git diff" },
|
|
1907
|
+
{ id: "default-git-log", pattern: ["git", "log"], action: "allow", source: "default", createdAt: "", description: "Git log" },
|
|
1908
|
+
{ id: "default-git-branch", pattern: ["git", "branch"], action: "allow", source: "default", createdAt: "", description: "Git branch" },
|
|
1909
|
+
{ id: "default-git-show", pattern: ["git", "show"], action: "allow", source: "default", createdAt: "", description: "Git show" },
|
|
1910
|
+
// Tier 1: Workspace writes (prompt)
|
|
1911
|
+
{ id: "default-touch", pattern: ["touch"], action: "prompt", source: "default", createdAt: "", description: "Create file" },
|
|
1912
|
+
{ id: "default-mkdir", pattern: ["mkdir"], action: "prompt", source: "default", createdAt: "", description: "Create directory" },
|
|
1913
|
+
{ id: "default-git-add", pattern: ["git", "add"], action: "prompt", source: "default", createdAt: "", description: "Git add" },
|
|
1914
|
+
{ id: "default-git-commit", pattern: ["git", "commit"], action: "prompt", source: "default", createdAt: "", description: "Git commit" },
|
|
1915
|
+
// Tier 2: Package managers (prompt)
|
|
1916
|
+
{ id: "default-npm-install", pattern: ["npm", "install"], action: "prompt", source: "default", createdAt: "", description: "npm install" },
|
|
1917
|
+
{ id: "default-npm-i", pattern: ["npm", "i"], action: "prompt", source: "default", createdAt: "", description: "npm install (short)" },
|
|
1918
|
+
{ id: "default-yarn-add", pattern: ["yarn", "add"], action: "prompt", source: "default", createdAt: "", description: "yarn add" },
|
|
1919
|
+
{ id: "default-yarn-install", pattern: ["yarn", "install"], action: "prompt", source: "default", createdAt: "", description: "yarn install" },
|
|
1920
|
+
{ id: "default-pnpm-install", pattern: ["pnpm", "install"], action: "prompt", source: "default", createdAt: "", description: "pnpm install" },
|
|
1921
|
+
{ id: "default-pnpm-add", pattern: ["pnpm", "add"], action: "prompt", source: "default", createdAt: "", description: "pnpm add" },
|
|
1922
|
+
{ id: "default-pip-install", pattern: ["pip", "install"], action: "prompt", source: "default", createdAt: "", description: "pip install" },
|
|
1923
|
+
// Tier 3: Dangerous (forbid)
|
|
1924
|
+
{ id: "default-sudo", pattern: ["sudo"], action: "forbid", source: "default", createdAt: "", description: "Elevated privileges" },
|
|
1925
|
+
{ id: "default-rm-rf", pattern: ["rm", "-rf"], action: "forbid", source: "default", createdAt: "", description: "Recursive force delete" },
|
|
1926
|
+
{ id: "default-rm-fr", pattern: ["rm", "-fr"], action: "forbid", source: "default", createdAt: "", description: "Recursive force delete" },
|
|
1927
|
+
{ id: "default-chmod-777", pattern: ["chmod", "777"], action: "forbid", source: "default", createdAt: "", description: "World-writable permissions" },
|
|
1928
|
+
{ id: "default-curl-bash", pattern: ["curl"], action: "prompt", source: "default", createdAt: "", description: "HTTP request" },
|
|
1929
|
+
{ id: "default-wget", pattern: ["wget"], action: "prompt", source: "default", createdAt: "", description: "HTTP download" },
|
|
1930
|
+
{ id: "default-git-push-force", pattern: ["git", "push", "--force"], action: "forbid", source: "default", createdAt: "", description: "Force push" },
|
|
1931
|
+
{ id: "default-git-push-f", pattern: ["git", "push", "-f"], action: "forbid", source: "default", createdAt: "", description: "Force push" }
|
|
1932
|
+
];
|
|
1933
|
+
function matchesPosition(argvItem, patternItem) {
|
|
1934
|
+
const argLower = argvItem.toLowerCase();
|
|
1935
|
+
if (Array.isArray(patternItem)) {
|
|
1936
|
+
return patternItem.some((p) => p.toLowerCase() === argLower);
|
|
1937
|
+
}
|
|
1938
|
+
return patternItem.toLowerCase() === argLower;
|
|
1939
|
+
}
|
|
1940
|
+
function matchesPattern(argv, pattern) {
|
|
1941
|
+
if (argv.length < pattern.length) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1945
|
+
if (!matchesPosition(argv[i], pattern[i])) {
|
|
1946
|
+
return false;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return true;
|
|
1950
|
+
}
|
|
1951
|
+
function getRestrictionLevel(action) {
|
|
1952
|
+
switch (action) {
|
|
1953
|
+
case "forbid":
|
|
1954
|
+
return 2;
|
|
1955
|
+
case "prompt":
|
|
1956
|
+
return 1;
|
|
1957
|
+
case "allow":
|
|
1958
|
+
return 0;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function evaluateRules(argv, rules) {
|
|
1962
|
+
const matchedRules = [];
|
|
1963
|
+
for (const rule of rules) {
|
|
1964
|
+
if (matchesPattern(argv, rule.pattern)) {
|
|
1965
|
+
matchedRules.push(rule);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (matchedRules.length === 0) {
|
|
1969
|
+
return {
|
|
1970
|
+
decision: "prompt",
|
|
1971
|
+
// Default: prompt for unknown
|
|
1972
|
+
matchedRules: [],
|
|
1973
|
+
mostRestrictive: null
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
let mostRestrictive = matchedRules[0];
|
|
1977
|
+
for (const rule of matchedRules) {
|
|
1978
|
+
if (getRestrictionLevel(rule.action) > getRestrictionLevel(mostRestrictive.action)) {
|
|
1979
|
+
mostRestrictive = rule;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
decision: mostRestrictive.action,
|
|
1984
|
+
matchedRules,
|
|
1985
|
+
mostRestrictive
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
var RULES_DIR = join2(homedir3(), ".agdi");
|
|
1989
|
+
var RULES_FILE = join2(RULES_DIR, "rules.json");
|
|
1990
|
+
function loadUserRules() {
|
|
1991
|
+
try {
|
|
1992
|
+
if (existsSync4(RULES_FILE)) {
|
|
1993
|
+
const data = readFileSync3(RULES_FILE, "utf-8");
|
|
1994
|
+
const parsed = JSON.parse(data);
|
|
1995
|
+
return parsed.rules || [];
|
|
1996
|
+
}
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
return [];
|
|
2000
|
+
}
|
|
2001
|
+
var sessionRules = [];
|
|
2002
|
+
function getAllRules() {
|
|
2003
|
+
return [...DEFAULT_RULES, ...loadUserRules(), ...sessionRules];
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// src/security/shell-wrapper-detector.ts
|
|
2007
|
+
var SHELL_WRAPPERS = {
|
|
2008
|
+
bash: {
|
|
2009
|
+
patterns: [/^bash$/i],
|
|
2010
|
+
extractScript: (argv) => {
|
|
2011
|
+
const cIdx = argv.findIndex((a) => a === "-c" || a === "-lc");
|
|
2012
|
+
return cIdx !== -1 && argv[cIdx + 1] ? argv[cIdx + 1] : void 0;
|
|
2013
|
+
}
|
|
2014
|
+
},
|
|
2015
|
+
sh: {
|
|
2016
|
+
patterns: [/^sh$/i],
|
|
2017
|
+
extractScript: (argv) => {
|
|
2018
|
+
const cIdx = argv.findIndex((a) => a === "-c");
|
|
2019
|
+
return cIdx !== -1 && argv[cIdx + 1] ? argv[cIdx + 1] : void 0;
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
zsh: {
|
|
2023
|
+
patterns: [/^zsh$/i],
|
|
2024
|
+
extractScript: (argv) => {
|
|
2025
|
+
const cIdx = argv.findIndex((a) => a === "-c");
|
|
2026
|
+
return cIdx !== -1 && argv[cIdx + 1] ? argv[cIdx + 1] : void 0;
|
|
2027
|
+
}
|
|
2028
|
+
},
|
|
2029
|
+
cmd: {
|
|
2030
|
+
patterns: [/^cmd$/i, /^cmd\.exe$/i],
|
|
2031
|
+
extractScript: (argv) => {
|
|
2032
|
+
const cIdx = argv.findIndex((a) => a.toLowerCase() === "/c" || a.toLowerCase() === "/k");
|
|
2033
|
+
return cIdx !== -1 ? argv.slice(cIdx + 1).join(" ") : void 0;
|
|
2034
|
+
}
|
|
2035
|
+
},
|
|
2036
|
+
powershell: {
|
|
2037
|
+
patterns: [/^powershell$/i, /^powershell\.exe$/i],
|
|
2038
|
+
extractScript: (argv) => {
|
|
2039
|
+
const cIdx = argv.findIndex(
|
|
2040
|
+
(a) => a.toLowerCase() === "-command" || a.toLowerCase() === "-c" || a.toLowerCase() === "-encodedcommand" || a.toLowerCase() === "-e"
|
|
2041
|
+
);
|
|
2042
|
+
return cIdx !== -1 && argv[cIdx + 1] ? argv[cIdx + 1] : void 0;
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
pwsh: {
|
|
2046
|
+
patterns: [/^pwsh$/i, /^pwsh\.exe$/i],
|
|
2047
|
+
extractScript: (argv) => {
|
|
2048
|
+
const cIdx = argv.findIndex(
|
|
2049
|
+
(a) => a.toLowerCase() === "-command" || a.toLowerCase() === "-c"
|
|
2050
|
+
);
|
|
2051
|
+
return cIdx !== -1 && argv[cIdx + 1] ? argv[cIdx + 1] : void 0;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
var COMPLEX_SCRIPT_PATTERNS = [
|
|
2056
|
+
/\bif\b.*\bthen\b/i,
|
|
2057
|
+
// if-then
|
|
2058
|
+
/\bfor\b.*\bdo\b/i,
|
|
2059
|
+
// for loops
|
|
2060
|
+
/\bwhile\b.*\bdo\b/i,
|
|
2061
|
+
// while loops
|
|
2062
|
+
/\bfunction\b/i,
|
|
2063
|
+
// function definitions
|
|
2064
|
+
/\$\(/,
|
|
2065
|
+
// command substitution
|
|
2066
|
+
/`[^`]+`/,
|
|
2067
|
+
// backtick substitution
|
|
2068
|
+
/\|\|/,
|
|
2069
|
+
// OR chain
|
|
2070
|
+
/\&\&.*\&\&/,
|
|
2071
|
+
// multiple AND chains
|
|
2072
|
+
/\beval\b/i,
|
|
2073
|
+
// eval
|
|
2074
|
+
/\bexec\b/i,
|
|
2075
|
+
// exec
|
|
2076
|
+
/\bsource\b/i,
|
|
2077
|
+
// source
|
|
2078
|
+
/\.\s+\//,
|
|
2079
|
+
// dot source
|
|
2080
|
+
/<<[<-]?\s*[A-Z]+/
|
|
2081
|
+
// heredocs
|
|
2082
|
+
];
|
|
2083
|
+
function isComplexScript(script) {
|
|
2084
|
+
return COMPLEX_SCRIPT_PATTERNS.some((pattern) => pattern.test(script));
|
|
2085
|
+
}
|
|
2086
|
+
function splitSimpleScript(script) {
|
|
2087
|
+
if (isComplexScript(script)) {
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
const commands = [];
|
|
2091
|
+
const semiParts = script.split(/\s*;\s*/);
|
|
2092
|
+
if (semiParts.length > 1) {
|
|
2093
|
+
for (const part of semiParts) {
|
|
2094
|
+
const trimmed = part.trim();
|
|
2095
|
+
if (trimmed) {
|
|
2096
|
+
commands.push(trimmed);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return commands;
|
|
2100
|
+
}
|
|
2101
|
+
const andParts = script.split(/\s*&&\s*/);
|
|
2102
|
+
if (andParts.length > 1 && andParts.length <= 3) {
|
|
2103
|
+
for (const part of andParts) {
|
|
2104
|
+
const trimmed = part.trim();
|
|
2105
|
+
if (trimmed) {
|
|
2106
|
+
commands.push(trimmed);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return commands;
|
|
2110
|
+
}
|
|
2111
|
+
return [script.trim()];
|
|
2112
|
+
}
|
|
2113
|
+
function detectShellWrapper(argv) {
|
|
2114
|
+
if (argv.length === 0) {
|
|
2115
|
+
return { isWrapper: false, isComplex: false };
|
|
2116
|
+
}
|
|
2117
|
+
const cmd = argv[0];
|
|
2118
|
+
for (const [wrapperType, config] of Object.entries(SHELL_WRAPPERS)) {
|
|
2119
|
+
for (const pattern of config.patterns) {
|
|
2120
|
+
if (pattern.test(cmd)) {
|
|
2121
|
+
const script = config.extractScript(argv);
|
|
2122
|
+
if (!script) {
|
|
2123
|
+
return {
|
|
2124
|
+
isWrapper: true,
|
|
2125
|
+
wrapperType,
|
|
2126
|
+
isComplex: true
|
|
2127
|
+
// Treat interactive shells as complex
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
const isComplex = isComplexScript(script);
|
|
2131
|
+
const subCommands = splitSimpleScript(script);
|
|
2132
|
+
return {
|
|
2133
|
+
isWrapper: true,
|
|
2134
|
+
wrapperType,
|
|
2135
|
+
embeddedScript: script,
|
|
2136
|
+
subCommands: subCommands || void 0,
|
|
2137
|
+
isComplex
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return { isWrapper: false, isComplex: false };
|
|
2143
|
+
}
|
|
2144
|
+
function getMostRestrictiveTier(tiers) {
|
|
2145
|
+
return Math.max(...tiers, 0);
|
|
2146
|
+
}
|
|
2147
|
+
function isHighRiskWrapper(result) {
|
|
2148
|
+
if (result.isComplex) {
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
if (result.embeddedScript?.includes("encodedcommand")) {
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/security/permission-gate.ts
|
|
2158
|
+
var HARD_DENY_PATTERNS = [
|
|
2159
|
+
{ pattern: /\.ssh\/.*(?:id_rsa|id_ed25519|authorized_keys)/i, msg: "SSH key access" },
|
|
2160
|
+
{ pattern: /\/etc\/shadow/i, msg: "Shadow file access" },
|
|
2161
|
+
{ pattern: /\/etc\/passwd/i, msg: "Passwd file access" },
|
|
2162
|
+
{ pattern: /\brm\s+(-[rf]+\s+)*[\/\\]$/i, msg: "Root filesystem deletion" },
|
|
2163
|
+
{ pattern: /\brm\s+(-[rf]+\s+)*~$/i, msg: "Home directory deletion" },
|
|
2164
|
+
{ pattern: /\bcurl\s+[^\|]*\|\s*(bash|sh|zsh)/i, msg: "Piped script execution" },
|
|
2165
|
+
{ pattern: /\bwget\s+[^\|]*\|\s*(bash|sh|zsh)/i, msg: "Piped script execution" },
|
|
2166
|
+
{ pattern: /\bchmod\s+777\s+\//i, msg: "World-writable root" },
|
|
2167
|
+
{ pattern: /\b(sudo|su|runas|doas)\b/i, msg: "Privilege escalation" },
|
|
2168
|
+
{ pattern: /\bformat\s+[a-z]:/i, msg: "Drive format" },
|
|
2169
|
+
{ pattern: /\bmkfs\b/i, msg: "Filesystem creation" },
|
|
2170
|
+
{ pattern: /\bdd\s+.*of=\/dev/i, msg: "Raw device write" }
|
|
2171
|
+
];
|
|
2172
|
+
var TIER_0_COMMANDS = /* @__PURE__ */ new Set([
|
|
2173
|
+
"ls",
|
|
2174
|
+
"dir",
|
|
2175
|
+
"cat",
|
|
2176
|
+
"type",
|
|
2177
|
+
"head",
|
|
2178
|
+
"tail",
|
|
2179
|
+
"less",
|
|
2180
|
+
"more",
|
|
2181
|
+
"pwd",
|
|
2182
|
+
"echo",
|
|
2183
|
+
"find",
|
|
2184
|
+
"grep",
|
|
2185
|
+
"findstr",
|
|
2186
|
+
"which",
|
|
2187
|
+
"where",
|
|
2188
|
+
"wc",
|
|
2189
|
+
"sort",
|
|
2190
|
+
"uniq",
|
|
2191
|
+
"diff",
|
|
2192
|
+
"cmp",
|
|
2193
|
+
"file",
|
|
2194
|
+
"stat"
|
|
2195
|
+
]);
|
|
2196
|
+
var TIER_0_GIT = /* @__PURE__ */ new Set([
|
|
2197
|
+
"status",
|
|
2198
|
+
"diff",
|
|
2199
|
+
"log",
|
|
2200
|
+
"show",
|
|
2201
|
+
"branch",
|
|
2202
|
+
"remote",
|
|
2203
|
+
"tag",
|
|
2204
|
+
"stash",
|
|
2205
|
+
"describe",
|
|
2206
|
+
"rev-parse",
|
|
2207
|
+
"config"
|
|
2208
|
+
]);
|
|
2209
|
+
var TIER_1_COMMANDS = /* @__PURE__ */ new Set([
|
|
2210
|
+
"touch",
|
|
2211
|
+
"mkdir",
|
|
2212
|
+
"cp",
|
|
2213
|
+
"mv",
|
|
2214
|
+
"rm",
|
|
2215
|
+
"rmdir",
|
|
2216
|
+
"ln"
|
|
2217
|
+
]);
|
|
2218
|
+
var TIER_1_GIT = /* @__PURE__ */ new Set([
|
|
2219
|
+
"add",
|
|
2220
|
+
"commit",
|
|
2221
|
+
"checkout",
|
|
2222
|
+
"switch",
|
|
2223
|
+
"merge",
|
|
2224
|
+
"rebase",
|
|
2225
|
+
"reset",
|
|
2226
|
+
"stash",
|
|
2227
|
+
"clean",
|
|
2228
|
+
"restore"
|
|
2229
|
+
]);
|
|
2230
|
+
var TIER_2_COMMANDS = /* @__PURE__ */ new Set([
|
|
2231
|
+
"npm",
|
|
2232
|
+
"yarn",
|
|
2233
|
+
"pnpm",
|
|
2234
|
+
"pip",
|
|
2235
|
+
"pip3",
|
|
2236
|
+
"poetry",
|
|
2237
|
+
"cargo",
|
|
2238
|
+
"node",
|
|
2239
|
+
"python",
|
|
2240
|
+
"python3",
|
|
2241
|
+
"ruby",
|
|
2242
|
+
"go",
|
|
2243
|
+
"java",
|
|
2244
|
+
"make",
|
|
2245
|
+
"cmake",
|
|
2246
|
+
"gcc",
|
|
2247
|
+
"g++",
|
|
2248
|
+
"clang",
|
|
2249
|
+
"docker",
|
|
2250
|
+
"docker-compose",
|
|
2251
|
+
"chmod",
|
|
2252
|
+
"chown"
|
|
2253
|
+
]);
|
|
2254
|
+
function classifyRisk(argv, rawCommand) {
|
|
2255
|
+
if (argv.length === 0) return 3;
|
|
2256
|
+
const cmd = argv[0].toLowerCase();
|
|
2257
|
+
const wrapperResult = detectShellWrapper(argv);
|
|
2258
|
+
if (wrapperResult.isWrapper) {
|
|
2259
|
+
if (isHighRiskWrapper(wrapperResult)) {
|
|
2260
|
+
return 3;
|
|
2261
|
+
}
|
|
2262
|
+
if (wrapperResult.subCommands && wrapperResult.subCommands.length > 0) {
|
|
2263
|
+
const subTiers = wrapperResult.subCommands.map((sc) => {
|
|
2264
|
+
const parsed = parseArgv(sc);
|
|
2265
|
+
return classifyRisk(parsed.argv, sc);
|
|
2266
|
+
});
|
|
2267
|
+
return getMostRestrictiveTier(subTiers);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
for (const { pattern } of HARD_DENY_PATTERNS) {
|
|
2271
|
+
if (pattern.test(rawCommand)) {
|
|
2272
|
+
return 3;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (cmd === "git" && argv.length > 1) {
|
|
2276
|
+
const subCmd = argv[1].toLowerCase();
|
|
2277
|
+
if (TIER_0_GIT.has(subCmd)) return 0;
|
|
2278
|
+
if (TIER_1_GIT.has(subCmd)) return 1;
|
|
2279
|
+
if (subCmd === "push" || subCmd === "pull" || subCmd === "fetch" || subCmd === "clone") {
|
|
2280
|
+
return 2;
|
|
2281
|
+
}
|
|
2282
|
+
return 2;
|
|
2283
|
+
}
|
|
2284
|
+
if (TIER_0_COMMANDS.has(cmd)) return 0;
|
|
2285
|
+
if (TIER_1_COMMANDS.has(cmd)) return 1;
|
|
2286
|
+
if (TIER_2_COMMANDS.has(cmd)) return 2;
|
|
2287
|
+
return 2;
|
|
2288
|
+
}
|
|
2289
|
+
function isWithinWorkspace2(p, workspaceRoot, cwd) {
|
|
2290
|
+
const resolved = resolve2(cwd, p);
|
|
2291
|
+
const root = resolve2(workspaceRoot);
|
|
2292
|
+
const rel = relative2(root, resolved);
|
|
2293
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2294
|
+
}
|
|
2295
|
+
function validatePaths(paths, workspaceRoot, cwd) {
|
|
2296
|
+
const violations = [];
|
|
2297
|
+
for (const p of paths) {
|
|
2298
|
+
if (p.operation === "write" && !isWithinWorkspace2(p.path, workspaceRoot, cwd)) {
|
|
2299
|
+
violations.push({
|
|
2300
|
+
message: `Write to path outside workspace: ${p.path}`,
|
|
2301
|
+
severity: "promptable"
|
|
2302
|
+
// User might intentionally allow
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return violations;
|
|
2307
|
+
}
|
|
2308
|
+
function validateNetwork(domains, policy, allowedDomains) {
|
|
2309
|
+
if (policy === "on" || domains.length === 0) {
|
|
2310
|
+
return [];
|
|
2311
|
+
}
|
|
2312
|
+
const violations = [];
|
|
2313
|
+
if (policy === "off" && domains.length > 0) {
|
|
2314
|
+
violations.push({
|
|
2315
|
+
message: `Network access requested: ${domains.join(", ")}`,
|
|
2316
|
+
severity: "promptable"
|
|
2317
|
+
// User can approve network
|
|
2318
|
+
});
|
|
2319
|
+
return violations;
|
|
2320
|
+
}
|
|
2321
|
+
for (const domain of domains) {
|
|
2322
|
+
const isAllowed = allowedDomains.some(
|
|
2323
|
+
(allowed) => domain === allowed || domain.endsWith("." + allowed)
|
|
2324
|
+
);
|
|
2325
|
+
if (!isAllowed) {
|
|
2326
|
+
violations.push({
|
|
2327
|
+
message: `Domain not in allowlist: ${domain}`,
|
|
2328
|
+
severity: "promptable"
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return violations;
|
|
2333
|
+
}
|
|
2334
|
+
function checkHardViolations(command) {
|
|
2335
|
+
const violations = [];
|
|
2336
|
+
for (const { pattern, msg } of HARD_DENY_PATTERNS) {
|
|
2337
|
+
if (pattern.test(command)) {
|
|
2338
|
+
violations.push({
|
|
2339
|
+
message: msg,
|
|
2340
|
+
severity: "hard"
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return violations;
|
|
2345
|
+
}
|
|
2346
|
+
function evaluateCommand(command, cwd) {
|
|
2347
|
+
const env = getEnvironment();
|
|
2348
|
+
const effectiveCwd = cwd || env.cwd;
|
|
2349
|
+
const parsed = parseArgv(command);
|
|
2350
|
+
const paths = extractPaths(parsed);
|
|
2351
|
+
const domains = extractDomains(command);
|
|
2352
|
+
const ports = extractPorts(command);
|
|
2353
|
+
const wrapperResult = detectShellWrapper(parsed.argv);
|
|
2354
|
+
const riskTier = classifyRisk(parsed.argv, command);
|
|
2355
|
+
const violations = [];
|
|
2356
|
+
violations.push(...checkHardViolations(command));
|
|
2357
|
+
if (env.trustLevel === "untrusted" && riskTier > 0) {
|
|
2358
|
+
return {
|
|
2359
|
+
decision: "prompt",
|
|
2360
|
+
riskTier,
|
|
2361
|
+
command,
|
|
2362
|
+
cwd: effectiveCwd,
|
|
2363
|
+
parsedArgv: parsed.argv,
|
|
2364
|
+
paths,
|
|
2365
|
+
ports,
|
|
2366
|
+
domains,
|
|
2367
|
+
reason: "Workspace is untrusted. Trust this folder to allow file writes and command execution.",
|
|
2368
|
+
violations: [{ message: "Workspace not trusted", severity: "promptable" }],
|
|
2369
|
+
isShellWrapper: wrapperResult.isWrapper,
|
|
2370
|
+
subCommands: wrapperResult.subCommands
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
violations.push(...validatePaths(paths, env.workspaceRoot, effectiveCwd));
|
|
2374
|
+
violations.push(...validateNetwork(domains, env.networkPolicy, env.allowedDomains));
|
|
2375
|
+
const rules = getAllRules();
|
|
2376
|
+
const ruleResult = evaluateRules(parsed.argv, rules);
|
|
2377
|
+
const hasHardViolation = violations.some((v) => v.severity === "hard");
|
|
2378
|
+
const hasPromptableViolation = violations.some((v) => v.severity === "promptable");
|
|
2379
|
+
let decision;
|
|
2380
|
+
let reason;
|
|
2381
|
+
if (hasHardViolation) {
|
|
2382
|
+
decision = "deny";
|
|
2383
|
+
reason = violations.find((v) => v.severity === "hard")?.message || "Security violation";
|
|
2384
|
+
} else if (ruleResult.decision === "forbid") {
|
|
2385
|
+
decision = "deny";
|
|
2386
|
+
reason = ruleResult.mostRestrictive?.description || "Forbidden by rule";
|
|
2387
|
+
} else if (hasPromptableViolation) {
|
|
2388
|
+
decision = "prompt";
|
|
2389
|
+
reason = violations.find((v) => v.severity === "promptable")?.message || "Approval required";
|
|
2390
|
+
} else if (ruleResult.decision === "allow" && riskTier <= 1) {
|
|
2391
|
+
decision = "allow";
|
|
2392
|
+
reason = ruleResult.mostRestrictive?.description || "Allowed by rule";
|
|
2393
|
+
} else {
|
|
2394
|
+
decision = "prompt";
|
|
2395
|
+
reason = getRiskDescription(riskTier, parsed.argv);
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
decision,
|
|
2399
|
+
riskTier,
|
|
2400
|
+
command,
|
|
2401
|
+
cwd: effectiveCwd,
|
|
2402
|
+
parsedArgv: parsed.argv,
|
|
2403
|
+
paths,
|
|
2404
|
+
ports,
|
|
2405
|
+
domains,
|
|
2406
|
+
reason,
|
|
2407
|
+
violations,
|
|
2408
|
+
matchedRuleId: ruleResult.mostRestrictive?.id,
|
|
2409
|
+
isShellWrapper: wrapperResult.isWrapper,
|
|
2410
|
+
subCommands: wrapperResult.subCommands
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function getRiskDescription(tier, argv) {
|
|
2414
|
+
const cmd = argv[0] || "unknown";
|
|
2415
|
+
switch (tier) {
|
|
2416
|
+
case 0:
|
|
2417
|
+
return `Read-only: ${cmd}`;
|
|
2418
|
+
case 1:
|
|
2419
|
+
return `Workspace modification: ${cmd}`;
|
|
2420
|
+
case 2:
|
|
2421
|
+
return `System/package operation: ${cmd}`;
|
|
2422
|
+
case 3:
|
|
2423
|
+
return `Potentially dangerous: ${cmd}`;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/security/workspace-trust.ts
|
|
2428
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
2429
|
+
import chalk9 from "chalk";
|
|
2430
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
2431
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
2432
|
+
import { homedir as homedir4 } from "os";
|
|
2433
|
+
var CONFIG_DIR2 = join3(homedir4(), ".agdi");
|
|
2434
|
+
var TRUST_FILE = join3(CONFIG_DIR2, "trusted-workspaces.json");
|
|
2435
|
+
function loadTrustConfig() {
|
|
2436
|
+
try {
|
|
2437
|
+
if (existsSync5(TRUST_FILE)) {
|
|
2438
|
+
const data = readFileSync4(TRUST_FILE, "utf-8");
|
|
2439
|
+
return JSON.parse(data);
|
|
2440
|
+
}
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2443
|
+
return { trustedWorkspaces: [] };
|
|
2444
|
+
}
|
|
2445
|
+
function saveTrustConfig(config) {
|
|
2446
|
+
try {
|
|
2447
|
+
if (!existsSync5(CONFIG_DIR2)) {
|
|
2448
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
2449
|
+
}
|
|
2450
|
+
writeFileSync3(TRUST_FILE, JSON.stringify(config, null, 2));
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
console.error("Failed to save trust config:", error);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function normalizePath(path4) {
|
|
2456
|
+
return resolve3(path4).toLowerCase().replace(/\\/g, "/");
|
|
2457
|
+
}
|
|
2458
|
+
function trustWorkspace(workspacePath) {
|
|
2459
|
+
const config = loadTrustConfig();
|
|
2460
|
+
const normalized = normalizePath(workspacePath);
|
|
2461
|
+
if (config.trustedWorkspaces.some((w) => w.normalizedPath === normalized)) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
config.trustedWorkspaces.push({
|
|
2465
|
+
path: workspacePath,
|
|
2466
|
+
normalizedPath: normalized,
|
|
2467
|
+
trustedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2468
|
+
});
|
|
2469
|
+
saveTrustConfig(config);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/actions/plan-executor.ts
|
|
2473
|
+
function displayPlanSummary(plan) {
|
|
2474
|
+
const summary = summarizePlan(plan);
|
|
2475
|
+
const boxWidth = 56;
|
|
2476
|
+
console.log("");
|
|
2477
|
+
console.log(chalk10.cyan("\u256D\u2500 Action Plan \u2500" + "\u2500".repeat(boxWidth - 15) + "\u256E"));
|
|
2478
|
+
console.log(chalk10.cyan("\u2502 ") + chalk10.white(`Project: ${plan.projectName}`.padEnd(boxWidth - 2)) + chalk10.cyan(" \u2502"));
|
|
2479
|
+
console.log(chalk10.cyan("\u2502 ") + "\u2500".repeat(boxWidth - 2) + chalk10.cyan(" \u2502"));
|
|
2480
|
+
const lines = [];
|
|
2481
|
+
if (summary.dirsCreated > 0) {
|
|
2482
|
+
lines.push(`\u{1F4C1} Create ${summary.dirsCreated} directories`);
|
|
2483
|
+
}
|
|
2484
|
+
if (summary.filesCreated > 0) {
|
|
2485
|
+
lines.push(`\u{1F4C4} Create ${summary.filesCreated} files`);
|
|
2486
|
+
}
|
|
2487
|
+
if (summary.filesDeleted > 0) {
|
|
2488
|
+
lines.push(`\u{1F5D1}\uFE0F Delete ${summary.filesDeleted} files`);
|
|
2489
|
+
}
|
|
2490
|
+
if (summary.commandsToRun > 0) {
|
|
2491
|
+
lines.push(`\u26A1 Run ${summary.commandsToRun} commands`);
|
|
2492
|
+
}
|
|
2493
|
+
if (summary.domains.length > 0) {
|
|
2494
|
+
lines.push(`\u{1F310} Network: ${summary.domains.join(", ")}`);
|
|
2495
|
+
}
|
|
2496
|
+
if (summary.ports.length > 0) {
|
|
2497
|
+
lines.push(`\u{1F50C} Ports: ${summary.ports.join(", ")}`);
|
|
2498
|
+
}
|
|
2499
|
+
for (const line of lines) {
|
|
2500
|
+
console.log(chalk10.cyan("\u2502 ") + chalk10.gray(line.padEnd(boxWidth - 2)) + chalk10.cyan(" \u2502"));
|
|
2501
|
+
}
|
|
2502
|
+
console.log(chalk10.cyan("\u2570" + "\u2500".repeat(boxWidth) + "\u256F"));
|
|
2503
|
+
console.log("");
|
|
2504
|
+
}
|
|
2505
|
+
function displayActionProgress(action, index, total) {
|
|
2506
|
+
const num = `[${index + 1}/${total}]`;
|
|
2507
|
+
switch (action.type) {
|
|
2508
|
+
case "mkdir":
|
|
2509
|
+
console.log(chalk10.gray(`${num} Creating directory: ${action.path}`));
|
|
2510
|
+
break;
|
|
2511
|
+
case "writeFile":
|
|
2512
|
+
console.log(chalk10.gray(`${num} Writing file: ${action.path}`));
|
|
2513
|
+
break;
|
|
2514
|
+
case "deleteFile":
|
|
2515
|
+
console.log(chalk10.gray(`${num} Deleting file: ${action.path}`));
|
|
2516
|
+
break;
|
|
2517
|
+
case "exec":
|
|
2518
|
+
console.log(chalk10.blue(`${num} Running: ${action.argv.join(" ")}`));
|
|
2519
|
+
break;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
async function dryRunActions(plan) {
|
|
2523
|
+
const gateResults = [];
|
|
2524
|
+
let requiresTrust = false;
|
|
2525
|
+
let hasHardDeny = false;
|
|
2526
|
+
for (const action of plan.actions) {
|
|
2527
|
+
if (action.type === "exec") {
|
|
2528
|
+
const command = action.argv.join(" ");
|
|
2529
|
+
const result = evaluateCommand(command, action.cwd);
|
|
2530
|
+
gateResults.push(result);
|
|
2531
|
+
if (result.decision === "deny") {
|
|
2532
|
+
const isHard = result.violations.some((v) => v.severity === "hard");
|
|
2533
|
+
if (isHard) {
|
|
2534
|
+
hasHardDeny = true;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
if (result.violations.some((v) => v.message === "Workspace not trusted")) {
|
|
2538
|
+
requiresTrust = true;
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
const env = getEnvironment();
|
|
2542
|
+
if (env.trustLevel === "untrusted") {
|
|
2543
|
+
requiresTrust = true;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
canProceed: !hasHardDeny,
|
|
2549
|
+
requiresTrust,
|
|
2550
|
+
gateResults
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
async function executeAction(action) {
|
|
2554
|
+
const env = getEnvironment();
|
|
2555
|
+
switch (action.type) {
|
|
2556
|
+
case "mkdir": {
|
|
2557
|
+
const result = await mkdirTool(action.path);
|
|
2558
|
+
return {
|
|
2559
|
+
action,
|
|
2560
|
+
success: result.success,
|
|
2561
|
+
error: result.error
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
case "writeFile": {
|
|
2565
|
+
const result = await writeFileTool(action.path, action.content);
|
|
2566
|
+
return {
|
|
2567
|
+
action,
|
|
2568
|
+
success: result.success,
|
|
2569
|
+
error: result.error
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
case "deleteFile": {
|
|
2573
|
+
const result = await deleteFileTool(action.path);
|
|
2574
|
+
return {
|
|
2575
|
+
action,
|
|
2576
|
+
success: result.success,
|
|
2577
|
+
error: result.error
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
case "exec": {
|
|
2581
|
+
const cwd = action.cwd ? resolve4(env.workspaceRoot, action.cwd) : env.workspaceRoot;
|
|
2582
|
+
const command = action.argv.join(" ");
|
|
2583
|
+
return new Promise((resolvePromise) => {
|
|
2584
|
+
let output = "";
|
|
2585
|
+
let error = "";
|
|
2586
|
+
const child = spawn2(action.argv[0], action.argv.slice(1), {
|
|
2587
|
+
cwd,
|
|
2588
|
+
shell: true,
|
|
2589
|
+
stdio: "pipe"
|
|
2590
|
+
});
|
|
2591
|
+
child.stdout?.on("data", (data) => {
|
|
2592
|
+
const text = data.toString();
|
|
2593
|
+
output += text;
|
|
2594
|
+
process.stdout.write(text);
|
|
2595
|
+
});
|
|
2596
|
+
child.stderr?.on("data", (data) => {
|
|
2597
|
+
const text = data.toString();
|
|
2598
|
+
error += text;
|
|
2599
|
+
process.stderr.write(text);
|
|
2600
|
+
});
|
|
2601
|
+
child.on("close", (code) => {
|
|
2602
|
+
resolvePromise({
|
|
2603
|
+
action,
|
|
2604
|
+
success: code === 0,
|
|
2605
|
+
error: code !== 0 ? error || `Exit code: ${code}` : void 0,
|
|
2606
|
+
output
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
child.on("error", (err) => {
|
|
2610
|
+
resolvePromise({
|
|
2611
|
+
action,
|
|
2612
|
+
success: false,
|
|
2613
|
+
error: err.message
|
|
2614
|
+
});
|
|
2615
|
+
});
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
async function executePlan(plan) {
|
|
2621
|
+
const env = getEnvironment();
|
|
2622
|
+
const results = [];
|
|
2623
|
+
const filesCreated = [];
|
|
2624
|
+
const commandsRun = [];
|
|
2625
|
+
const errors = [];
|
|
2626
|
+
displayPlanSummary(plan);
|
|
2627
|
+
const dryRun = await dryRunActions(plan);
|
|
2628
|
+
if (!dryRun.canProceed) {
|
|
2629
|
+
console.log(chalk10.red("\n\u26D4 Plan contains actions that cannot be approved:"));
|
|
2630
|
+
for (const result of dryRun.gateResults) {
|
|
2631
|
+
if (result.decision === "deny") {
|
|
2632
|
+
console.log(chalk10.red(` - ${result.command}: ${result.reason}`));
|
|
2633
|
+
errors.push(result.reason);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return { success: false, results, filesCreated, commandsRun, errors };
|
|
2637
|
+
}
|
|
2638
|
+
if (dryRun.requiresTrust) {
|
|
2639
|
+
console.log(chalk10.yellow("\u26A0\uFE0F This workspace is not trusted."));
|
|
2640
|
+
console.log(chalk10.gray(" The plan requires file writes and command execution.\n"));
|
|
2641
|
+
const trustChoice = await select4({
|
|
2642
|
+
message: "Trust this workspace?",
|
|
2643
|
+
choices: [
|
|
2644
|
+
{ name: "Trust for this session", value: "session" },
|
|
2645
|
+
{ name: "Trust and remember", value: "persistent" },
|
|
2646
|
+
{ name: "Cancel", value: "cancel" }
|
|
2647
|
+
]
|
|
2648
|
+
});
|
|
2649
|
+
if (trustChoice === "cancel") {
|
|
2650
|
+
console.log(chalk10.yellow("\n\u{1F44B} Plan cancelled.\n"));
|
|
2651
|
+
return { success: false, results, filesCreated, commandsRun, errors: ["User cancelled"] };
|
|
2652
|
+
}
|
|
2653
|
+
if (trustChoice === "persistent") {
|
|
2654
|
+
trustWorkspace(env.workspaceRoot);
|
|
2655
|
+
updateEnvironment({ trustLevel: "persistent" });
|
|
2656
|
+
console.log(chalk10.green("\u2713 Workspace trusted and remembered\n"));
|
|
2657
|
+
} else {
|
|
2658
|
+
updateEnvironment({ trustLevel: "session" });
|
|
2659
|
+
console.log(chalk10.green("\u2713 Trusted for this session\n"));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const approved = await confirm({
|
|
2663
|
+
message: `Execute ${plan.actions.length} actions?`,
|
|
2664
|
+
default: true
|
|
2665
|
+
});
|
|
2666
|
+
if (!approved) {
|
|
2667
|
+
console.log(chalk10.yellow("\n\u{1F44B} Plan cancelled.\n"));
|
|
2668
|
+
return { success: false, results, filesCreated, commandsRun, errors: ["User cancelled"] };
|
|
2669
|
+
}
|
|
2670
|
+
logEvent({
|
|
2671
|
+
eventType: "command_start",
|
|
2672
|
+
command: `executePlan: ${plan.projectName}`,
|
|
2673
|
+
metadata: {
|
|
2674
|
+
actionsCount: plan.actions.length,
|
|
2675
|
+
summary: summarizePlan(plan)
|
|
2676
|
+
}
|
|
2677
|
+
});
|
|
2678
|
+
console.log(chalk10.cyan("\n\u25B6 Executing plan...\n"));
|
|
2679
|
+
for (let i = 0; i < plan.actions.length; i++) {
|
|
2680
|
+
const action = plan.actions[i];
|
|
2681
|
+
displayActionProgress(action, i, plan.actions.length);
|
|
2682
|
+
const result = await executeAction(action);
|
|
2683
|
+
results.push(result);
|
|
2684
|
+
if (result.success) {
|
|
2685
|
+
if (action.type === "writeFile" || action.type === "mkdir") {
|
|
2686
|
+
filesCreated.push(action.path);
|
|
2687
|
+
}
|
|
2688
|
+
if (action.type === "exec") {
|
|
2689
|
+
commandsRun.push(action.argv.join(" "));
|
|
2690
|
+
}
|
|
2691
|
+
} else {
|
|
2692
|
+
errors.push(result.error || "Unknown error");
|
|
2693
|
+
console.log(chalk10.red(` \u2717 Failed: ${result.error}`));
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
const success = errors.length === 0;
|
|
2697
|
+
if (success) {
|
|
2698
|
+
console.log(chalk10.green(`
|
|
2699
|
+
\u2713 Plan executed successfully!`));
|
|
2700
|
+
console.log(chalk10.gray(` Created ${filesCreated.length} files`));
|
|
2701
|
+
console.log(chalk10.gray(` Ran ${commandsRun.length} commands
|
|
2702
|
+
`));
|
|
2703
|
+
} else {
|
|
2704
|
+
console.log(chalk10.red(`
|
|
2705
|
+
\u2717 Plan completed with ${errors.length} errors
|
|
2706
|
+
`));
|
|
2707
|
+
}
|
|
2708
|
+
if (plan.nextSteps) {
|
|
2709
|
+
console.log(chalk10.cyan("Next steps:"));
|
|
2710
|
+
console.log(chalk10.gray(` ${plan.nextSteps}
|
|
2711
|
+
`));
|
|
2712
|
+
}
|
|
2713
|
+
logEvent({
|
|
2714
|
+
eventType: "command_result",
|
|
2715
|
+
command: `executePlan: ${plan.projectName}`,
|
|
2716
|
+
result: {
|
|
2717
|
+
exitCode: success ? 0 : 1
|
|
2718
|
+
},
|
|
2719
|
+
metadata: {
|
|
2720
|
+
filesCreated,
|
|
2721
|
+
commandsRun,
|
|
2722
|
+
errors
|
|
2723
|
+
}
|
|
2724
|
+
});
|
|
2725
|
+
return { success, results, filesCreated, commandsRun, errors };
|
|
2726
|
+
}
|
|
2727
|
+
async function parseAndExecutePlan(response) {
|
|
2728
|
+
const plan = parseActionPlan(response);
|
|
2729
|
+
if (!plan) {
|
|
2730
|
+
console.log(chalk10.yellow("\n\u26A0\uFE0F Could not parse action plan from response.\n"));
|
|
2731
|
+
return null;
|
|
2732
|
+
}
|
|
2733
|
+
return executePlan(plan);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// src/commands/codex.ts
|
|
2737
|
+
var CHAT_SYSTEM_PROMPT = `You are Agdi dev, an elite AI coding assistant. You help developers write code, debug issues, and build applications.
|
|
1278
2738
|
|
|
1279
2739
|
## Your Capabilities
|
|
1280
2740
|
- Write complete, production-ready code
|
|
1281
2741
|
- Debug and fix code issues
|
|
1282
2742
|
- Explain code and architecture
|
|
1283
|
-
-
|
|
1284
|
-
- Run shell commands (when requested)
|
|
2743
|
+
- Answer coding questions
|
|
1285
2744
|
|
|
1286
2745
|
## Response Style
|
|
1287
2746
|
- Be concise and direct
|
|
@@ -1293,31 +2752,66 @@ var SYSTEM_PROMPT3 = `You are Agdi dev, an elite AI coding assistant. You help d
|
|
|
1293
2752
|
- Use TypeScript by default
|
|
1294
2753
|
- Follow modern best practices
|
|
1295
2754
|
- Include proper error handling
|
|
1296
|
-
- Write self-documenting code
|
|
2755
|
+
- Write self-documenting code`;
|
|
2756
|
+
var BUILD_SYSTEM_PROMPT = `You are Agdi dev, an AI coding assistant that generates applications.
|
|
2757
|
+
|
|
2758
|
+
## CRITICAL: Output Format
|
|
2759
|
+
When the user asks to build an app, do NOT print file contents in markdown blocks.
|
|
2760
|
+
|
|
2761
|
+
Instead, output a single JSON object with this exact structure:
|
|
2762
|
+
{
|
|
2763
|
+
"projectName": "my-app",
|
|
2764
|
+
"actions": [
|
|
2765
|
+
{ "type": "mkdir", "path": "src" },
|
|
2766
|
+
{ "type": "writeFile", "path": "package.json", "content": "{...}" },
|
|
2767
|
+
{ "type": "writeFile", "path": "src/index.ts", "content": "..." },
|
|
2768
|
+
{ "type": "exec", "argv": ["pnpm", "install"], "cwd": "." },
|
|
2769
|
+
{ "type": "exec", "argv": ["pnpm", "dev"], "cwd": "." }
|
|
2770
|
+
],
|
|
2771
|
+
"nextSteps": "Open http://localhost:3000 to see your app"
|
|
2772
|
+
}
|
|
1297
2773
|
|
|
1298
|
-
|
|
2774
|
+
## Action Types
|
|
2775
|
+
- mkdir: { type: "mkdir", path: "relative/path" }
|
|
2776
|
+
- writeFile: { type: "writeFile", path: "relative/path/file.ts", content: "file contents" }
|
|
2777
|
+
- deleteFile: { type: "deleteFile", path: "relative/path/file.ts" }
|
|
2778
|
+
- exec: { type: "exec", argv: ["command", "arg1", "arg2"], cwd: "." }
|
|
2779
|
+
|
|
2780
|
+
## Rules
|
|
2781
|
+
1. All paths MUST be relative to workspace root (no leading /)
|
|
2782
|
+
2. Use pnpm as package manager
|
|
2783
|
+
3. Include exec steps for install/dev only if user requests running the app
|
|
2784
|
+
4. Keep actions minimal (no redundant writes)
|
|
2785
|
+
5. Create complete, production-ready code
|
|
2786
|
+
6. Use TypeScript by default
|
|
2787
|
+
7. Include proper error handling
|
|
2788
|
+
8. Output ONLY the JSON object, no extra text`;
|
|
1299
2789
|
async function startCodingMode() {
|
|
1300
2790
|
const activeConfig = getActiveProvider();
|
|
1301
2791
|
if (!activeConfig) {
|
|
1302
|
-
console.log(
|
|
2792
|
+
console.log(chalk11.red("\u274C No API key configured. Run: agdi"));
|
|
1303
2793
|
return;
|
|
1304
2794
|
}
|
|
1305
2795
|
const { provider, apiKey, model } = activeConfig;
|
|
1306
2796
|
const config = loadConfig();
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
console.log(
|
|
2797
|
+
const env = initSession();
|
|
2798
|
+
displaySessionHeader(env);
|
|
2799
|
+
logSessionStart(env.workspaceRoot, env.trustLevel);
|
|
2800
|
+
console.log(chalk11.cyan.bold("\u26A1 Agdi dev\n"));
|
|
2801
|
+
console.log(chalk11.gray(`Model: ${chalk11.cyan(model)}`));
|
|
2802
|
+
console.log(chalk11.gray("Commands: /model, /chat, /build, /help, /exit\n"));
|
|
2803
|
+
console.log(chalk11.gray("\u2500".repeat(50) + "\n"));
|
|
1311
2804
|
const pm = new ProjectManager();
|
|
1312
2805
|
let llm = createLLMProvider(provider, { apiKey, model });
|
|
1313
2806
|
while (true) {
|
|
1314
2807
|
try {
|
|
1315
2808
|
const userInput = await input4({
|
|
1316
|
-
message:
|
|
2809
|
+
message: chalk11.green("\u2192")
|
|
1317
2810
|
});
|
|
1318
2811
|
const trimmed = userInput.trim().toLowerCase();
|
|
1319
2812
|
if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit") {
|
|
1320
|
-
|
|
2813
|
+
logSessionEnd();
|
|
2814
|
+
console.log(chalk11.gray("\n\u{1F44B} Goodbye!\n"));
|
|
1321
2815
|
break;
|
|
1322
2816
|
}
|
|
1323
2817
|
if (trimmed === "/help") {
|
|
@@ -1332,22 +2826,22 @@ async function startCodingMode() {
|
|
|
1332
2826
|
apiKey: newConfig.apiKey,
|
|
1333
2827
|
model: newConfig.model
|
|
1334
2828
|
});
|
|
1335
|
-
console.log(
|
|
2829
|
+
console.log(chalk11.gray(`Now using: ${chalk11.cyan(newConfig.model)}
|
|
1336
2830
|
`));
|
|
1337
2831
|
}
|
|
1338
2832
|
continue;
|
|
1339
2833
|
}
|
|
1340
2834
|
if (trimmed === "/chat") {
|
|
1341
|
-
console.log(
|
|
2835
|
+
console.log(chalk11.gray("\nSwitching to chat mode. Type /code to return.\n"));
|
|
1342
2836
|
await chatMode(llm);
|
|
1343
2837
|
continue;
|
|
1344
2838
|
}
|
|
1345
2839
|
if (trimmed.startsWith("/build ") || trimmed.startsWith("build ")) {
|
|
1346
2840
|
const prompt = userInput.replace(/^\/?build\s+/i, "").trim();
|
|
1347
2841
|
if (prompt) {
|
|
1348
|
-
await
|
|
2842
|
+
await buildAppWithPlan(prompt, llm);
|
|
1349
2843
|
} else {
|
|
1350
|
-
console.log(
|
|
2844
|
+
console.log(chalk11.yellow("\nUsage: /build <description>\n"));
|
|
1351
2845
|
}
|
|
1352
2846
|
continue;
|
|
1353
2847
|
}
|
|
@@ -1356,12 +2850,12 @@ async function startCodingMode() {
|
|
|
1356
2850
|
}
|
|
1357
2851
|
const isGenerationRequest = /^(create|build|make|generate|design|implement)\s+(me\s+)?(a\s+|an\s+)?/i.test(userInput.trim());
|
|
1358
2852
|
if (isGenerationRequest) {
|
|
1359
|
-
await
|
|
2853
|
+
await buildAppWithPlan(userInput, llm);
|
|
1360
2854
|
continue;
|
|
1361
2855
|
}
|
|
1362
2856
|
const spinner = ora3("Thinking...").start();
|
|
1363
2857
|
try {
|
|
1364
|
-
const response = await llm.generate(userInput,
|
|
2858
|
+
const response = await llm.generate(userInput, CHAT_SYSTEM_PROMPT);
|
|
1365
2859
|
spinner.stop();
|
|
1366
2860
|
console.log("\n" + formatResponse(response.text) + "\n");
|
|
1367
2861
|
} catch (error) {
|
|
@@ -1370,43 +2864,23 @@ async function startCodingMode() {
|
|
|
1370
2864
|
}
|
|
1371
2865
|
} catch (error) {
|
|
1372
2866
|
if (error.name === "ExitPromptError") {
|
|
1373
|
-
|
|
2867
|
+
logSessionEnd();
|
|
2868
|
+
console.log(chalk11.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
1374
2869
|
process.exit(0);
|
|
1375
2870
|
}
|
|
1376
2871
|
throw error;
|
|
1377
2872
|
}
|
|
1378
2873
|
}
|
|
1379
2874
|
}
|
|
1380
|
-
async function
|
|
1381
|
-
const spinner = ora3("Generating
|
|
2875
|
+
async function buildAppWithPlan(prompt, llm) {
|
|
2876
|
+
const spinner = ora3("Generating action plan...").start();
|
|
1382
2877
|
try {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
console.log(chalk8.gray("\n\u{1F4C1} Files:"));
|
|
1390
|
-
for (const file of files.slice(0, 10)) {
|
|
1391
|
-
console.log(chalk8.gray(` ${file.path}`));
|
|
1392
|
-
}
|
|
1393
|
-
if (files.length > 10) {
|
|
1394
|
-
console.log(chalk8.gray(` ... and ${files.length - 10} more`));
|
|
1395
|
-
}
|
|
1396
|
-
const shouldWrite = await input4({
|
|
1397
|
-
message: "Write to disk? (y/n):",
|
|
1398
|
-
default: "y"
|
|
1399
|
-
});
|
|
1400
|
-
if (shouldWrite.toLowerCase() === "y") {
|
|
1401
|
-
const dir = await input4({
|
|
1402
|
-
message: "Output directory:",
|
|
1403
|
-
default: "./generated-app"
|
|
1404
|
-
});
|
|
1405
|
-
await writeProject(pm.get(), dir);
|
|
1406
|
-
console.log(chalk8.green(`
|
|
1407
|
-
\u2705 Written to ${dir}
|
|
1408
|
-
`));
|
|
1409
|
-
console.log(chalk8.gray("Next: cd " + dir + " && npm install && npm run dev\n"));
|
|
2878
|
+
const response = await llm.generate(prompt, BUILD_SYSTEM_PROMPT);
|
|
2879
|
+
spinner.stop();
|
|
2880
|
+
const result = await parseAndExecutePlan(response.text);
|
|
2881
|
+
if (!result) {
|
|
2882
|
+
console.log(chalk11.yellow("\n\u26A0\uFE0F Model did not return an action plan. Showing response:\n"));
|
|
2883
|
+
console.log(formatResponse(response.text) + "\n");
|
|
1410
2884
|
}
|
|
1411
2885
|
} catch (error) {
|
|
1412
2886
|
spinner.fail("Generation failed");
|
|
@@ -1417,10 +2891,10 @@ async function chatMode(llm) {
|
|
|
1417
2891
|
while (true) {
|
|
1418
2892
|
try {
|
|
1419
2893
|
const userInput = await input4({
|
|
1420
|
-
message:
|
|
2894
|
+
message: chalk11.blue("\u{1F4AC}")
|
|
1421
2895
|
});
|
|
1422
2896
|
if (userInput.toLowerCase() === "/code" || userInput.toLowerCase() === "/exit") {
|
|
1423
|
-
console.log(
|
|
2897
|
+
console.log(chalk11.gray("\nBack to Agdi dev mode.\n"));
|
|
1424
2898
|
return;
|
|
1425
2899
|
}
|
|
1426
2900
|
if (!userInput.trim()) continue;
|
|
@@ -1438,48 +2912,49 @@ async function chatMode(llm) {
|
|
|
1438
2912
|
}
|
|
1439
2913
|
function formatResponse(text) {
|
|
1440
2914
|
return text.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
1441
|
-
const header = lang ?
|
|
2915
|
+
const header = lang ? chalk11.gray(`\u2500\u2500 ${lang} \u2500\u2500`) : chalk11.gray("\u2500\u2500 code \u2500\u2500");
|
|
1442
2916
|
return `
|
|
1443
2917
|
${header}
|
|
1444
|
-
${
|
|
1445
|
-
${
|
|
2918
|
+
${chalk11.white(code.trim())}
|
|
2919
|
+
${chalk11.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
1446
2920
|
`;
|
|
1447
2921
|
});
|
|
1448
2922
|
}
|
|
1449
2923
|
function showHelp() {
|
|
1450
|
-
console.log(
|
|
1451
|
-
console.log(
|
|
1452
|
-
console.log(
|
|
1453
|
-
console.log(
|
|
1454
|
-
console.log(
|
|
1455
|
-
console.log(
|
|
1456
|
-
console.log(
|
|
2924
|
+
console.log(chalk11.cyan.bold("\n\u{1F4D6} Commands\n"));
|
|
2925
|
+
console.log(chalk11.gray(" /build ") + "Generate and execute an application");
|
|
2926
|
+
console.log(chalk11.gray(" /model ") + "Change AI model");
|
|
2927
|
+
console.log(chalk11.gray(" /chat ") + "Switch to chat mode");
|
|
2928
|
+
console.log(chalk11.gray(" /help ") + "Show this help");
|
|
2929
|
+
console.log(chalk11.gray(" /exit ") + "Exit Agdi");
|
|
2930
|
+
console.log(chalk11.gray("\n Or just type your coding question!\n"));
|
|
2931
|
+
console.log(chalk11.gray('Tip: "Create a todo app" will generate & write files.\n'));
|
|
1457
2932
|
}
|
|
1458
2933
|
function handleError(error) {
|
|
1459
2934
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1460
2935
|
if (msg.includes("429") || msg.includes("quota")) {
|
|
1461
|
-
console.log(
|
|
2936
|
+
console.log(chalk11.yellow("\n\u26A0\uFE0F Quota exceeded. Run /model to switch.\n"));
|
|
1462
2937
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
1463
|
-
console.log(
|
|
2938
|
+
console.log(chalk11.red("\n\u{1F511} Invalid API key. Run: agdi auth\n"));
|
|
1464
2939
|
} else {
|
|
1465
|
-
console.log(
|
|
2940
|
+
console.log(chalk11.red("\n" + msg + "\n"));
|
|
1466
2941
|
}
|
|
1467
2942
|
}
|
|
1468
2943
|
|
|
1469
2944
|
// src/index.ts
|
|
1470
2945
|
var BANNER = `
|
|
1471
|
-
${
|
|
1472
|
-
${
|
|
1473
|
-
${
|
|
1474
|
-
${
|
|
1475
|
-
${
|
|
1476
|
-
${
|
|
2946
|
+
${chalk12.cyan(` ___ __ _ `)}
|
|
2947
|
+
${chalk12.cyan(` / | ____ _____/ /(_) `)}
|
|
2948
|
+
${chalk12.cyan(` / /| | / __ \`/ __ // / `)}
|
|
2949
|
+
${chalk12.cyan(` / ___ |/ /_/ / /_/ // / `)}
|
|
2950
|
+
${chalk12.cyan(`/_/ |_|\\_, /\\__,_//_/ `)}
|
|
2951
|
+
${chalk12.cyan(` /____/ `)}
|
|
1477
2952
|
`;
|
|
1478
2953
|
var program = new Command();
|
|
1479
|
-
program.name("agdi").description(
|
|
2954
|
+
program.name("agdi").description(chalk12.cyan("\u{1F680} AI-powered coding assistant")).version("2.1.0").configureHelp({
|
|
1480
2955
|
// Show banner only when help is requested
|
|
1481
2956
|
formatHelp: (cmd, helper) => {
|
|
1482
|
-
return BANNER + "\n" +
|
|
2957
|
+
return BANNER + "\n" + chalk12.gray(" The Open Source AI Architect") + "\n" + chalk12.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);
|
|
1483
2958
|
}
|
|
1484
2959
|
});
|
|
1485
2960
|
program.action(async () => {
|
|
@@ -1490,7 +2965,7 @@ program.action(async () => {
|
|
|
1490
2965
|
await startCodingMode();
|
|
1491
2966
|
} catch (error) {
|
|
1492
2967
|
if (error.name === "ExitPromptError") {
|
|
1493
|
-
console.log(
|
|
2968
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
1494
2969
|
process.exit(0);
|
|
1495
2970
|
}
|
|
1496
2971
|
throw error;
|
|
@@ -1505,7 +2980,7 @@ program.command("auth").description("Configure API keys").option("--status", "Sh
|
|
|
1505
2980
|
}
|
|
1506
2981
|
} catch (error) {
|
|
1507
2982
|
if (error.name === "ExitPromptError") {
|
|
1508
|
-
console.log(
|
|
2983
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
1509
2984
|
process.exit(0);
|
|
1510
2985
|
}
|
|
1511
2986
|
throw error;
|
|
@@ -1516,7 +2991,7 @@ program.command("model").alias("models").description("Change AI model").action(a
|
|
|
1516
2991
|
await selectModel();
|
|
1517
2992
|
} catch (error) {
|
|
1518
2993
|
if (error.name === "ExitPromptError") {
|
|
1519
|
-
console.log(
|
|
2994
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
1520
2995
|
process.exit(0);
|
|
1521
2996
|
}
|
|
1522
2997
|
throw error;
|
|
@@ -1530,7 +3005,7 @@ program.command("chat").description("Start a chat session").action(async () => {
|
|
|
1530
3005
|
await startChat();
|
|
1531
3006
|
} catch (error) {
|
|
1532
3007
|
if (error.name === "ExitPromptError") {
|
|
1533
|
-
console.log(
|
|
3008
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Goodbye!\n"));
|
|
1534
3009
|
process.exit(0);
|
|
1535
3010
|
}
|
|
1536
3011
|
throw error;
|
|
@@ -1541,7 +3016,7 @@ program.command("run [directory]").description("Run a generated project").action
|
|
|
1541
3016
|
await runProject(directory);
|
|
1542
3017
|
} catch (error) {
|
|
1543
3018
|
if (error.name === "ExitPromptError") {
|
|
1544
|
-
console.log(
|
|
3019
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
1545
3020
|
process.exit(0);
|
|
1546
3021
|
}
|
|
1547
3022
|
throw error;
|
|
@@ -1554,7 +3029,7 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
1554
3029
|
}
|
|
1555
3030
|
const activeConfig = getActiveProvider();
|
|
1556
3031
|
if (!activeConfig) {
|
|
1557
|
-
console.log(
|
|
3032
|
+
console.log(chalk12.red("\u274C No API key configured. Run: agdi auth"));
|
|
1558
3033
|
return;
|
|
1559
3034
|
}
|
|
1560
3035
|
const spinner = ora4("Generating application...").start();
|
|
@@ -1566,30 +3041,30 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
1566
3041
|
const pm = new ProjectManager();
|
|
1567
3042
|
pm.create(options.output.replace("./", ""), prompt);
|
|
1568
3043
|
const { plan, files } = await generateApp(prompt, llm, (step, file) => {
|
|
1569
|
-
spinner.text = file ? `${step} ${
|
|
3044
|
+
spinner.text = file ? `${step} ${chalk12.gray(file)}` : step;
|
|
1570
3045
|
});
|
|
1571
3046
|
pm.updateFiles(files);
|
|
1572
3047
|
pm.updateDependencies(plan.dependencies);
|
|
1573
3048
|
await writeProject(pm.get(), options.output);
|
|
1574
|
-
spinner.succeed(
|
|
1575
|
-
console.log(
|
|
1576
|
-
\u{1F4C1} Created ${files.length} files in ${
|
|
1577
|
-
console.log(
|
|
3049
|
+
spinner.succeed(chalk12.green("App generated!"));
|
|
3050
|
+
console.log(chalk12.gray(`
|
|
3051
|
+
\u{1F4C1} Created ${files.length} files in ${chalk12.cyan(options.output)}`));
|
|
3052
|
+
console.log(chalk12.gray("\nNext: cd " + options.output + " && npm install && npm run dev\n"));
|
|
1578
3053
|
} catch (error) {
|
|
1579
3054
|
spinner.fail("Generation failed");
|
|
1580
3055
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1581
3056
|
if (msg.includes("429") || msg.includes("quota")) {
|
|
1582
|
-
console.log(
|
|
3057
|
+
console.log(chalk12.yellow("\n\u26A0\uFE0F Quota exceeded. Run: agdi model\n"));
|
|
1583
3058
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
1584
|
-
console.log(
|
|
3059
|
+
console.log(chalk12.red("\n\u{1F511} Invalid API key. Run: agdi auth\n"));
|
|
1585
3060
|
} else {
|
|
1586
|
-
console.error(
|
|
3061
|
+
console.error(chalk12.red("\n" + msg + "\n"));
|
|
1587
3062
|
}
|
|
1588
3063
|
process.exit(1);
|
|
1589
3064
|
}
|
|
1590
3065
|
} catch (error) {
|
|
1591
3066
|
if (error.name === "ExitPromptError") {
|
|
1592
|
-
console.log(
|
|
3067
|
+
console.log(chalk12.gray("\n\n\u{1F44B} Cancelled.\n"));
|
|
1593
3068
|
process.exit(0);
|
|
1594
3069
|
}
|
|
1595
3070
|
throw error;
|
|
@@ -1598,11 +3073,11 @@ program.command("build <prompt>").alias("b").description("Generate an app from a
|
|
|
1598
3073
|
program.command("config").description("Show configuration").action(async () => {
|
|
1599
3074
|
const config = loadConfig();
|
|
1600
3075
|
const active = getActiveProvider();
|
|
1601
|
-
console.log(
|
|
1602
|
-
console.log(
|
|
1603
|
-
console.log(
|
|
1604
|
-
console.log(
|
|
1605
|
-
console.log(
|
|
3076
|
+
console.log(chalk12.cyan.bold("\n\u2699\uFE0F Configuration\n"));
|
|
3077
|
+
console.log(chalk12.gray(" Provider: ") + chalk12.cyan(config.defaultProvider || "not set"));
|
|
3078
|
+
console.log(chalk12.gray(" Model: ") + chalk12.cyan(config.defaultModel || "not set"));
|
|
3079
|
+
console.log(chalk12.gray(" Config: ") + chalk12.gray("~/.agdi/config.json"));
|
|
3080
|
+
console.log(chalk12.cyan.bold("\n\u{1F510} API Keys\n"));
|
|
1606
3081
|
const keys = [
|
|
1607
3082
|
["Gemini", config.geminiApiKey],
|
|
1608
3083
|
["OpenRouter", config.openrouterApiKey],
|
|
@@ -1611,7 +3086,7 @@ program.command("config").description("Show configuration").action(async () => {
|
|
|
1611
3086
|
["DeepSeek", config.deepseekApiKey]
|
|
1612
3087
|
];
|
|
1613
3088
|
for (const [name, key] of keys) {
|
|
1614
|
-
const status = key ?
|
|
3089
|
+
const status = key ? chalk12.green("\u2713") : chalk12.gray("\u2717");
|
|
1615
3090
|
console.log(` ${status} ${name}`);
|
|
1616
3091
|
}
|
|
1617
3092
|
console.log("");
|