@pikaa-ai/pikaa 0.3.27 → 0.3.28
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/cli.js +2010 -1686
- package/dist/index.js +757 -86
- package/package.json +9 -9
- package/templates/base/groupy_prompt.md +22 -19
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli/index.ts
|
|
5
5
|
import { resolve as resolve22 } from "path";
|
|
6
|
-
import { existsSync as
|
|
6
|
+
import { existsSync as existsSync25 } from "fs";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
8
|
|
|
9
9
|
// src/auth/store.ts
|
|
@@ -1109,761 +1109,1335 @@ class EphemeralWorkspaceManager {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
}
|
|
1111
1111
|
var globalEphemeralWorkspace = new EphemeralWorkspaceManager;
|
|
1112
|
+
// src/verification/verifier.ts
|
|
1113
|
+
import { spawnSync } from "child_process";
|
|
1114
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1115
|
+
import { join as join6 } from "path";
|
|
1112
1116
|
|
|
1113
|
-
// src/
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const estimatedTokens = estimateTotalTokens(currentHistory);
|
|
1122
|
-
const maxTokenLimit = DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS;
|
|
1123
|
-
if (estimatedTokens > maxTokenLimit) {
|
|
1124
|
-
const compacted = compactHistory(currentHistory);
|
|
1125
|
-
session.setHistory(compacted);
|
|
1126
|
-
session.emitEvent({
|
|
1127
|
-
type: "Warning",
|
|
1128
|
-
message: `Auto-compacted conversation history (${estimatedTokens} estimated tokens exceeded limit).`
|
|
1129
|
-
});
|
|
1117
|
+
// src/init/project-analyzer.ts
|
|
1118
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
1119
|
+
import { join as join5, basename } from "path";
|
|
1120
|
+
|
|
1121
|
+
class ProjectAnalyzer {
|
|
1122
|
+
cwd;
|
|
1123
|
+
constructor(cwd = process.cwd()) {
|
|
1124
|
+
this.cwd = cwd;
|
|
1130
1125
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
iteration++;
|
|
1169
|
-
let currentAgentText = "";
|
|
1170
|
-
const toolCallRequests = [];
|
|
1171
|
-
let iterInputTokens = Math.ceil((effectiveSystemPrompt.length + JSON.stringify(session.getHistory()).length) / 4);
|
|
1172
|
-
let iterOutputTokens = 0;
|
|
1173
|
-
const stream = clientSession.stream({
|
|
1174
|
-
model: turnContext.model,
|
|
1175
|
-
systemPrompt: effectiveSystemPrompt,
|
|
1176
|
-
history: session.getHistory(),
|
|
1177
|
-
tools: turnContext.tools,
|
|
1178
|
-
signal,
|
|
1179
|
-
enablePromptCache: true
|
|
1180
|
-
});
|
|
1181
|
-
for await (const chunk of stream) {
|
|
1182
|
-
if (signal.aborted) {
|
|
1183
|
-
throw new TurnAbortedError;
|
|
1126
|
+
analyze() {
|
|
1127
|
+
const readmeInfo = this.extractReadmeMetadata();
|
|
1128
|
+
const projectName = readmeInfo.title || this.detectProjectName();
|
|
1129
|
+
const languages = this.detectLanguages();
|
|
1130
|
+
const packageManager = this.detectPackageManager();
|
|
1131
|
+
const frameworks = [];
|
|
1132
|
+
const infrastructure = [];
|
|
1133
|
+
const commands = {};
|
|
1134
|
+
const architectureNotes = [];
|
|
1135
|
+
const codeConventions = [];
|
|
1136
|
+
let description = readmeInfo.description;
|
|
1137
|
+
const pkgPath = join5(this.cwd, "package.json");
|
|
1138
|
+
if (existsSync6(pkgPath)) {
|
|
1139
|
+
try {
|
|
1140
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1141
|
+
if (!description && pkg.description)
|
|
1142
|
+
description = pkg.description;
|
|
1143
|
+
const pm = packageManager || "npm";
|
|
1144
|
+
const runPrefix = pm === "bun" || pm === "yarn" || pm === "pnpm" ? `${pm} run` : "npm run";
|
|
1145
|
+
const testPrefix = pm === "bun" ? "bun test" : pm === "pnpm" ? "pnpm test" : pm === "yarn" ? "yarn test" : "npm test";
|
|
1146
|
+
if (pkg.scripts) {
|
|
1147
|
+
if (pkg.scripts.dev)
|
|
1148
|
+
commands.dev = `${runPrefix} dev`;
|
|
1149
|
+
else if (pkg.scripts.start)
|
|
1150
|
+
commands.dev = `${runPrefix} start`;
|
|
1151
|
+
if (pkg.scripts.build)
|
|
1152
|
+
commands.build = `${runPrefix} build`;
|
|
1153
|
+
if (pkg.scripts.test)
|
|
1154
|
+
commands.test = pkg.scripts.test === "bun test" ? "bun test" : testPrefix;
|
|
1155
|
+
if (pkg.scripts.typecheck)
|
|
1156
|
+
commands.typecheck = `${runPrefix} typecheck`;
|
|
1157
|
+
else if (pkg.scripts.check)
|
|
1158
|
+
commands.typecheck = `${runPrefix} check`;
|
|
1159
|
+
if (pkg.scripts.lint)
|
|
1160
|
+
commands.lint = `${runPrefix} lint`;
|
|
1161
|
+
if (pkg.scripts.format)
|
|
1162
|
+
commands.format = `${runPrefix} format`;
|
|
1184
1163
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1164
|
+
const allDeps = {
|
|
1165
|
+
...pkg.dependencies || {},
|
|
1166
|
+
...pkg.devDependencies || {}
|
|
1167
|
+
};
|
|
1168
|
+
if (allDeps.next)
|
|
1169
|
+
frameworks.push("Next.js");
|
|
1170
|
+
if (allDeps.react)
|
|
1171
|
+
frameworks.push("React");
|
|
1172
|
+
if (allDeps.vue)
|
|
1173
|
+
frameworks.push("Vue.js");
|
|
1174
|
+
if (allDeps.svelte || allDeps["@sveltejs/kit"])
|
|
1175
|
+
frameworks.push("Svelte");
|
|
1176
|
+
if (allDeps.astro)
|
|
1177
|
+
frameworks.push("Astro");
|
|
1178
|
+
if (allDeps.vite)
|
|
1179
|
+
frameworks.push("Vite");
|
|
1180
|
+
if (allDeps.express)
|
|
1181
|
+
frameworks.push("Express");
|
|
1182
|
+
if (allDeps.hono)
|
|
1183
|
+
frameworks.push("Hono");
|
|
1184
|
+
if (allDeps.fastify)
|
|
1185
|
+
frameworks.push("Fastify");
|
|
1186
|
+
if (allDeps["@nestjs/core"])
|
|
1187
|
+
frameworks.push("NestJS");
|
|
1188
|
+
if (allDeps.tailwindcss)
|
|
1189
|
+
frameworks.push("TailwindCSS");
|
|
1190
|
+
if (allDeps["lucide-react"] || allDeps.lucide)
|
|
1191
|
+
frameworks.push("Lucide Icons");
|
|
1192
|
+
if (allDeps.zustand)
|
|
1193
|
+
frameworks.push("Zustand");
|
|
1194
|
+
if (allDeps["@tanstack/react-query"])
|
|
1195
|
+
frameworks.push("TanStack Query");
|
|
1196
|
+
if (allDeps.oxlint)
|
|
1197
|
+
frameworks.push("Oxlint");
|
|
1198
|
+
if (allDeps.eslint)
|
|
1199
|
+
frameworks.push("ESLint");
|
|
1200
|
+
if (allDeps.vitest)
|
|
1201
|
+
frameworks.push("Vitest");
|
|
1202
|
+
if (allDeps.jest)
|
|
1203
|
+
frameworks.push("Jest");
|
|
1204
|
+
if (allDeps.playwright || allDeps["@playwright/test"])
|
|
1205
|
+
frameworks.push("Playwright");
|
|
1206
|
+
if (pkg.type === "module") {
|
|
1207
|
+
codeConventions.push("Use ES modules (`import/export`), not CommonJS (`require`).");
|
|
1208
|
+
}
|
|
1209
|
+
} catch {}
|
|
1210
|
+
}
|
|
1211
|
+
const tsconfigPath = join5(this.cwd, "tsconfig.json");
|
|
1212
|
+
if (existsSync6(tsconfigPath)) {
|
|
1213
|
+
try {
|
|
1214
|
+
const tsconfig = JSON.parse(readFileSync4(tsconfigPath, "utf8"));
|
|
1215
|
+
if (tsconfig.compilerOptions?.strict) {
|
|
1216
|
+
codeConventions.push("TypeScript strict mode enabled.");
|
|
1217
|
+
}
|
|
1218
|
+
if (!commands.typecheck) {
|
|
1219
|
+
commands.typecheck = "tsc --noEmit";
|
|
1209
1220
|
}
|
|
1221
|
+
} catch {}
|
|
1222
|
+
}
|
|
1223
|
+
const cargoPath = join5(this.cwd, "Cargo.toml");
|
|
1224
|
+
if (existsSync6(cargoPath)) {
|
|
1225
|
+
try {
|
|
1226
|
+
commands.dev = commands.dev || "cargo run";
|
|
1227
|
+
commands.build = commands.build || "cargo build";
|
|
1228
|
+
commands.test = commands.test || "cargo test";
|
|
1229
|
+
commands.lint = commands.lint || "cargo clippy";
|
|
1230
|
+
frameworks.push("Rust Cargo");
|
|
1231
|
+
} catch {}
|
|
1232
|
+
}
|
|
1233
|
+
const goModPath = join5(this.cwd, "go.mod");
|
|
1234
|
+
if (existsSync6(goModPath)) {
|
|
1235
|
+
try {
|
|
1236
|
+
commands.dev = commands.dev || "go run .";
|
|
1237
|
+
commands.build = commands.build || "go build ./...";
|
|
1238
|
+
commands.test = commands.test || "go test ./...";
|
|
1239
|
+
commands.lint = commands.lint || "golangci-lint run";
|
|
1240
|
+
frameworks.push("Go Modules");
|
|
1241
|
+
} catch {}
|
|
1242
|
+
}
|
|
1243
|
+
const pyprojectPath = join5(this.cwd, "pyproject.toml");
|
|
1244
|
+
const requirementsPath = join5(this.cwd, "requirements.txt");
|
|
1245
|
+
if (existsSync6(pyprojectPath) || existsSync6(requirementsPath)) {
|
|
1246
|
+
commands.test = commands.test || "pytest";
|
|
1247
|
+
commands.lint = commands.lint || "ruff check .";
|
|
1248
|
+
if (existsSync6(join5(this.cwd, "uv.lock"))) {
|
|
1249
|
+
frameworks.push("uv");
|
|
1250
|
+
commands.test = "uv run pytest";
|
|
1251
|
+
} else if (existsSync6(join5(this.cwd, "poetry.lock"))) {
|
|
1252
|
+
frameworks.push("Poetry");
|
|
1253
|
+
commands.test = "poetry run pytest";
|
|
1210
1254
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1255
|
+
}
|
|
1256
|
+
if (existsSync6(join5(this.cwd, "Dockerfile"))) {
|
|
1257
|
+
infrastructure.push("Docker");
|
|
1258
|
+
const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
|
|
1259
|
+
commands.dockerBuild = `docker build -t ${sanitizedName || "app"} .`;
|
|
1260
|
+
}
|
|
1261
|
+
if (existsSync6(join5(this.cwd, "nginx.conf"))) {
|
|
1262
|
+
infrastructure.push("Nginx");
|
|
1263
|
+
}
|
|
1264
|
+
if (existsSync6(join5(this.cwd, "src/api.ts")) || existsSync6(join5(this.cwd, "src/api"))) {
|
|
1265
|
+
architectureNotes.push("Backend API endpoints and network client logic are centralized in `src/api`.");
|
|
1266
|
+
}
|
|
1267
|
+
if (existsSync6(join5(this.cwd, "src/components"))) {
|
|
1268
|
+
architectureNotes.push("Reusable UI presentation components live in `src/components/`.");
|
|
1269
|
+
}
|
|
1270
|
+
if (existsSync6(join5(this.cwd, "src/types.ts")) || existsSync6(join5(this.cwd, "src/types"))) {
|
|
1271
|
+
architectureNotes.push("Shared TypeScript data models and interfaces are defined in `src/types`.");
|
|
1272
|
+
}
|
|
1273
|
+
if (existsSync6(join5(this.cwd, ".env.example"))) {
|
|
1274
|
+
architectureNotes.push("Environment configuration template is in `.env.example`.");
|
|
1275
|
+
}
|
|
1276
|
+
if (commands.typecheck || commands.lint || commands.test) {
|
|
1277
|
+
const checks = [];
|
|
1278
|
+
if (commands.typecheck)
|
|
1279
|
+
checks.push(`typecheck (\`${commands.typecheck}\`)`);
|
|
1280
|
+
if (commands.lint)
|
|
1281
|
+
checks.push(`lint (\`${commands.lint}\`)`);
|
|
1282
|
+
if (commands.test)
|
|
1283
|
+
checks.push(`tests (\`${commands.test}\`)`);
|
|
1284
|
+
codeConventions.push(`Run ${checks.join(" and ")} before concluding any major code edits.`);
|
|
1285
|
+
}
|
|
1286
|
+
const instructionFiles = ["AGENTS.md", "CLAUDE.md", ".agents.md", "AGENTS.override.md"];
|
|
1287
|
+
let hasExistingInstructions = false;
|
|
1288
|
+
let existingInstructionFile;
|
|
1289
|
+
for (const f of instructionFiles) {
|
|
1290
|
+
if (existsSync6(join5(this.cwd, f))) {
|
|
1291
|
+
hasExistingInstructions = true;
|
|
1292
|
+
existingInstructionFile = f;
|
|
1293
|
+
break;
|
|
1213
1294
|
}
|
|
1214
|
-
accumulatedInputTokens += iterInputTokens;
|
|
1215
|
-
accumulatedOutputTokens += iterOutputTokens;
|
|
1216
|
-
if (currentAgentText.trim()) {
|
|
1217
|
-
const agentItem = {
|
|
1218
|
-
id: `msg_agent_${Date.now()}`,
|
|
1219
|
-
type: "agent_message",
|
|
1220
|
-
content: currentAgentText,
|
|
1221
|
-
createdAt: Date.now()
|
|
1222
|
-
};
|
|
1223
|
-
session.addHistoryItem(agentItem);
|
|
1224
|
-
session.emitEvent({
|
|
1225
|
-
type: "ItemCompleted",
|
|
1226
|
-
turnId,
|
|
1227
|
-
item: agentItem
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
if (toolCallRequests.length > 0) {
|
|
1231
|
-
for (const toolCall of toolCallRequests) {
|
|
1232
|
-
if (!toolCall.name || !toolCall.name.trim())
|
|
1233
|
-
continue;
|
|
1234
|
-
const functionCallItem = {
|
|
1235
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1236
|
-
type: "function_call",
|
|
1237
|
-
callId: toolCall.callId,
|
|
1238
|
-
name: toolCall.name,
|
|
1239
|
-
arguments: toolCall.arguments,
|
|
1240
|
-
createdAt: Date.now()
|
|
1241
|
-
};
|
|
1242
|
-
session.addHistoryItem(functionCallItem);
|
|
1243
|
-
session.emitEvent({
|
|
1244
|
-
type: "ItemStarted",
|
|
1245
|
-
turnId,
|
|
1246
|
-
item: functionCallItem
|
|
1247
|
-
});
|
|
1248
|
-
session.emitEvent({
|
|
1249
|
-
type: "ToolCallStarted",
|
|
1250
|
-
turnId,
|
|
1251
|
-
toolName: toolCall.name,
|
|
1252
|
-
arguments: toolCall.arguments
|
|
1253
|
-
});
|
|
1254
|
-
const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
|
|
1255
|
-
cwd: turnContext.environment.cwd,
|
|
1256
|
-
turnId,
|
|
1257
|
-
signal,
|
|
1258
|
-
execPolicy: session.execPolicy,
|
|
1259
|
-
mode: session.collaborationMode,
|
|
1260
|
-
permissionMode: session.permissionMode,
|
|
1261
|
-
onPlanUpdate: (plan, explanation) => {
|
|
1262
|
-
session.emitEvent({
|
|
1263
|
-
type: "PlanUpdated",
|
|
1264
|
-
turnId,
|
|
1265
|
-
explanation,
|
|
1266
|
-
plan
|
|
1267
|
-
});
|
|
1268
|
-
},
|
|
1269
|
-
requestApproval: async (description, command, prefixRule) => {
|
|
1270
|
-
const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1271
|
-
return session.requestApproval({
|
|
1272
|
-
approvalId,
|
|
1273
|
-
turnId,
|
|
1274
|
-
toolName: toolCall.name,
|
|
1275
|
-
description,
|
|
1276
|
-
command
|
|
1277
|
-
});
|
|
1278
|
-
},
|
|
1279
|
-
requestInput: async (question, options) => {
|
|
1280
|
-
const questionId = `quest_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1281
|
-
return session.requestUserQuestion({
|
|
1282
|
-
questionId,
|
|
1283
|
-
turnId,
|
|
1284
|
-
question,
|
|
1285
|
-
options
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
const functionOutputItem = {
|
|
1290
|
-
id: `out_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1291
|
-
type: "function_call_output",
|
|
1292
|
-
callId: toolCall.callId,
|
|
1293
|
-
output: toolResult.output,
|
|
1294
|
-
isError: toolResult.isError,
|
|
1295
|
-
createdAt: Date.now()
|
|
1296
|
-
};
|
|
1297
|
-
session.addHistoryItem(functionOutputItem);
|
|
1298
|
-
session.emitEvent({
|
|
1299
|
-
type: "ToolCallFinished",
|
|
1300
|
-
turnId,
|
|
1301
|
-
toolName: toolCall.name,
|
|
1302
|
-
output: toolResult.output,
|
|
1303
|
-
isError: toolResult.isError
|
|
1304
|
-
});
|
|
1305
|
-
session.emitEvent({
|
|
1306
|
-
type: "ItemCompleted",
|
|
1307
|
-
turnId,
|
|
1308
|
-
item: functionOutputItem
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
if (!currentAgentText.trim() && toolCallRequests.length === 0) {
|
|
1314
|
-
if (iteration === 1 && iteration < turnContext.maxIterations) {
|
|
1315
|
-
session.addHistoryItem({
|
|
1316
|
-
id: `msg_nudge_${Date.now()}`,
|
|
1317
|
-
type: "user_message",
|
|
1318
|
-
content: `[Systematic ReAct Nudge]: No tool actions or answers were produced in this iteration.
|
|
1319
|
-
1. Review the user's objective and determine the immediate next action.
|
|
1320
|
-
2. If more context is required, invoke an exploration tool ('read_file', 'list_dir', 'grep_search', 'find_files').
|
|
1321
|
-
3. If ready to answer or implement, call the required mutating tool or deliver your full, concrete response now.`,
|
|
1322
|
-
createdAt: Date.now()
|
|
1323
|
-
});
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
break;
|
|
1328
1295
|
}
|
|
1329
|
-
const totalContextTokens = estimateTotalTokens(session.getHistory()) + Math.ceil(effectiveSystemPrompt.length / 4);
|
|
1330
|
-
const maxContextTokens = DEFAULT_MAX_CONTEXT_TOKENS;
|
|
1331
|
-
session.emitEvent({
|
|
1332
|
-
type: "TurnCompleted",
|
|
1333
|
-
turnId,
|
|
1334
|
-
inputTokens: accumulatedInputTokens,
|
|
1335
|
-
outputTokens: accumulatedOutputTokens,
|
|
1336
|
-
totalTokens: accumulatedInputTokens + accumulatedOutputTokens,
|
|
1337
|
-
cachedTokens: accumulatedCachedTokens > 0 ? accumulatedCachedTokens : undefined,
|
|
1338
|
-
contextTokens: totalContextTokens,
|
|
1339
|
-
maxContextTokens
|
|
1340
|
-
});
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
const isAborted = error instanceof TurnAbortedError || signal.aborted;
|
|
1343
|
-
const message = isAborted ? "Turn was interrupted" : error instanceof Error ? error.message : String(error);
|
|
1344
|
-
session.emitEvent({
|
|
1345
|
-
type: "Error",
|
|
1346
|
-
turnId,
|
|
1347
|
-
message,
|
|
1348
|
-
code: isAborted ? "TURN_ABORTED" : "EXECUTION_ERROR"
|
|
1349
|
-
});
|
|
1350
|
-
} finally {
|
|
1351
|
-
session.clearActiveTurn(turnId);
|
|
1352
|
-
globalEphemeralWorkspace.cleanupTurn(turnId);
|
|
1353
|
-
globalEphemeralWorkspace.cleanRootResidue(turnContext.environment.cwd);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// src/session/turn-input.ts
|
|
1358
|
-
async function handleTurnInput(session, request) {
|
|
1359
|
-
const activeTurn = session.getActiveTurn();
|
|
1360
|
-
if (!activeTurn) {
|
|
1361
|
-
const turnId = `turn_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
1362
|
-
const turnContext = new TurnContext(turnId, session.model, session.tools, { cwd: session.cwd });
|
|
1363
|
-
session.setActiveTurn(turnContext);
|
|
1364
|
-
runTurn(session, turnContext, request).catch((err) => {
|
|
1365
|
-
session.emitEvent({
|
|
1366
|
-
type: "Error",
|
|
1367
|
-
turnId,
|
|
1368
|
-
message: `Unhandled turn error: ${err instanceof Error ? err.message : String(err)}`
|
|
1369
|
-
});
|
|
1370
|
-
});
|
|
1371
1296
|
return {
|
|
1372
|
-
|
|
1373
|
-
|
|
1297
|
+
projectName,
|
|
1298
|
+
description,
|
|
1299
|
+
languages,
|
|
1300
|
+
packageManager,
|
|
1301
|
+
frameworks,
|
|
1302
|
+
infrastructure,
|
|
1303
|
+
commands,
|
|
1304
|
+
architectureNotes,
|
|
1305
|
+
codeConventions,
|
|
1306
|
+
hasExistingInstructions,
|
|
1307
|
+
existingInstructionFile
|
|
1374
1308
|
};
|
|
1375
1309
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1310
|
+
generateAgentsMarkdown(analysis) {
|
|
1311
|
+
const lines = [];
|
|
1312
|
+
lines.push(`# ${analysis.projectName}`);
|
|
1313
|
+
lines.push("");
|
|
1314
|
+
if (analysis.description) {
|
|
1315
|
+
lines.push(`> ${analysis.description}`);
|
|
1316
|
+
lines.push("");
|
|
1317
|
+
}
|
|
1318
|
+
lines.push("## Commands");
|
|
1319
|
+
lines.push("");
|
|
1320
|
+
if (Object.keys(analysis.commands).length > 0) {
|
|
1321
|
+
if (analysis.commands.dev)
|
|
1322
|
+
lines.push(`- **Dev Server**: \`${analysis.commands.dev}\``);
|
|
1323
|
+
if (analysis.commands.build)
|
|
1324
|
+
lines.push(`- **Build**: \`${analysis.commands.build}\``);
|
|
1325
|
+
if (analysis.commands.test)
|
|
1326
|
+
lines.push(`- **Test**: \`${analysis.commands.test}\``);
|
|
1327
|
+
if (analysis.commands.typecheck)
|
|
1328
|
+
lines.push(`- **Typecheck**: \`${analysis.commands.typecheck}\``);
|
|
1329
|
+
if (analysis.commands.lint)
|
|
1330
|
+
lines.push(`- **Lint**: \`${analysis.commands.lint}\``);
|
|
1331
|
+
if (analysis.commands.format)
|
|
1332
|
+
lines.push(`- **Format**: \`${analysis.commands.format}\``);
|
|
1333
|
+
if (analysis.commands.dockerBuild)
|
|
1334
|
+
lines.push(`- **Docker Build**: \`${analysis.commands.dockerBuild}\``);
|
|
1335
|
+
} else {
|
|
1336
|
+
lines.push("- *No standard build/test commands detected.*");
|
|
1337
|
+
}
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
lines.push("## Architecture & Stack");
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
const stackItems = [];
|
|
1342
|
+
if (analysis.languages.length > 0)
|
|
1343
|
+
stackItems.push(analysis.languages.join(", "));
|
|
1344
|
+
if (analysis.frameworks.length > 0)
|
|
1345
|
+
stackItems.push(analysis.frameworks.join(", "));
|
|
1346
|
+
if (analysis.infrastructure.length > 0)
|
|
1347
|
+
stackItems.push(analysis.infrastructure.join(", "));
|
|
1348
|
+
if (stackItems.length > 0) {
|
|
1349
|
+
lines.push(`- **Core Stack**: ${stackItems.join(" \u2022 ")}`);
|
|
1350
|
+
}
|
|
1351
|
+
for (const note of analysis.architectureNotes) {
|
|
1352
|
+
lines.push(`- ${note}`);
|
|
1353
|
+
}
|
|
1354
|
+
lines.push("");
|
|
1355
|
+
lines.push("## Workflow & Code Guidelines");
|
|
1356
|
+
lines.push("");
|
|
1357
|
+
if (analysis.codeConventions.length > 0) {
|
|
1358
|
+
for (const conv of analysis.codeConventions) {
|
|
1359
|
+
lines.push(`- ${conv}`);
|
|
1420
1360
|
}
|
|
1421
1361
|
}
|
|
1362
|
+
lines.push("- Prefer targeted edits over whole-file rewrites.");
|
|
1363
|
+
lines.push("- When fixing errors, address the root cause rather than suppressing compiler warnings.");
|
|
1364
|
+
lines.push("");
|
|
1365
|
+
return lines.join(`
|
|
1366
|
+
`);
|
|
1422
1367
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1368
|
+
extractReadmeMetadata() {
|
|
1369
|
+
const readmeFiles = ["README.md", "readme.md", "README.MD"];
|
|
1370
|
+
for (const file of readmeFiles) {
|
|
1371
|
+
const fullPath = join5(this.cwd, file);
|
|
1372
|
+
if (existsSync6(fullPath)) {
|
|
1373
|
+
try {
|
|
1374
|
+
const content = readFileSync4(fullPath, "utf8");
|
|
1375
|
+
const lines = content.split(`
|
|
1376
|
+
`);
|
|
1377
|
+
let title;
|
|
1378
|
+
let description;
|
|
1379
|
+
for (const line of lines) {
|
|
1380
|
+
const trimmed = line.trim();
|
|
1381
|
+
if (!title && trimmed.startsWith("# ")) {
|
|
1382
|
+
title = trimmed.replace(/^#\s+/, "").trim();
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (title && !description && trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("```") && !trimmed.startsWith("[")) {
|
|
1386
|
+
description = trimmed;
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return { title, description };
|
|
1391
|
+
} catch {}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return {};
|
|
1449
1395
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1396
|
+
detectProjectName() {
|
|
1397
|
+
const pkgPath = join5(this.cwd, "package.json");
|
|
1398
|
+
if (existsSync6(pkgPath)) {
|
|
1399
|
+
try {
|
|
1400
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1401
|
+
if (pkg.name && pkg.name !== "frontend" && pkg.name !== "backend" && pkg.name !== "app") {
|
|
1402
|
+
return pkg.name.startsWith("@") ? pkg.name.split("/")[1] || pkg.name : pkg.name;
|
|
1403
|
+
}
|
|
1404
|
+
} catch {}
|
|
1457
1405
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1406
|
+
const cargoPath = join5(this.cwd, "Cargo.toml");
|
|
1407
|
+
if (existsSync6(cargoPath)) {
|
|
1408
|
+
try {
|
|
1409
|
+
const match = readFileSync4(cargoPath, "utf8").match(/name\s*=\s*"([^"]+)"/);
|
|
1410
|
+
if (match?.[1])
|
|
1411
|
+
return match[1];
|
|
1412
|
+
} catch {}
|
|
1463
1413
|
}
|
|
1464
|
-
|
|
1414
|
+
const goModPath = join5(this.cwd, "go.mod");
|
|
1415
|
+
if (existsSync6(goModPath)) {
|
|
1416
|
+
try {
|
|
1417
|
+
const match = readFileSync4(goModPath, "utf8").match(/module\s+([^\s]+)/);
|
|
1418
|
+
if (match?.[1])
|
|
1419
|
+
return basename(match[1]);
|
|
1420
|
+
} catch {}
|
|
1421
|
+
}
|
|
1422
|
+
return basename(this.cwd);
|
|
1465
1423
|
}
|
|
1466
|
-
|
|
1467
|
-
const
|
|
1468
|
-
if (this.
|
|
1469
|
-
|
|
1470
|
-
if (isReadOnly) {
|
|
1471
|
-
return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
|
|
1472
|
-
}
|
|
1473
|
-
return {
|
|
1474
|
-
decision: "prompt",
|
|
1475
|
-
reason: `[Plan Mode Gate] Approval required to execute shell command '${trimmed}' in Plan Mode`
|
|
1476
|
-
};
|
|
1424
|
+
detectLanguages() {
|
|
1425
|
+
const langs = new Set;
|
|
1426
|
+
if (existsSync6(join5(this.cwd, "tsconfig.json")) || this.hasFileWithExtension(".ts", ".tsx")) {
|
|
1427
|
+
langs.add("TypeScript");
|
|
1477
1428
|
}
|
|
1478
|
-
if (this.
|
|
1479
|
-
|
|
1480
|
-
decision: "prompt",
|
|
1481
|
-
reason: "Manual mode requires confirmation for all shell commands"
|
|
1482
|
-
};
|
|
1429
|
+
if (existsSync6(join5(this.cwd, "package.json")) || this.hasFileWithExtension(".js", ".jsx", ".mjs")) {
|
|
1430
|
+
langs.add("JavaScript");
|
|
1483
1431
|
}
|
|
1484
|
-
if (this.
|
|
1485
|
-
|
|
1486
|
-
if (isReadOnly) {
|
|
1487
|
-
return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
|
|
1488
|
-
}
|
|
1489
|
-
return {
|
|
1490
|
-
decision: "prompt",
|
|
1491
|
-
reason: "Accept-edits mode requires approval for active shell commands"
|
|
1492
|
-
};
|
|
1432
|
+
if (existsSync6(join5(this.cwd, "Cargo.toml")) || this.hasFileWithExtension(".rs")) {
|
|
1433
|
+
langs.add("Rust");
|
|
1493
1434
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1435
|
+
if (existsSync6(join5(this.cwd, "go.mod")) || this.hasFileWithExtension(".go")) {
|
|
1436
|
+
langs.add("Go");
|
|
1437
|
+
}
|
|
1438
|
+
if (existsSync6(join5(this.cwd, "pyproject.toml")) || existsSync6(join5(this.cwd, "requirements.txt")) || this.hasFileWithExtension(".py")) {
|
|
1439
|
+
langs.add("Python");
|
|
1440
|
+
}
|
|
1441
|
+
if (existsSync6(join5(this.cwd, "pom.xml")) || existsSync6(join5(this.cwd, "build.gradle")) || this.hasFileWithExtension(".java")) {
|
|
1442
|
+
langs.add("Java");
|
|
1443
|
+
}
|
|
1444
|
+
if (existsSync6(join5(this.cwd, "CMakeLists.txt")) || this.hasFileWithExtension(".cpp", ".c", ".h", ".hpp")) {
|
|
1445
|
+
langs.add("C/C++");
|
|
1446
|
+
}
|
|
1447
|
+
return Array.from(langs);
|
|
1448
|
+
}
|
|
1449
|
+
detectPackageManager() {
|
|
1450
|
+
if (existsSync6(join5(this.cwd, "bun.lockb")) || existsSync6(join5(this.cwd, "bun.lock")))
|
|
1451
|
+
return "bun";
|
|
1452
|
+
if (existsSync6(join5(this.cwd, "pnpm-lock.yaml")))
|
|
1453
|
+
return "pnpm";
|
|
1454
|
+
if (existsSync6(join5(this.cwd, "yarn.lock")))
|
|
1455
|
+
return "yarn";
|
|
1456
|
+
if (existsSync6(join5(this.cwd, "package-lock.json")))
|
|
1457
|
+
return "npm";
|
|
1458
|
+
if (existsSync6(join5(this.cwd, "Cargo.lock")) || existsSync6(join5(this.cwd, "Cargo.toml")))
|
|
1459
|
+
return "cargo";
|
|
1460
|
+
if (existsSync6(join5(this.cwd, "uv.lock")))
|
|
1461
|
+
return "uv";
|
|
1462
|
+
if (existsSync6(join5(this.cwd, "poetry.lock")))
|
|
1463
|
+
return "poetry";
|
|
1464
|
+
if (existsSync6(join5(this.cwd, "go.sum")) || existsSync6(join5(this.cwd, "go.mod")))
|
|
1465
|
+
return "go";
|
|
1466
|
+
if (existsSync6(join5(this.cwd, "package.json")))
|
|
1467
|
+
return "npm";
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
hasFileWithExtension(...exts) {
|
|
1471
|
+
try {
|
|
1472
|
+
const entries = readdirSync3(this.cwd);
|
|
1473
|
+
return entries.some((e) => exts.some((ext) => e.endsWith(ext)));
|
|
1474
|
+
} catch {
|
|
1475
|
+
return false;
|
|
1501
1476
|
}
|
|
1502
|
-
return {
|
|
1503
|
-
decision: "allow",
|
|
1504
|
-
reason: "Auto mode allows execution"
|
|
1505
|
-
};
|
|
1506
1477
|
}
|
|
1507
1478
|
}
|
|
1508
1479
|
|
|
1509
|
-
// src/
|
|
1510
|
-
class
|
|
1511
|
-
threadId;
|
|
1512
|
-
model;
|
|
1480
|
+
// src/verification/verifier.ts
|
|
1481
|
+
class AutoVerifier {
|
|
1513
1482
|
cwd;
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
execPolicy;
|
|
1521
|
-
collaborationMode = "default";
|
|
1522
|
-
get permissionMode() {
|
|
1523
|
-
return this.execPolicy.getMode();
|
|
1483
|
+
customCommand;
|
|
1484
|
+
timeoutMs;
|
|
1485
|
+
constructor(options) {
|
|
1486
|
+
this.cwd = options.cwd;
|
|
1487
|
+
this.customCommand = options.customCommand;
|
|
1488
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
1524
1489
|
}
|
|
1525
|
-
|
|
1526
|
-
this.
|
|
1527
|
-
|
|
1528
|
-
this.collaborationMode = "plan";
|
|
1529
|
-
} else if (this.collaborationMode === "plan") {
|
|
1530
|
-
this.collaborationMode = "default";
|
|
1490
|
+
resolveVerificationCommand() {
|
|
1491
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
1492
|
+
return this.customCommand.trim();
|
|
1531
1493
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1494
|
+
try {
|
|
1495
|
+
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
1496
|
+
const analysis = analyzer.analyze();
|
|
1497
|
+
if (analysis.commands.typecheck) {
|
|
1498
|
+
return analysis.commands.typecheck;
|
|
1499
|
+
}
|
|
1500
|
+
if (analysis.commands.lint) {
|
|
1501
|
+
return analysis.commands.lint;
|
|
1502
|
+
}
|
|
1503
|
+
if (analysis.commands.test) {
|
|
1504
|
+
return analysis.commands.test;
|
|
1505
|
+
}
|
|
1506
|
+
} catch {}
|
|
1507
|
+
const pkgPath = join6(this.cwd, "package.json");
|
|
1508
|
+
if (existsSync7(pkgPath)) {
|
|
1509
|
+
try {
|
|
1510
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1511
|
+
if (pkg.scripts) {
|
|
1512
|
+
if (pkg.scripts.typecheck)
|
|
1513
|
+
return "npm run typecheck";
|
|
1514
|
+
if (pkg.scripts.check)
|
|
1515
|
+
return "npm run check";
|
|
1516
|
+
if (pkg.scripts.test)
|
|
1517
|
+
return "npm test";
|
|
1518
|
+
}
|
|
1519
|
+
} catch {}
|
|
1557
1520
|
}
|
|
1558
|
-
this.
|
|
1559
|
-
|
|
1560
|
-
type: "SessionConfigured",
|
|
1561
|
-
threadId: this.threadId,
|
|
1562
|
-
model: this.model
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
onEvent(listener) {
|
|
1566
|
-
this.eventListeners.push(listener);
|
|
1567
|
-
return () => {
|
|
1568
|
-
this.eventListeners = this.eventListeners.filter((l) => l !== listener);
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
emitEvent(msg) {
|
|
1572
|
-
if (msg.type === "StatusChanged") {
|
|
1573
|
-
this.status = msg.status;
|
|
1521
|
+
if (existsSync7(join6(this.cwd, "tsconfig.json"))) {
|
|
1522
|
+
return "npx tsc --noEmit";
|
|
1574
1523
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
console.error("Error in event listener:", err);
|
|
1524
|
+
if (existsSync7(join6(this.cwd, "Cargo.toml"))) {
|
|
1525
|
+
return "cargo check";
|
|
1526
|
+
}
|
|
1527
|
+
if (existsSync7(join6(this.cwd, "go.mod"))) {
|
|
1528
|
+
return "go vet ./...";
|
|
1529
|
+
}
|
|
1530
|
+
if (existsSync7(join6(this.cwd, "pyproject.toml")) || existsSync7(join6(this.cwd, "setup.py"))) {
|
|
1531
|
+
if (existsSync7(join6(this.cwd, "mypy.ini")) || existsSync7(join6(this.cwd, ".mypy.ini"))) {
|
|
1532
|
+
return "mypy .";
|
|
1585
1533
|
}
|
|
1586
1534
|
}
|
|
1535
|
+
return null;
|
|
1587
1536
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
setActiveTurn(turn) {
|
|
1601
|
-
this.activeTurn = turn;
|
|
1602
|
-
this.emitEvent({
|
|
1603
|
-
type: "StatusChanged",
|
|
1604
|
-
status: "running"
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
clearActiveTurn(turnId) {
|
|
1608
|
-
if (!turnId || this.activeTurn?.turnId === turnId) {
|
|
1609
|
-
this.activeTurn = null;
|
|
1610
|
-
this.emitEvent({
|
|
1611
|
-
type: "StatusChanged",
|
|
1612
|
-
status: "idle"
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
interrupt() {
|
|
1617
|
-
if (this.activeTurn) {
|
|
1618
|
-
this.activeTurn.abort("Interrupted by user");
|
|
1619
|
-
this.clearActiveTurn();
|
|
1620
|
-
this.emitEvent({
|
|
1621
|
-
type: "StatusChanged",
|
|
1622
|
-
status: "interrupted"
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
requestApproval(params) {
|
|
1627
|
-
this.emitEvent({
|
|
1628
|
-
type: "ApprovalRequired",
|
|
1629
|
-
approvalId: params.approvalId,
|
|
1630
|
-
turnId: params.turnId,
|
|
1631
|
-
toolName: params.toolName,
|
|
1632
|
-
description: params.description,
|
|
1633
|
-
command: params.command
|
|
1634
|
-
});
|
|
1635
|
-
this.emitEvent({
|
|
1636
|
-
type: "StatusChanged",
|
|
1637
|
-
status: "waiting_approval"
|
|
1638
|
-
});
|
|
1639
|
-
return new Promise((resolve) => {
|
|
1640
|
-
this.pendingApprovals.set(params.approvalId, (approved) => {
|
|
1641
|
-
this.emitEvent({
|
|
1642
|
-
type: "StatusChanged",
|
|
1643
|
-
status: "running"
|
|
1644
|
-
});
|
|
1645
|
-
resolve(approved);
|
|
1646
|
-
});
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
resolveApproval(approvalId, approved) {
|
|
1650
|
-
const resolver = this.pendingApprovals.get(approvalId);
|
|
1651
|
-
if (resolver) {
|
|
1652
|
-
this.pendingApprovals.delete(approvalId);
|
|
1653
|
-
resolver(approved);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
requestUserQuestion(params) {
|
|
1657
|
-
this.emitEvent({
|
|
1658
|
-
type: "UserQuestionRequired",
|
|
1659
|
-
questionId: params.questionId,
|
|
1660
|
-
turnId: params.turnId,
|
|
1661
|
-
question: params.question,
|
|
1662
|
-
options: params.options
|
|
1663
|
-
});
|
|
1664
|
-
this.emitEvent({
|
|
1665
|
-
type: "StatusChanged",
|
|
1666
|
-
status: "waiting_user_input"
|
|
1667
|
-
});
|
|
1668
|
-
return new Promise((resolve) => {
|
|
1669
|
-
this.pendingUserQuestions.set(params.questionId, (answer) => {
|
|
1670
|
-
this.emitEvent({
|
|
1671
|
-
type: "StatusChanged",
|
|
1672
|
-
status: "running"
|
|
1673
|
-
});
|
|
1674
|
-
resolve(answer);
|
|
1675
|
-
});
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
resolveUserQuestion(questionId, answer) {
|
|
1679
|
-
const resolver = this.pendingUserQuestions.get(questionId);
|
|
1680
|
-
if (resolver) {
|
|
1681
|
-
this.pendingUserQuestions.delete(questionId);
|
|
1682
|
-
resolver(answer);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
async submit(op) {
|
|
1686
|
-
const subId = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1687
|
-
const submission = {
|
|
1688
|
-
id: subId,
|
|
1689
|
-
op,
|
|
1690
|
-
createdAt: Date.now()
|
|
1691
|
-
};
|
|
1692
|
-
const resolver = this.submissionResolvers.shift();
|
|
1693
|
-
if (resolver) {
|
|
1694
|
-
resolver(submission);
|
|
1695
|
-
} else {
|
|
1696
|
-
this.submissionQueue.push(submission);
|
|
1537
|
+
verify(modifiedFiles = []) {
|
|
1538
|
+
const command = this.resolveVerificationCommand();
|
|
1539
|
+
const startTime = performance.now();
|
|
1540
|
+
if (!command) {
|
|
1541
|
+
return {
|
|
1542
|
+
command: "none",
|
|
1543
|
+
success: true,
|
|
1544
|
+
exitCode: 0,
|
|
1545
|
+
output: "No automated verification command configured or detected for this workspace.",
|
|
1546
|
+
durationMs: 0,
|
|
1547
|
+
reason: "NO_VERIFIER_DETECTED"
|
|
1548
|
+
};
|
|
1697
1549
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if (event.msg.type === "TurnCompleted") {
|
|
1711
|
-
clearTimeout(timer);
|
|
1712
|
-
unsub();
|
|
1713
|
-
resolve();
|
|
1714
|
-
} else if (event.msg.type === "Error") {
|
|
1715
|
-
clearTimeout(timer);
|
|
1716
|
-
unsub();
|
|
1717
|
-
reject(new Error(event.msg.message));
|
|
1550
|
+
try {
|
|
1551
|
+
const isWindows = process.platform === "win32";
|
|
1552
|
+
const proc = spawnSync(command, {
|
|
1553
|
+
cwd: this.cwd,
|
|
1554
|
+
shell: true,
|
|
1555
|
+
encoding: "utf8",
|
|
1556
|
+
timeout: this.timeoutMs,
|
|
1557
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1558
|
+
env: {
|
|
1559
|
+
...process.env,
|
|
1560
|
+
CI: "true",
|
|
1561
|
+
FORCE_COLOR: "0"
|
|
1718
1562
|
}
|
|
1719
1563
|
});
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
}
|
|
1739
|
-
startSubmissionLoop() {
|
|
1740
|
-
const iterator = this.createSubmissionIterator();
|
|
1741
|
-
submissionLoop(this, iterator).catch((err) => {
|
|
1742
|
-
console.error("Submission loop terminated with error:", err);
|
|
1743
|
-
});
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
// src/tools/handlers/apply-patch.ts
|
|
1747
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1748
|
-
import { resolve as resolve5, dirname as dirname3 } from "path";
|
|
1749
|
-
import { mkdirSync as mkdirSync4 } from "fs";
|
|
1750
|
-
var applyPatchTool = {
|
|
1751
|
-
name: "apply_patch",
|
|
1752
|
-
description: "Apply precise multi-line modifications to an existing file or create a new file. TargetContent must match the file content exactly.",
|
|
1753
|
-
parameters: {
|
|
1754
|
-
type: "object",
|
|
1755
|
-
properties: {
|
|
1756
|
-
path: {
|
|
1757
|
-
type: "string",
|
|
1758
|
-
description: "Relative or absolute path to the target file"
|
|
1759
|
-
},
|
|
1760
|
-
targetContent: {
|
|
1761
|
-
type: "string",
|
|
1762
|
-
description: "The exact block of code in the file to be replaced. For creating a new file, leave this empty."
|
|
1763
|
-
},
|
|
1764
|
-
replacementContent: {
|
|
1765
|
-
type: "string",
|
|
1766
|
-
description: "The new code content to replace the targetContent with."
|
|
1767
|
-
}
|
|
1768
|
-
},
|
|
1769
|
-
required: ["path", "replacementContent"]
|
|
1770
|
-
},
|
|
1771
|
-
async execute(args, ctx) {
|
|
1772
|
-
const rawPath = String(args.path || "");
|
|
1773
|
-
if (!rawPath) {
|
|
1774
|
-
return { output: "Error: 'path' parameter is required", isError: true };
|
|
1775
|
-
}
|
|
1776
|
-
const filePath = resolve5(ctx.cwd, rawPath);
|
|
1777
|
-
const targetContent = typeof args.targetContent === "string" ? args.targetContent : "";
|
|
1778
|
-
const replacementContent = String(args.replacementContent ?? "");
|
|
1779
|
-
if (ctx.execPolicy) {
|
|
1780
|
-
const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
|
|
1781
|
-
if (evalResult.prompt && ctx.requestApproval) {
|
|
1782
|
-
const approval = await ctx.requestApproval(evalResult.reason || `Apply patch to: ${rawPath}`, `apply_patch ${rawPath}`);
|
|
1783
|
-
const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
|
|
1784
|
-
if (!allowed) {
|
|
1785
|
-
return {
|
|
1786
|
-
output: `[Plan Mode Gate]: File modification declined by user for '${rawPath}'. Please refine your implementation plan or ask the user for guidance.`,
|
|
1787
|
-
isError: true
|
|
1788
|
-
};
|
|
1564
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
1565
|
+
const stdout = proc.stdout ? String(proc.stdout) : "";
|
|
1566
|
+
const stderr = proc.stderr ? String(proc.stderr) : "";
|
|
1567
|
+
let combined = (stdout + `
|
|
1568
|
+
` + stderr).trim();
|
|
1569
|
+
if (combined.length > 3000) {
|
|
1570
|
+
const lines = combined.split(`
|
|
1571
|
+
`);
|
|
1572
|
+
if (lines.length > 60) {
|
|
1573
|
+
const head = lines.slice(0, 30).join(`
|
|
1574
|
+
`);
|
|
1575
|
+
const tail = lines.slice(-25).join(`
|
|
1576
|
+
`);
|
|
1577
|
+
combined = `${head}
|
|
1578
|
+
|
|
1579
|
+
... [${lines.length - 55} lines truncated for context efficiency] ...
|
|
1580
|
+
|
|
1581
|
+
${tail}`;
|
|
1789
1582
|
}
|
|
1790
|
-
} else if (evalResult.isPlanBlocked || ctx.mode === "plan") {
|
|
1791
|
-
return {
|
|
1792
|
-
output: `[Plan Mode Gate]: Cannot mutate '${rawPath}' while in Plan Mode without user approval. Please present your implementation plan first.`,
|
|
1793
|
-
isError: true
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
if (!existsSync6(filePath)) {
|
|
1798
|
-
if (targetContent) {
|
|
1799
|
-
return {
|
|
1800
|
-
output: `Error: Target file '${rawPath}' does not exist, but targetContent was provided.
|
|
1801
|
-
[Systematic Error Recovery Checklist]:
|
|
1802
|
-
1. Root Cause: Trying to patch a non-existent file with targetContent.
|
|
1803
|
-
2. Fix: For creating new files, leave 'targetContent' empty and provide full contents in 'replacementContent'.
|
|
1804
|
-
3. Alternatively, check if the file path '${rawPath}' was mistyped.`,
|
|
1805
|
-
isError: true
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
try {
|
|
1809
|
-
mkdirSync4(dirname3(filePath), { recursive: true });
|
|
1810
|
-
writeFileSync2(filePath, replacementContent, "utf8");
|
|
1811
|
-
return { output: `Successfully created new file '${rawPath}'` };
|
|
1812
|
-
} catch (err) {
|
|
1813
|
-
return {
|
|
1814
|
-
output: `Failed to create file '${rawPath}': ${err instanceof Error ? err.message : String(err)}`,
|
|
1815
|
-
isError: true
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
try {
|
|
1820
|
-
const originalFileContent = readFileSync4(filePath, "utf8");
|
|
1821
|
-
if (!targetContent) {
|
|
1822
|
-
return {
|
|
1823
|
-
output: `Error: File '${rawPath}' already exists, but targetContent was empty.
|
|
1824
|
-
[Systematic Error Recovery Checklist]:
|
|
1825
|
-
1. Root Cause: An existing file requires targetContent to specify which lines to replace.
|
|
1826
|
-
2. Fix: Call 'read_file' on '${rawPath}', extract the exact target lines, and provide them in 'targetContent'.
|
|
1827
|
-
3. To overwrite the whole file, use the 'write_file' tool instead.`,
|
|
1828
|
-
isError: true
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
const firstIndex = originalFileContent.indexOf(targetContent);
|
|
1832
|
-
if (firstIndex === -1) {
|
|
1833
|
-
return {
|
|
1834
|
-
output: `Error: targetContent was not found in '${rawPath}'.
|
|
1835
|
-
[Systematic Error Recovery Checklist]:
|
|
1836
|
-
1. Root Cause: The snippet in targetContent does not match the actual file content (differences in whitespace, indentation, line endings, or prior edits).
|
|
1837
|
-
2. Action: Call 'read_file' on '${rawPath}' to inspect current exact lines and indentation.
|
|
1838
|
-
3. Fix: Provide the exact matching lines (including leading spaces) or wider context, then retry 'apply_patch'.`,
|
|
1839
|
-
isError: true
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
const secondIndex = originalFileContent.indexOf(targetContent, firstIndex + 1);
|
|
1843
|
-
if (secondIndex !== -1) {
|
|
1844
|
-
return {
|
|
1845
|
-
output: `Error: targetContent matched multiple locations in '${rawPath}'.
|
|
1846
|
-
[Systematic Error Recovery Checklist]:
|
|
1847
|
-
1. Root Cause: targetContent is ambiguous and occurs multiple times in the file.
|
|
1848
|
-
2. Action: Include 2-3 additional surrounding lines (before or after the target block) to make the target snippet uniquely identifiable.
|
|
1849
|
-
3. Fix: Re-run 'apply_patch' with the extended unique block.`,
|
|
1850
|
-
isError: true
|
|
1851
|
-
};
|
|
1852
1583
|
}
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1584
|
+
const exitCode = proc.status ?? (proc.error ? 1 : 0);
|
|
1585
|
+
const success = exitCode === 0;
|
|
1855
1586
|
return {
|
|
1856
|
-
|
|
1587
|
+
command,
|
|
1588
|
+
success,
|
|
1589
|
+
exitCode,
|
|
1590
|
+
output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
|
|
1591
|
+
durationMs
|
|
1857
1592
|
};
|
|
1858
1593
|
} catch (err) {
|
|
1594
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
1859
1595
|
return {
|
|
1860
|
-
|
|
1861
|
-
|
|
1596
|
+
command,
|
|
1597
|
+
success: false,
|
|
1598
|
+
exitCode: 1,
|
|
1599
|
+
output: `Verification execution error: ${err.message || String(err)}`,
|
|
1600
|
+
durationMs
|
|
1862
1601
|
};
|
|
1863
1602
|
}
|
|
1864
1603
|
}
|
|
1865
|
-
}
|
|
1866
|
-
// src/
|
|
1604
|
+
}
|
|
1605
|
+
// src/session/turn.ts
|
|
1606
|
+
async function runTurn(session, turnContext, input) {
|
|
1607
|
+
const { turnId, signal } = turnContext;
|
|
1608
|
+
session.emitEvent({
|
|
1609
|
+
type: "TurnStarted",
|
|
1610
|
+
turnId
|
|
1611
|
+
});
|
|
1612
|
+
const currentHistory = session.getHistory();
|
|
1613
|
+
const estimatedTokens = estimateTotalTokens(currentHistory);
|
|
1614
|
+
const maxTokenLimit = DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS;
|
|
1615
|
+
if (estimatedTokens > maxTokenLimit) {
|
|
1616
|
+
const compacted = compactHistory(currentHistory);
|
|
1617
|
+
session.setHistory(compacted);
|
|
1618
|
+
session.emitEvent({
|
|
1619
|
+
type: "Warning",
|
|
1620
|
+
message: `Auto-compacted conversation history (${estimatedTokens} estimated tokens exceeded limit).`
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
const userItem = {
|
|
1624
|
+
id: `msg_user_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1625
|
+
type: "user_message",
|
|
1626
|
+
content: typeof input === "string" ? input : input?.text ?? input?.prompt ?? "",
|
|
1627
|
+
images: typeof input === "object" ? input?.images : undefined,
|
|
1628
|
+
createdAt: Date.now()
|
|
1629
|
+
};
|
|
1630
|
+
session.addHistoryItem(userItem);
|
|
1631
|
+
session.emitEvent({
|
|
1632
|
+
type: "ItemCompleted",
|
|
1633
|
+
turnId,
|
|
1634
|
+
item: userItem
|
|
1635
|
+
});
|
|
1636
|
+
const cwd = turnContext.environment.cwd;
|
|
1637
|
+
const worldState = await captureWorldState(cwd);
|
|
1638
|
+
const memoriesPrompt = session.memoryStore?.formatMemoriesPrompt(cwd);
|
|
1639
|
+
const skillsPrompt = session.skillsLoader?.formatSkillsPrompt(cwd);
|
|
1640
|
+
const mcpPrompt = session.mcpManager?.formatMcpPrompt();
|
|
1641
|
+
const effectiveSystemPrompt = buildSystemPrompt({
|
|
1642
|
+
basePrompt: session.systemPrompt || undefined,
|
|
1643
|
+
worldStatePrompt: formatWorldStatePrompt(worldState),
|
|
1644
|
+
memoriesPrompt,
|
|
1645
|
+
skillsPrompt,
|
|
1646
|
+
mcpPrompt,
|
|
1647
|
+
isOrchestrator: session.tools.has("spawn_agent"),
|
|
1648
|
+
cwd
|
|
1649
|
+
});
|
|
1650
|
+
let iteration = 0;
|
|
1651
|
+
let accumulatedInputTokens = 0;
|
|
1652
|
+
let accumulatedOutputTokens = 0;
|
|
1653
|
+
let accumulatedCachedTokens = 0;
|
|
1654
|
+
const clientSession = session.modelClient.newSession();
|
|
1655
|
+
const modifiedFiles = new Set;
|
|
1656
|
+
let selfHealingAttempts = 0;
|
|
1657
|
+
let hasRunVerification = false;
|
|
1658
|
+
try {
|
|
1659
|
+
while (iteration < turnContext.maxIterations) {
|
|
1660
|
+
if (signal.aborted) {
|
|
1661
|
+
throw new TurnAbortedError;
|
|
1662
|
+
}
|
|
1663
|
+
iteration++;
|
|
1664
|
+
let currentAgentText = "";
|
|
1665
|
+
const toolCallRequests = [];
|
|
1666
|
+
let iterInputTokens = Math.ceil((effectiveSystemPrompt.length + JSON.stringify(session.getHistory()).length) / 4);
|
|
1667
|
+
let iterOutputTokens = 0;
|
|
1668
|
+
const stream = clientSession.stream({
|
|
1669
|
+
model: turnContext.model,
|
|
1670
|
+
systemPrompt: effectiveSystemPrompt,
|
|
1671
|
+
history: session.getHistory(),
|
|
1672
|
+
tools: turnContext.tools,
|
|
1673
|
+
signal,
|
|
1674
|
+
enablePromptCache: true
|
|
1675
|
+
});
|
|
1676
|
+
for await (const chunk of stream) {
|
|
1677
|
+
if (signal.aborted) {
|
|
1678
|
+
throw new TurnAbortedError;
|
|
1679
|
+
}
|
|
1680
|
+
if (chunk.type === "reasoning_delta") {
|
|
1681
|
+
session.emitEvent({
|
|
1682
|
+
type: "ReasoningDelta",
|
|
1683
|
+
turnId,
|
|
1684
|
+
delta: chunk.delta
|
|
1685
|
+
});
|
|
1686
|
+
} else if (chunk.type === "text_delta") {
|
|
1687
|
+
currentAgentText += chunk.delta;
|
|
1688
|
+
session.emitEvent({
|
|
1689
|
+
type: "AgentMessageDelta",
|
|
1690
|
+
turnId,
|
|
1691
|
+
delta: chunk.delta
|
|
1692
|
+
});
|
|
1693
|
+
} else if (chunk.type === "tool_call") {
|
|
1694
|
+
toolCallRequests.push(chunk);
|
|
1695
|
+
} else if (chunk.type === "done") {
|
|
1696
|
+
if (chunk.inputTokens !== undefined)
|
|
1697
|
+
iterInputTokens = chunk.inputTokens;
|
|
1698
|
+
if (chunk.outputTokens !== undefined)
|
|
1699
|
+
iterOutputTokens = chunk.outputTokens;
|
|
1700
|
+
if (chunk.cachedTokens !== undefined)
|
|
1701
|
+
accumulatedCachedTokens += chunk.cachedTokens;
|
|
1702
|
+
} else if (chunk.type === "error") {
|
|
1703
|
+
throw chunk.error;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (iterOutputTokens === 0) {
|
|
1707
|
+
iterOutputTokens = Math.ceil((currentAgentText.length + JSON.stringify(toolCallRequests).length) / 4);
|
|
1708
|
+
}
|
|
1709
|
+
accumulatedInputTokens += iterInputTokens;
|
|
1710
|
+
accumulatedOutputTokens += iterOutputTokens;
|
|
1711
|
+
if (currentAgentText.trim()) {
|
|
1712
|
+
const agentItem = {
|
|
1713
|
+
id: `msg_agent_${Date.now()}`,
|
|
1714
|
+
type: "agent_message",
|
|
1715
|
+
content: currentAgentText,
|
|
1716
|
+
createdAt: Date.now()
|
|
1717
|
+
};
|
|
1718
|
+
session.addHistoryItem(agentItem);
|
|
1719
|
+
session.emitEvent({
|
|
1720
|
+
type: "ItemCompleted",
|
|
1721
|
+
turnId,
|
|
1722
|
+
item: agentItem
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
if (toolCallRequests.length > 0) {
|
|
1726
|
+
for (const toolCall of toolCallRequests) {
|
|
1727
|
+
if (!toolCall.name || !toolCall.name.trim())
|
|
1728
|
+
continue;
|
|
1729
|
+
const functionCallItem = {
|
|
1730
|
+
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1731
|
+
type: "function_call",
|
|
1732
|
+
callId: toolCall.callId,
|
|
1733
|
+
name: toolCall.name,
|
|
1734
|
+
arguments: toolCall.arguments,
|
|
1735
|
+
createdAt: Date.now()
|
|
1736
|
+
};
|
|
1737
|
+
session.addHistoryItem(functionCallItem);
|
|
1738
|
+
session.emitEvent({
|
|
1739
|
+
type: "ItemStarted",
|
|
1740
|
+
turnId,
|
|
1741
|
+
item: functionCallItem
|
|
1742
|
+
});
|
|
1743
|
+
session.emitEvent({
|
|
1744
|
+
type: "ToolCallStarted",
|
|
1745
|
+
turnId,
|
|
1746
|
+
toolName: toolCall.name,
|
|
1747
|
+
arguments: toolCall.arguments
|
|
1748
|
+
});
|
|
1749
|
+
const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
|
|
1750
|
+
cwd: turnContext.environment.cwd,
|
|
1751
|
+
turnId,
|
|
1752
|
+
signal,
|
|
1753
|
+
execPolicy: session.execPolicy,
|
|
1754
|
+
mode: session.collaborationMode,
|
|
1755
|
+
permissionMode: session.permissionMode,
|
|
1756
|
+
onPlanUpdate: (plan, explanation) => {
|
|
1757
|
+
session.emitEvent({
|
|
1758
|
+
type: "PlanUpdated",
|
|
1759
|
+
turnId,
|
|
1760
|
+
explanation,
|
|
1761
|
+
plan
|
|
1762
|
+
});
|
|
1763
|
+
},
|
|
1764
|
+
requestApproval: async (description, command, prefixRule) => {
|
|
1765
|
+
const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1766
|
+
return session.requestApproval({
|
|
1767
|
+
approvalId,
|
|
1768
|
+
turnId,
|
|
1769
|
+
toolName: toolCall.name,
|
|
1770
|
+
description,
|
|
1771
|
+
command
|
|
1772
|
+
});
|
|
1773
|
+
},
|
|
1774
|
+
requestInput: async (question, options) => {
|
|
1775
|
+
const questionId = `quest_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1776
|
+
return session.requestUserQuestion({
|
|
1777
|
+
questionId,
|
|
1778
|
+
turnId,
|
|
1779
|
+
question,
|
|
1780
|
+
options
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
const functionOutputItem = {
|
|
1785
|
+
id: `out_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1786
|
+
type: "function_call_output",
|
|
1787
|
+
callId: toolCall.callId,
|
|
1788
|
+
output: toolResult.output,
|
|
1789
|
+
isError: toolResult.isError,
|
|
1790
|
+
createdAt: Date.now()
|
|
1791
|
+
};
|
|
1792
|
+
if (!toolResult.isError) {
|
|
1793
|
+
if (toolCall.name === "apply_patch" || toolCall.name === "write_file") {
|
|
1794
|
+
const p = String(toolCall.arguments?.path || "");
|
|
1795
|
+
if (p) {
|
|
1796
|
+
modifiedFiles.add(p);
|
|
1797
|
+
hasRunVerification = false;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
session.addHistoryItem(functionOutputItem);
|
|
1802
|
+
session.emitEvent({
|
|
1803
|
+
type: "ToolCallFinished",
|
|
1804
|
+
turnId,
|
|
1805
|
+
toolName: toolCall.name,
|
|
1806
|
+
output: toolResult.output,
|
|
1807
|
+
isError: toolResult.isError
|
|
1808
|
+
});
|
|
1809
|
+
session.emitEvent({
|
|
1810
|
+
type: "ItemCompleted",
|
|
1811
|
+
turnId,
|
|
1812
|
+
item: functionOutputItem
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (session.autoVerification && modifiedFiles.size > 0 && !hasRunVerification && iteration < turnContext.maxIterations) {
|
|
1818
|
+
const verifier = new AutoVerifier({
|
|
1819
|
+
cwd,
|
|
1820
|
+
customCommand: session.autoVerificationCommand
|
|
1821
|
+
});
|
|
1822
|
+
const command = verifier.resolveVerificationCommand();
|
|
1823
|
+
if (command) {
|
|
1824
|
+
session.emitEvent({
|
|
1825
|
+
type: "VerificationStarted",
|
|
1826
|
+
turnId,
|
|
1827
|
+
command,
|
|
1828
|
+
modifiedFiles: Array.from(modifiedFiles)
|
|
1829
|
+
});
|
|
1830
|
+
const vResult = verifier.verify(Array.from(modifiedFiles));
|
|
1831
|
+
session.emitEvent({
|
|
1832
|
+
type: "VerificationCompleted",
|
|
1833
|
+
turnId,
|
|
1834
|
+
command: vResult.command,
|
|
1835
|
+
success: vResult.success,
|
|
1836
|
+
output: vResult.output,
|
|
1837
|
+
durationMs: vResult.durationMs
|
|
1838
|
+
});
|
|
1839
|
+
if (!vResult.success) {
|
|
1840
|
+
if (selfHealingAttempts < session.maxSelfHealingAttempts) {
|
|
1841
|
+
selfHealingAttempts++;
|
|
1842
|
+
session.emitEvent({
|
|
1843
|
+
type: "SelfHealingStarted",
|
|
1844
|
+
turnId,
|
|
1845
|
+
attempt: selfHealingAttempts,
|
|
1846
|
+
maxAttempts: session.maxSelfHealingAttempts,
|
|
1847
|
+
command: vResult.command,
|
|
1848
|
+
error: vResult.output
|
|
1849
|
+
});
|
|
1850
|
+
const feedbackMsg = `[Automated Self-Verification Failure]
|
|
1851
|
+
Verification command '${vResult.command}' failed with exit code ${vResult.exitCode}.
|
|
1852
|
+
|
|
1853
|
+
Error trace / compiler output:
|
|
1854
|
+
${vResult.output}
|
|
1855
|
+
|
|
1856
|
+
Modified file(s) in this turn: ${Array.from(modifiedFiles).join(", ")}
|
|
1857
|
+
|
|
1858
|
+
Self-Healing Directive (Attempt ${selfHealingAttempts} of ${session.maxSelfHealingAttempts}):
|
|
1859
|
+
1. Review the error trace above carefully and locate the exact root cause.
|
|
1860
|
+
2. Formulate and apply the necessary surgical fix using 'apply_patch' or 'write_file'.
|
|
1861
|
+
3. Do NOT conclude the turn or report to the user until this error is resolved and verification passes cleanly.`;
|
|
1862
|
+
session.addHistoryItem({
|
|
1863
|
+
id: `msg_heal_${Date.now()}`,
|
|
1864
|
+
type: "user_message",
|
|
1865
|
+
content: feedbackMsg,
|
|
1866
|
+
createdAt: Date.now()
|
|
1867
|
+
});
|
|
1868
|
+
continue;
|
|
1869
|
+
} else {
|
|
1870
|
+
session.emitEvent({
|
|
1871
|
+
type: "Warning",
|
|
1872
|
+
message: `Auto-verification failed after ${selfHealingAttempts} self-healing attempts for command: ${vResult.command}`
|
|
1873
|
+
});
|
|
1874
|
+
hasRunVerification = true;
|
|
1875
|
+
}
|
|
1876
|
+
} else {
|
|
1877
|
+
hasRunVerification = true;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!currentAgentText.trim() && toolCallRequests.length === 0) {
|
|
1882
|
+
if (iteration === 1 && iteration < turnContext.maxIterations) {
|
|
1883
|
+
session.addHistoryItem({
|
|
1884
|
+
id: `msg_nudge_${Date.now()}`,
|
|
1885
|
+
type: "user_message",
|
|
1886
|
+
content: `[Systematic ReAct Nudge]: No tool actions or answers were produced in this iteration.
|
|
1887
|
+
1. Review the user's objective and determine the immediate next action.
|
|
1888
|
+
2. If more context is required, invoke an exploration tool ('read_file', 'list_dir', 'grep_search', 'find_files').
|
|
1889
|
+
3. If ready to answer or implement, call the required mutating tool or deliver your full, concrete response now.`,
|
|
1890
|
+
createdAt: Date.now()
|
|
1891
|
+
});
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
const totalContextTokens = estimateTotalTokens(session.getHistory()) + Math.ceil(effectiveSystemPrompt.length / 4);
|
|
1898
|
+
const maxContextTokens = DEFAULT_MAX_CONTEXT_TOKENS;
|
|
1899
|
+
session.emitEvent({
|
|
1900
|
+
type: "TurnCompleted",
|
|
1901
|
+
turnId,
|
|
1902
|
+
inputTokens: accumulatedInputTokens,
|
|
1903
|
+
outputTokens: accumulatedOutputTokens,
|
|
1904
|
+
totalTokens: accumulatedInputTokens + accumulatedOutputTokens,
|
|
1905
|
+
cachedTokens: accumulatedCachedTokens > 0 ? accumulatedCachedTokens : undefined,
|
|
1906
|
+
contextTokens: totalContextTokens,
|
|
1907
|
+
maxContextTokens
|
|
1908
|
+
});
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
const isAborted = error instanceof TurnAbortedError || signal.aborted;
|
|
1911
|
+
const message = isAborted ? "Turn was interrupted" : error instanceof Error ? error.message : String(error);
|
|
1912
|
+
session.emitEvent({
|
|
1913
|
+
type: "Error",
|
|
1914
|
+
turnId,
|
|
1915
|
+
message,
|
|
1916
|
+
code: isAborted ? "TURN_ABORTED" : "EXECUTION_ERROR"
|
|
1917
|
+
});
|
|
1918
|
+
} finally {
|
|
1919
|
+
session.clearActiveTurn(turnId);
|
|
1920
|
+
globalEphemeralWorkspace.cleanupTurn(turnId);
|
|
1921
|
+
globalEphemeralWorkspace.cleanRootResidue(turnContext.environment.cwd);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/session/turn-input.ts
|
|
1926
|
+
async function handleTurnInput(session, request) {
|
|
1927
|
+
const activeTurn = session.getActiveTurn();
|
|
1928
|
+
if (!activeTurn) {
|
|
1929
|
+
const turnId = `turn_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
1930
|
+
const turnContext = new TurnContext(turnId, session.model, session.tools, { cwd: session.cwd });
|
|
1931
|
+
session.setActiveTurn(turnContext);
|
|
1932
|
+
runTurn(session, turnContext, request).catch((err) => {
|
|
1933
|
+
session.emitEvent({
|
|
1934
|
+
type: "Error",
|
|
1935
|
+
turnId,
|
|
1936
|
+
message: `Unhandled turn error: ${err instanceof Error ? err.message : String(err)}`
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
return {
|
|
1940
|
+
kind: "started",
|
|
1941
|
+
turnId
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
session.addHistoryItem({
|
|
1945
|
+
id: `steer_${Date.now()}`,
|
|
1946
|
+
type: "user_message",
|
|
1947
|
+
content: `[Steering Guidance]: ${request.text}`,
|
|
1948
|
+
images: request.images,
|
|
1949
|
+
createdAt: Date.now()
|
|
1950
|
+
});
|
|
1951
|
+
return {
|
|
1952
|
+
kind: "steered",
|
|
1953
|
+
turnId: activeTurn.turnId
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/session/submission-loop.ts
|
|
1958
|
+
async function submissionLoop(session, queue) {
|
|
1959
|
+
for await (const submission of queue) {
|
|
1960
|
+
const { op } = submission;
|
|
1961
|
+
switch (op.type) {
|
|
1962
|
+
case "TurnInput": {
|
|
1963
|
+
const text = op.request?.text ?? op.prompt ?? (typeof op.request === "string" ? op.request : "");
|
|
1964
|
+
const images = op.request?.images ?? op.images;
|
|
1965
|
+
await handleTurnInput(session, {
|
|
1966
|
+
text,
|
|
1967
|
+
images,
|
|
1968
|
+
clientId: op.request?.clientId,
|
|
1969
|
+
additionalContext: op.request?.additionalContext
|
|
1970
|
+
});
|
|
1971
|
+
break;
|
|
1972
|
+
}
|
|
1973
|
+
case "Interrupt": {
|
|
1974
|
+
session.interrupt();
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
case "ExecApproval": {
|
|
1978
|
+
session.resolveApproval(op.approvalId, op.approved);
|
|
1979
|
+
break;
|
|
1980
|
+
}
|
|
1981
|
+
case "Shutdown": {
|
|
1982
|
+
session.interrupt();
|
|
1983
|
+
session.emitEvent({
|
|
1984
|
+
type: "StatusChanged",
|
|
1985
|
+
status: "terminated"
|
|
1986
|
+
});
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/security/exec-policy.ts
|
|
1994
|
+
class ExecPolicy {
|
|
1995
|
+
rules = [];
|
|
1996
|
+
mode = "auto";
|
|
1997
|
+
constructor(initialMode = "auto") {
|
|
1998
|
+
this.mode = initialMode;
|
|
1999
|
+
this.initDefaultRules();
|
|
2000
|
+
}
|
|
2001
|
+
getMode() {
|
|
2002
|
+
return this.mode;
|
|
2003
|
+
}
|
|
2004
|
+
setMode(mode) {
|
|
2005
|
+
this.mode = mode;
|
|
2006
|
+
}
|
|
2007
|
+
initDefaultRules() {
|
|
2008
|
+
this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
|
|
2009
|
+
this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
|
|
2010
|
+
this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
|
|
2011
|
+
this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
|
|
2012
|
+
this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
|
|
2013
|
+
this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
|
|
2014
|
+
}
|
|
2015
|
+
addRule(pattern, decision, description) {
|
|
2016
|
+
this.rules.unshift({ pattern, decision, description });
|
|
2017
|
+
}
|
|
2018
|
+
shouldPromptFileEdit(filePath) {
|
|
2019
|
+
if (this.mode === "plan") {
|
|
2020
|
+
return {
|
|
2021
|
+
prompt: true,
|
|
2022
|
+
isPlanBlocked: true,
|
|
2023
|
+
reason: `[Plan Mode Gate] Approval required to mutate '${filePath || "file"}' and proceed with implementation.`
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
if (this.mode === "manual") {
|
|
2027
|
+
return {
|
|
2028
|
+
prompt: true,
|
|
2029
|
+
reason: `Manual mode requires approval to modify '${filePath || "file"}'`
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
return { prompt: false };
|
|
2033
|
+
}
|
|
2034
|
+
evaluate(command) {
|
|
2035
|
+
const trimmed = command.trim();
|
|
2036
|
+
if (this.mode === "plan") {
|
|
2037
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
|
|
2038
|
+
if (isReadOnly) {
|
|
2039
|
+
return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
decision: "prompt",
|
|
2043
|
+
reason: `[Plan Mode Gate] Approval required to execute shell command '${trimmed}' in Plan Mode`
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
if (this.mode === "manual") {
|
|
2047
|
+
return {
|
|
2048
|
+
decision: "prompt",
|
|
2049
|
+
reason: "Manual mode requires confirmation for all shell commands"
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
if (this.mode === "accept-edits") {
|
|
2053
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
|
|
2054
|
+
if (isReadOnly) {
|
|
2055
|
+
return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
|
|
2056
|
+
}
|
|
2057
|
+
return {
|
|
2058
|
+
decision: "prompt",
|
|
2059
|
+
reason: "Accept-edits mode requires approval for active shell commands"
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
for (const rule of this.rules) {
|
|
2063
|
+
if (rule.pattern.test(trimmed)) {
|
|
2064
|
+
return {
|
|
2065
|
+
decision: rule.decision,
|
|
2066
|
+
reason: rule.description
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
decision: "allow",
|
|
2072
|
+
reason: "Auto mode allows execution"
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/session/session.ts
|
|
2078
|
+
class Session {
|
|
2079
|
+
threadId;
|
|
2080
|
+
model;
|
|
2081
|
+
cwd;
|
|
2082
|
+
systemPrompt;
|
|
2083
|
+
modelClient;
|
|
2084
|
+
tools;
|
|
2085
|
+
skillsLoader;
|
|
2086
|
+
memoryStore;
|
|
2087
|
+
mcpManager;
|
|
2088
|
+
execPolicy;
|
|
2089
|
+
collaborationMode = "default";
|
|
2090
|
+
autoVerification;
|
|
2091
|
+
autoVerificationCommand;
|
|
2092
|
+
maxSelfHealingAttempts;
|
|
2093
|
+
get permissionMode() {
|
|
2094
|
+
return this.execPolicy.getMode();
|
|
2095
|
+
}
|
|
2096
|
+
setPermissionMode(mode) {
|
|
2097
|
+
this.execPolicy.setMode(mode);
|
|
2098
|
+
if (mode === "plan") {
|
|
2099
|
+
this.collaborationMode = "plan";
|
|
2100
|
+
} else if (this.collaborationMode === "plan") {
|
|
2101
|
+
this.collaborationMode = "default";
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
history = [];
|
|
2105
|
+
activeTurn = null;
|
|
2106
|
+
status = "idle";
|
|
2107
|
+
eventListeners = [];
|
|
2108
|
+
pendingApprovals = new Map;
|
|
2109
|
+
pendingUserQuestions = new Map;
|
|
2110
|
+
submissionResolvers = [];
|
|
2111
|
+
submissionQueue = [];
|
|
2112
|
+
isTerminated = false;
|
|
2113
|
+
constructor(options = {}) {
|
|
2114
|
+
this.threadId = options.threadId || `thread_${Date.now()}`;
|
|
2115
|
+
this.model = options.model || "gpt-4o";
|
|
2116
|
+
this.cwd = options.cwd || process.cwd();
|
|
2117
|
+
this.systemPrompt = options.systemPrompt || "";
|
|
2118
|
+
this.modelClient = options.modelClient || new ModelClient;
|
|
2119
|
+
this.tools = options.tools || new ToolRouter;
|
|
2120
|
+
this.skillsLoader = options.skillsLoader;
|
|
2121
|
+
this.memoryStore = options.memoryStore;
|
|
2122
|
+
this.mcpManager = options.mcpManager;
|
|
2123
|
+
this.execPolicy = options.execPolicy || new ExecPolicy;
|
|
2124
|
+
this.collaborationMode = options.collaborationMode || "default";
|
|
2125
|
+
this.autoVerification = options.autoVerification ?? (process.env.PIKAA_AUTO_VERIFY !== "0" && process.env.PIKAA_AUTO_VERIFY !== "false");
|
|
2126
|
+
this.autoVerificationCommand = options.autoVerificationCommand;
|
|
2127
|
+
this.maxSelfHealingAttempts = options.maxSelfHealingAttempts ?? 3;
|
|
2128
|
+
this.history = options.initialHistory ? [...options.initialHistory] : [];
|
|
2129
|
+
if (options.onEvent) {
|
|
2130
|
+
this.eventListeners.push(options.onEvent);
|
|
2131
|
+
}
|
|
2132
|
+
this.startSubmissionLoop();
|
|
2133
|
+
this.emitEvent({
|
|
2134
|
+
type: "SessionConfigured",
|
|
2135
|
+
threadId: this.threadId,
|
|
2136
|
+
model: this.model
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
onEvent(listener) {
|
|
2140
|
+
this.eventListeners.push(listener);
|
|
2141
|
+
return () => {
|
|
2142
|
+
this.eventListeners = this.eventListeners.filter((l) => l !== listener);
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
emitEvent(msg) {
|
|
2146
|
+
if (msg.type === "StatusChanged") {
|
|
2147
|
+
this.status = msg.status;
|
|
2148
|
+
}
|
|
2149
|
+
const event = {
|
|
2150
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
2151
|
+
timestamp: Date.now(),
|
|
2152
|
+
msg
|
|
2153
|
+
};
|
|
2154
|
+
for (const listener of this.eventListeners) {
|
|
2155
|
+
try {
|
|
2156
|
+
listener(event);
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
console.error("Error in event listener:", err);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
getHistory() {
|
|
2163
|
+
return [...this.history];
|
|
2164
|
+
}
|
|
2165
|
+
setHistory(items) {
|
|
2166
|
+
this.history = [...items];
|
|
2167
|
+
}
|
|
2168
|
+
addHistoryItem(item) {
|
|
2169
|
+
this.history.push(item);
|
|
2170
|
+
}
|
|
2171
|
+
getActiveTurn() {
|
|
2172
|
+
return this.activeTurn;
|
|
2173
|
+
}
|
|
2174
|
+
setActiveTurn(turn) {
|
|
2175
|
+
this.activeTurn = turn;
|
|
2176
|
+
this.emitEvent({
|
|
2177
|
+
type: "StatusChanged",
|
|
2178
|
+
status: "running"
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
clearActiveTurn(turnId) {
|
|
2182
|
+
if (!turnId || this.activeTurn?.turnId === turnId) {
|
|
2183
|
+
this.activeTurn = null;
|
|
2184
|
+
this.emitEvent({
|
|
2185
|
+
type: "StatusChanged",
|
|
2186
|
+
status: "idle"
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
interrupt() {
|
|
2191
|
+
if (this.activeTurn) {
|
|
2192
|
+
this.activeTurn.abort("Interrupted by user");
|
|
2193
|
+
this.clearActiveTurn();
|
|
2194
|
+
this.emitEvent({
|
|
2195
|
+
type: "StatusChanged",
|
|
2196
|
+
status: "interrupted"
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
requestApproval(params) {
|
|
2201
|
+
this.emitEvent({
|
|
2202
|
+
type: "ApprovalRequired",
|
|
2203
|
+
approvalId: params.approvalId,
|
|
2204
|
+
turnId: params.turnId,
|
|
2205
|
+
toolName: params.toolName,
|
|
2206
|
+
description: params.description,
|
|
2207
|
+
command: params.command
|
|
2208
|
+
});
|
|
2209
|
+
this.emitEvent({
|
|
2210
|
+
type: "StatusChanged",
|
|
2211
|
+
status: "waiting_approval"
|
|
2212
|
+
});
|
|
2213
|
+
return new Promise((resolve) => {
|
|
2214
|
+
this.pendingApprovals.set(params.approvalId, (approved) => {
|
|
2215
|
+
this.emitEvent({
|
|
2216
|
+
type: "StatusChanged",
|
|
2217
|
+
status: "running"
|
|
2218
|
+
});
|
|
2219
|
+
resolve(approved);
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
resolveApproval(approvalId, approved) {
|
|
2224
|
+
const resolver = this.pendingApprovals.get(approvalId);
|
|
2225
|
+
if (resolver) {
|
|
2226
|
+
this.pendingApprovals.delete(approvalId);
|
|
2227
|
+
resolver(approved);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
requestUserQuestion(params) {
|
|
2231
|
+
this.emitEvent({
|
|
2232
|
+
type: "UserQuestionRequired",
|
|
2233
|
+
questionId: params.questionId,
|
|
2234
|
+
turnId: params.turnId,
|
|
2235
|
+
question: params.question,
|
|
2236
|
+
options: params.options
|
|
2237
|
+
});
|
|
2238
|
+
this.emitEvent({
|
|
2239
|
+
type: "StatusChanged",
|
|
2240
|
+
status: "waiting_user_input"
|
|
2241
|
+
});
|
|
2242
|
+
return new Promise((resolve) => {
|
|
2243
|
+
this.pendingUserQuestions.set(params.questionId, (answer) => {
|
|
2244
|
+
this.emitEvent({
|
|
2245
|
+
type: "StatusChanged",
|
|
2246
|
+
status: "running"
|
|
2247
|
+
});
|
|
2248
|
+
resolve(answer);
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
resolveUserQuestion(questionId, answer) {
|
|
2253
|
+
const resolver = this.pendingUserQuestions.get(questionId);
|
|
2254
|
+
if (resolver) {
|
|
2255
|
+
this.pendingUserQuestions.delete(questionId);
|
|
2256
|
+
resolver(answer);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
async submit(op) {
|
|
2260
|
+
const subId = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2261
|
+
const submission = {
|
|
2262
|
+
id: subId,
|
|
2263
|
+
op,
|
|
2264
|
+
createdAt: Date.now()
|
|
2265
|
+
};
|
|
2266
|
+
const resolver = this.submissionResolvers.shift();
|
|
2267
|
+
if (resolver) {
|
|
2268
|
+
resolver(submission);
|
|
2269
|
+
} else {
|
|
2270
|
+
this.submissionQueue.push(submission);
|
|
2271
|
+
}
|
|
2272
|
+
return subId;
|
|
2273
|
+
}
|
|
2274
|
+
async prompt(text, images) {
|
|
2275
|
+
return handleTurnInput(this, { text, images });
|
|
2276
|
+
}
|
|
2277
|
+
async promptAndWait(text, images, timeoutMs = 30000) {
|
|
2278
|
+
return new Promise((resolve, reject) => {
|
|
2279
|
+
const timer = setTimeout(() => {
|
|
2280
|
+
unsub();
|
|
2281
|
+
reject(new Error(`Turn timed out after ${timeoutMs}ms`));
|
|
2282
|
+
}, timeoutMs);
|
|
2283
|
+
const unsub = this.onEvent((event) => {
|
|
2284
|
+
if (event.msg.type === "TurnCompleted") {
|
|
2285
|
+
clearTimeout(timer);
|
|
2286
|
+
unsub();
|
|
2287
|
+
resolve();
|
|
2288
|
+
} else if (event.msg.type === "Error") {
|
|
2289
|
+
clearTimeout(timer);
|
|
2290
|
+
unsub();
|
|
2291
|
+
reject(new Error(event.msg.message));
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
this.prompt(text, images).catch((err) => {
|
|
2295
|
+
clearTimeout(timer);
|
|
2296
|
+
unsub();
|
|
2297
|
+
reject(err);
|
|
2298
|
+
});
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
async* createSubmissionIterator() {
|
|
2302
|
+
while (!this.isTerminated) {
|
|
2303
|
+
if (this.submissionQueue.length > 0) {
|
|
2304
|
+
yield this.submissionQueue.shift();
|
|
2305
|
+
} else {
|
|
2306
|
+
const nextSub = await new Promise((resolve) => {
|
|
2307
|
+
this.submissionResolvers.push(resolve);
|
|
2308
|
+
});
|
|
2309
|
+
yield nextSub;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
startSubmissionLoop() {
|
|
2314
|
+
const iterator = this.createSubmissionIterator();
|
|
2315
|
+
submissionLoop(this, iterator).catch((err) => {
|
|
2316
|
+
console.error("Submission loop terminated with error:", err);
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
// src/tools/handlers/apply-patch.ts
|
|
2321
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
2322
|
+
import { resolve as resolve5, dirname as dirname3 } from "path";
|
|
2323
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
2324
|
+
var applyPatchTool = {
|
|
2325
|
+
name: "apply_patch",
|
|
2326
|
+
description: "Apply precise multi-line modifications to an existing file or create a new file. TargetContent must match the file content exactly.",
|
|
2327
|
+
parameters: {
|
|
2328
|
+
type: "object",
|
|
2329
|
+
properties: {
|
|
2330
|
+
path: {
|
|
2331
|
+
type: "string",
|
|
2332
|
+
description: "Relative or absolute path to the target file"
|
|
2333
|
+
},
|
|
2334
|
+
targetContent: {
|
|
2335
|
+
type: "string",
|
|
2336
|
+
description: "The exact block of code in the file to be replaced. For creating a new file, leave this empty."
|
|
2337
|
+
},
|
|
2338
|
+
replacementContent: {
|
|
2339
|
+
type: "string",
|
|
2340
|
+
description: "The new code content to replace the targetContent with."
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
required: ["path", "replacementContent"]
|
|
2344
|
+
},
|
|
2345
|
+
async execute(args, ctx) {
|
|
2346
|
+
const rawPath = String(args.path || "");
|
|
2347
|
+
if (!rawPath) {
|
|
2348
|
+
return { output: "Error: 'path' parameter is required", isError: true };
|
|
2349
|
+
}
|
|
2350
|
+
const filePath = resolve5(ctx.cwd, rawPath);
|
|
2351
|
+
const targetContent = typeof args.targetContent === "string" ? args.targetContent : "";
|
|
2352
|
+
const replacementContent = String(args.replacementContent ?? "");
|
|
2353
|
+
if (ctx.execPolicy) {
|
|
2354
|
+
const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
|
|
2355
|
+
if (evalResult.prompt && ctx.requestApproval) {
|
|
2356
|
+
const approval = await ctx.requestApproval(evalResult.reason || `Apply patch to: ${rawPath}`, `apply_patch ${rawPath}`);
|
|
2357
|
+
const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
|
|
2358
|
+
if (!allowed) {
|
|
2359
|
+
return {
|
|
2360
|
+
output: `[Plan Mode Gate]: File modification declined by user for '${rawPath}'. Please refine your implementation plan or ask the user for guidance.`,
|
|
2361
|
+
isError: true
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
} else if (evalResult.isPlanBlocked || ctx.mode === "plan") {
|
|
2365
|
+
return {
|
|
2366
|
+
output: `[Plan Mode Gate]: Cannot mutate '${rawPath}' while in Plan Mode without user approval. Please present your implementation plan first.`,
|
|
2367
|
+
isError: true
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!existsSync8(filePath)) {
|
|
2372
|
+
if (targetContent) {
|
|
2373
|
+
return {
|
|
2374
|
+
output: `Error: Target file '${rawPath}' does not exist, but targetContent was provided.
|
|
2375
|
+
[Systematic Error Recovery Checklist]:
|
|
2376
|
+
1. Root Cause: Trying to patch a non-existent file with targetContent.
|
|
2377
|
+
2. Fix: For creating new files, leave 'targetContent' empty and provide full contents in 'replacementContent'.
|
|
2378
|
+
3. Alternatively, check if the file path '${rawPath}' was mistyped.`,
|
|
2379
|
+
isError: true
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
mkdirSync4(dirname3(filePath), { recursive: true });
|
|
2384
|
+
writeFileSync2(filePath, replacementContent, "utf8");
|
|
2385
|
+
return { output: `Successfully created new file '${rawPath}'` };
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
return {
|
|
2388
|
+
output: `Failed to create file '${rawPath}': ${err instanceof Error ? err.message : String(err)}`,
|
|
2389
|
+
isError: true
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
try {
|
|
2394
|
+
const originalFileContent = readFileSync6(filePath, "utf8");
|
|
2395
|
+
if (!targetContent) {
|
|
2396
|
+
return {
|
|
2397
|
+
output: `Error: File '${rawPath}' already exists, but targetContent was empty.
|
|
2398
|
+
[Systematic Error Recovery Checklist]:
|
|
2399
|
+
1. Root Cause: An existing file requires targetContent to specify which lines to replace.
|
|
2400
|
+
2. Fix: Call 'read_file' on '${rawPath}', extract the exact target lines, and provide them in 'targetContent'.
|
|
2401
|
+
3. To overwrite the whole file, use the 'write_file' tool instead.`,
|
|
2402
|
+
isError: true
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
const firstIndex = originalFileContent.indexOf(targetContent);
|
|
2406
|
+
if (firstIndex === -1) {
|
|
2407
|
+
return {
|
|
2408
|
+
output: `Error: targetContent was not found in '${rawPath}'.
|
|
2409
|
+
[Systematic Error Recovery Checklist]:
|
|
2410
|
+
1. Root Cause: The snippet in targetContent does not match the actual file content (differences in whitespace, indentation, line endings, or prior edits).
|
|
2411
|
+
2. Action: Call 'read_file' on '${rawPath}' to inspect current exact lines and indentation.
|
|
2412
|
+
3. Fix: Provide the exact matching lines (including leading spaces) or wider context, then retry 'apply_patch'.`,
|
|
2413
|
+
isError: true
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const secondIndex = originalFileContent.indexOf(targetContent, firstIndex + 1);
|
|
2417
|
+
if (secondIndex !== -1) {
|
|
2418
|
+
return {
|
|
2419
|
+
output: `Error: targetContent matched multiple locations in '${rawPath}'.
|
|
2420
|
+
[Systematic Error Recovery Checklist]:
|
|
2421
|
+
1. Root Cause: targetContent is ambiguous and occurs multiple times in the file.
|
|
2422
|
+
2. Action: Include 2-3 additional surrounding lines (before or after the target block) to make the target snippet uniquely identifiable.
|
|
2423
|
+
3. Fix: Re-run 'apply_patch' with the extended unique block.`,
|
|
2424
|
+
isError: true
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
const newFileContent = originalFileContent.slice(0, firstIndex) + replacementContent + originalFileContent.slice(firstIndex + targetContent.length);
|
|
2428
|
+
writeFileSync2(filePath, newFileContent, "utf8");
|
|
2429
|
+
return {
|
|
2430
|
+
output: `Successfully applied patch to '${rawPath}'`
|
|
2431
|
+
};
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
return {
|
|
2434
|
+
output: `Failed to apply patch to '${rawPath}': ${err instanceof Error ? err.message : String(err)}`,
|
|
2435
|
+
isError: true
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
// src/security/kernel/windows.ts
|
|
1867
2441
|
import { dlopen, FFIType } from "bun:ffi";
|
|
1868
2442
|
|
|
1869
2443
|
class WindowsSandbox {
|
|
@@ -1950,7 +2524,7 @@ class WindowsSandbox {
|
|
|
1950
2524
|
}
|
|
1951
2525
|
|
|
1952
2526
|
// src/security/kernel/linux.ts
|
|
1953
|
-
import { existsSync as
|
|
2527
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1954
2528
|
|
|
1955
2529
|
class LinuxSandbox {
|
|
1956
2530
|
hasBwrap = false;
|
|
@@ -1961,7 +2535,7 @@ class LinuxSandbox {
|
|
|
1961
2535
|
if (process.platform !== "linux") {
|
|
1962
2536
|
return;
|
|
1963
2537
|
}
|
|
1964
|
-
this.hasBwrap =
|
|
2538
|
+
this.hasBwrap = existsSync9("/usr/bin/bwrap") || existsSync9("/bin/bwrap") || existsSync9("/usr/local/bin/bwrap");
|
|
1965
2539
|
}
|
|
1966
2540
|
wrapCommand(cmd, profile) {
|
|
1967
2541
|
if (!this.hasBwrap || profile.kind === "danger-unrestricted") {
|
|
@@ -1995,7 +2569,7 @@ class LinuxSandbox {
|
|
|
1995
2569
|
}
|
|
1996
2570
|
|
|
1997
2571
|
// src/security/kernel/macos.ts
|
|
1998
|
-
import { existsSync as
|
|
2572
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1999
2573
|
|
|
2000
2574
|
class MacOSSandbox {
|
|
2001
2575
|
hasSandboxExec = false;
|
|
@@ -2006,7 +2580,7 @@ class MacOSSandbox {
|
|
|
2006
2580
|
if (process.platform !== "darwin") {
|
|
2007
2581
|
return;
|
|
2008
2582
|
}
|
|
2009
|
-
this.hasSandboxExec =
|
|
2583
|
+
this.hasSandboxExec = existsSync10("/usr/bin/sandbox-exec");
|
|
2010
2584
|
}
|
|
2011
2585
|
generateProfile(profile) {
|
|
2012
2586
|
const rules = [
|
|
@@ -2116,7 +2690,7 @@ var globalKernelSandbox = new KernelSandboxManager;
|
|
|
2116
2690
|
|
|
2117
2691
|
// src/storage/prefix-rules-store.ts
|
|
2118
2692
|
import { Database } from "bun:sqlite";
|
|
2119
|
-
import { existsSync as
|
|
2693
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5 } from "fs";
|
|
2120
2694
|
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
2121
2695
|
class PrefixRulesStore {
|
|
2122
2696
|
db;
|
|
@@ -2127,7 +2701,7 @@ class PrefixRulesStore {
|
|
|
2127
2701
|
const effectivePath = dbOrPath || getPrefixRulesDbPath();
|
|
2128
2702
|
if (effectivePath !== ":memory:") {
|
|
2129
2703
|
const dir = dirname4(effectivePath);
|
|
2130
|
-
if (!
|
|
2704
|
+
if (!existsSync11(dir)) {
|
|
2131
2705
|
mkdirSync5(dir, { recursive: true });
|
|
2132
2706
|
}
|
|
2133
2707
|
}
|
|
@@ -2398,31 +2972,118 @@ ${result.stderr.trim()}`);
|
|
|
2398
2972
|
}
|
|
2399
2973
|
var shellTool = createShellTool();
|
|
2400
2974
|
// src/tools/handlers/file-ops.ts
|
|
2401
|
-
import { readdirSync as
|
|
2975
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, mkdirSync as mkdirSync6 } from "fs";
|
|
2402
2976
|
import { resolve as resolve8, dirname as dirname5 } from "path";
|
|
2977
|
+
var DEFAULT_MAX_UNPAGINATED_LINES = 250;
|
|
2403
2978
|
var readFileTool = {
|
|
2404
2979
|
name: "read_file",
|
|
2405
|
-
description: "Read
|
|
2980
|
+
description: "Read file content with surgical line-range support. Supports start_line and end_line to inspect specific sections of large files without exhausting token context.",
|
|
2406
2981
|
parameters: {
|
|
2407
2982
|
type: "object",
|
|
2408
2983
|
properties: {
|
|
2409
|
-
path: {
|
|
2984
|
+
path: {
|
|
2985
|
+
type: "string",
|
|
2986
|
+
description: "Relative or absolute path to the file."
|
|
2987
|
+
},
|
|
2988
|
+
start_line: {
|
|
2989
|
+
type: "number",
|
|
2990
|
+
description: "Optional 1-indexed line number to start reading from (e.g. 120)."
|
|
2991
|
+
},
|
|
2992
|
+
end_line: {
|
|
2993
|
+
type: "number",
|
|
2994
|
+
description: "Optional 1-indexed line number to end reading at, inclusive (e.g. 180)."
|
|
2995
|
+
},
|
|
2996
|
+
offset: {
|
|
2997
|
+
type: "number",
|
|
2998
|
+
description: "Alias for start_line (1-indexed)."
|
|
2999
|
+
},
|
|
3000
|
+
limit: {
|
|
3001
|
+
type: "number",
|
|
3002
|
+
description: "Maximum number of lines to read."
|
|
3003
|
+
},
|
|
3004
|
+
line_numbers: {
|
|
3005
|
+
type: "boolean",
|
|
3006
|
+
description: "Whether to include line number prefixes ('<line>: <content>'). Defaults to true for range reads."
|
|
3007
|
+
}
|
|
2410
3008
|
},
|
|
2411
3009
|
required: ["path"]
|
|
2412
3010
|
},
|
|
2413
3011
|
async execute(args, ctx) {
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
3012
|
+
const rawPath = String(args.path || "");
|
|
3013
|
+
const filePath = resolve8(ctx.cwd, rawPath);
|
|
3014
|
+
if (!existsSync12(filePath)) {
|
|
3015
|
+
return { output: `Error: File not found: '${rawPath}'`, isError: true };
|
|
2417
3016
|
}
|
|
2418
3017
|
try {
|
|
2419
|
-
const content =
|
|
2420
|
-
|
|
3018
|
+
const content = readFileSync7(filePath, "utf8");
|
|
3019
|
+
const lines = content.split(/\r?\n/);
|
|
3020
|
+
const totalLines = lines.length;
|
|
3021
|
+
const hasRange = args.start_line !== undefined || args.end_line !== undefined || args.startLine !== undefined || args.endLine !== undefined || args.offset !== undefined || args.limit !== undefined;
|
|
3022
|
+
if (!hasRange) {
|
|
3023
|
+
if (args.line_numbers === true) {
|
|
3024
|
+
const formatted = lines.map((l, idx) => `${idx + 1}: ${l}`).join(`
|
|
3025
|
+
`);
|
|
3026
|
+
return { output: formatted };
|
|
3027
|
+
}
|
|
3028
|
+
if (totalLines <= DEFAULT_MAX_UNPAGINATED_LINES) {
|
|
3029
|
+
return { output: content };
|
|
3030
|
+
}
|
|
3031
|
+
const truncated = lines.slice(0, DEFAULT_MAX_UNPAGINATED_LINES);
|
|
3032
|
+
const formatted = truncated.map((l, idx) => `${idx + 1}: ${l}`).join(`
|
|
3033
|
+
`);
|
|
3034
|
+
return {
|
|
3035
|
+
output: `[Showing lines 1 to ${DEFAULT_MAX_UNPAGINATED_LINES} of ${totalLines} in '${rawPath}']
|
|
3036
|
+
${formatted}
|
|
3037
|
+
|
|
3038
|
+
[Truncated: ${totalLines - DEFAULT_MAX_UNPAGINATED_LINES} more lines. Use start_line=${DEFAULT_MAX_UNPAGINATED_LINES + 1} to continue reading.]`
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
const startArg = args.start_line ?? args.startLine ?? args.offset;
|
|
3042
|
+
const start = Math.max(1, typeof startArg === "number" ? Math.floor(startArg) : 1);
|
|
3043
|
+
let end;
|
|
3044
|
+
const endArg = args.end_line ?? args.endLine;
|
|
3045
|
+
if (typeof endArg === "number") {
|
|
3046
|
+
end = Math.min(totalLines, Math.floor(endArg));
|
|
3047
|
+
} else if (typeof args.limit === "number") {
|
|
3048
|
+
end = Math.min(totalLines, start + Math.floor(args.limit) - 1);
|
|
3049
|
+
} else {
|
|
3050
|
+
end = Math.min(totalLines, start + DEFAULT_MAX_UNPAGINATED_LINES - 1);
|
|
3051
|
+
}
|
|
3052
|
+
if (start > totalLines) {
|
|
3053
|
+
return {
|
|
3054
|
+
output: `Error: start_line (${start}) exceeds total lines in file (${totalLines}).`,
|
|
3055
|
+
isError: true
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
if (end < start) {
|
|
3059
|
+
return {
|
|
3060
|
+
output: `Error: end_line (${end}) cannot be less than start_line (${start}).`,
|
|
3061
|
+
isError: true
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
const sliced = lines.slice(start - 1, end);
|
|
3065
|
+
const withNums = args.line_numbers !== false;
|
|
3066
|
+
const rendered = withNums ? sliced.map((l, idx) => `${start + idx}: ${l}`).join(`
|
|
3067
|
+
`) : sliced.join(`
|
|
3068
|
+
`);
|
|
3069
|
+
let notice = `[Showing lines ${start} to ${end} of ${totalLines} in '${rawPath}']
|
|
3070
|
+
${rendered}`;
|
|
3071
|
+
if (end < totalLines) {
|
|
3072
|
+
notice += `
|
|
3073
|
+
|
|
3074
|
+
[File has ${totalLines} lines. To read further, use start_line=${end + 1}.]`;
|
|
3075
|
+
}
|
|
3076
|
+
return { output: notice };
|
|
2421
3077
|
} catch (err) {
|
|
2422
3078
|
return { output: `Failed to read file: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
2423
3079
|
}
|
|
2424
3080
|
}
|
|
2425
3081
|
};
|
|
3082
|
+
var viewFileTool = {
|
|
3083
|
+
...readFileTool,
|
|
3084
|
+
name: "view_file",
|
|
3085
|
+
description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
|
|
3086
|
+
};
|
|
2426
3087
|
var listDirTool = {
|
|
2427
3088
|
name: "list_dir",
|
|
2428
3089
|
description: "List contents of a directory with file names and types.",
|
|
@@ -2434,14 +3095,14 @@ var listDirTool = {
|
|
|
2434
3095
|
},
|
|
2435
3096
|
async execute(args, ctx) {
|
|
2436
3097
|
const dirPath = resolve8(ctx.cwd, String(args.path || "."));
|
|
2437
|
-
if (!
|
|
3098
|
+
if (!existsSync12(dirPath)) {
|
|
2438
3099
|
return { output: `Error: Directory not found: '${args.path}'`, isError: true };
|
|
2439
3100
|
}
|
|
2440
3101
|
try {
|
|
2441
|
-
const entries =
|
|
3102
|
+
const entries = readdirSync4(dirPath);
|
|
2442
3103
|
const formatted = entries.map((entry) => {
|
|
2443
3104
|
const full = resolve8(dirPath, entry);
|
|
2444
|
-
const isDir =
|
|
3105
|
+
const isDir = statSync4(full).isDirectory();
|
|
2445
3106
|
return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
|
|
2446
3107
|
});
|
|
2447
3108
|
return { output: formatted.join(`
|
|
@@ -2614,8 +3275,8 @@ var updatePlanTool = {
|
|
|
2614
3275
|
}
|
|
2615
3276
|
};
|
|
2616
3277
|
// src/search/engine.ts
|
|
2617
|
-
import { readdirSync as
|
|
2618
|
-
import { resolve as resolve9, relative, join as
|
|
3278
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5, existsSync as existsSync13 } from "fs";
|
|
3279
|
+
import { resolve as resolve9, relative, join as join7, extname } from "path";
|
|
2619
3280
|
var DEFAULT_IGNORE_DIRS = new Set([
|
|
2620
3281
|
".git",
|
|
2621
3282
|
"node_modules",
|
|
@@ -2661,7 +3322,7 @@ var BINARY_EXTENSIONS = new Set([
|
|
|
2661
3322
|
class FileSearchEngine {
|
|
2662
3323
|
grep(cwd, options) {
|
|
2663
3324
|
const searchRoot = resolve9(cwd, options.path || ".");
|
|
2664
|
-
if (!
|
|
3325
|
+
if (!existsSync13(searchRoot)) {
|
|
2665
3326
|
return { matches: [], totalMatches: 0, truncated: false };
|
|
2666
3327
|
}
|
|
2667
3328
|
const maxResults = options.maxResults || 50;
|
|
@@ -2686,7 +3347,7 @@ class FileSearchEngine {
|
|
|
2686
3347
|
if (truncated)
|
|
2687
3348
|
break;
|
|
2688
3349
|
try {
|
|
2689
|
-
const content =
|
|
3350
|
+
const content = readFileSync8(filePath, "utf8");
|
|
2690
3351
|
const lines = content.split(`
|
|
2691
3352
|
`);
|
|
2692
3353
|
for (let i = 0;i < lines.length; i++) {
|
|
@@ -2711,7 +3372,7 @@ class FileSearchEngine {
|
|
|
2711
3372
|
}
|
|
2712
3373
|
findFiles(cwd, options) {
|
|
2713
3374
|
const searchRoot = resolve9(cwd, options.path || ".");
|
|
2714
|
-
if (!
|
|
3375
|
+
if (!existsSync13(searchRoot))
|
|
2715
3376
|
return [];
|
|
2716
3377
|
const maxResults = options.maxResults || 100;
|
|
2717
3378
|
const gitignoreRules = this.loadGitignoreRules(searchRoot);
|
|
@@ -2750,10 +3411,10 @@ class FileSearchEngine {
|
|
|
2750
3411
|
}
|
|
2751
3412
|
loadGitignoreRules(root) {
|
|
2752
3413
|
const rules = new Set;
|
|
2753
|
-
const gitignorePath =
|
|
2754
|
-
if (
|
|
3414
|
+
const gitignorePath = join7(root, ".gitignore");
|
|
3415
|
+
if (existsSync13(gitignorePath)) {
|
|
2755
3416
|
try {
|
|
2756
|
-
const lines =
|
|
3417
|
+
const lines = readFileSync8(gitignorePath, "utf8").split(`
|
|
2757
3418
|
`);
|
|
2758
3419
|
for (const line of lines) {
|
|
2759
3420
|
const trimmed = line.trim();
|
|
@@ -2768,7 +3429,7 @@ class FileSearchEngine {
|
|
|
2768
3429
|
collectFiles(dir, root, gitignoreRules, includePattern) {
|
|
2769
3430
|
const results = [];
|
|
2770
3431
|
try {
|
|
2771
|
-
const stat =
|
|
3432
|
+
const stat = statSync5(dir);
|
|
2772
3433
|
if (!stat.isDirectory()) {
|
|
2773
3434
|
if (!this.isBinary(dir)) {
|
|
2774
3435
|
results.push(dir);
|
|
@@ -2782,9 +3443,9 @@ class FileSearchEngine {
|
|
|
2782
3443
|
while (queue.length > 0) {
|
|
2783
3444
|
const currentDir = queue.shift();
|
|
2784
3445
|
try {
|
|
2785
|
-
const entries =
|
|
3446
|
+
const entries = readdirSync5(currentDir, { withFileTypes: true });
|
|
2786
3447
|
for (const entry of entries) {
|
|
2787
|
-
const fullPath =
|
|
3448
|
+
const fullPath = join7(currentDir, entry.name);
|
|
2788
3449
|
const relToRoot = relative(root, fullPath).replace(/\\/g, "/");
|
|
2789
3450
|
if (this.isIgnored(entry.name, relToRoot, gitignoreRules)) {
|
|
2790
3451
|
continue;
|
|
@@ -2936,7 +3597,13 @@ function createFileSearchTools(engine = new FileSearchEngine) {
|
|
|
2936
3597
|
`) };
|
|
2937
3598
|
}
|
|
2938
3599
|
};
|
|
2939
|
-
|
|
3600
|
+
const findByNameTool = {
|
|
3601
|
+
name: "find_by_name",
|
|
3602
|
+
description: "Search for files and directories across the workspace matching a name or glob pattern. Alias for find_files matching Antigravity & Claude Code conventions.",
|
|
3603
|
+
parameters: findFilesTool.parameters,
|
|
3604
|
+
execute: findFilesTool.execute
|
|
3605
|
+
};
|
|
3606
|
+
return [grepSearchTool, findFilesTool, findByNameTool];
|
|
2940
3607
|
}
|
|
2941
3608
|
|
|
2942
3609
|
// src/code-mode/tools-proxy.ts
|
|
@@ -3455,6 +4122,7 @@ function createDefaultTools(options = {}) {
|
|
|
3455
4122
|
router2.register(applyPatchTool);
|
|
3456
4123
|
router2.register(shellTool);
|
|
3457
4124
|
router2.register(readFileTool);
|
|
4125
|
+
router2.register(viewFileTool);
|
|
3458
4126
|
router2.register(writeFileTool);
|
|
3459
4127
|
router2.register(listDirTool);
|
|
3460
4128
|
router2.register(requestUserInputTool);
|
|
@@ -3483,8 +4151,8 @@ function createDefaultTools(options = {}) {
|
|
|
3483
4151
|
}
|
|
3484
4152
|
|
|
3485
4153
|
// src/agents/roles.ts
|
|
3486
|
-
import { existsSync as
|
|
3487
|
-
import { resolve as resolve10, join as
|
|
4154
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
|
|
4155
|
+
import { resolve as resolve10, join as join8 } from "path";
|
|
3488
4156
|
|
|
3489
4157
|
class AgentRoleRegistry {
|
|
3490
4158
|
roles = new Map;
|
|
@@ -3569,13 +4237,13 @@ class AgentRoleRegistry {
|
|
|
3569
4237
|
}
|
|
3570
4238
|
loadRolesFromDir(dirPath) {
|
|
3571
4239
|
const fullPath = resolve10(dirPath);
|
|
3572
|
-
if (!
|
|
4240
|
+
if (!existsSync14(fullPath))
|
|
3573
4241
|
return;
|
|
3574
|
-
const entries =
|
|
4242
|
+
const entries = readdirSync6(fullPath);
|
|
3575
4243
|
for (const entry of entries) {
|
|
3576
4244
|
if (entry.endsWith(".json")) {
|
|
3577
4245
|
try {
|
|
3578
|
-
const content =
|
|
4246
|
+
const content = readFileSync9(join8(fullPath, entry), "utf8");
|
|
3579
4247
|
const parsed = JSON.parse(content);
|
|
3580
4248
|
if (parsed.name && parsed.systemPrompt) {
|
|
3581
4249
|
this.registerRole(parsed);
|
|
@@ -3628,7 +4296,7 @@ function createAgentIdentity(parentId, harnessId = "groupy-harness-v1") {
|
|
|
3628
4296
|
// src/agents/graph-store.ts
|
|
3629
4297
|
import { Database as Database2 } from "bun:sqlite";
|
|
3630
4298
|
import { resolve as resolve11 } from "path";
|
|
3631
|
-
import { existsSync as
|
|
4299
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7 } from "fs";
|
|
3632
4300
|
class AgentGraphStore {
|
|
3633
4301
|
db;
|
|
3634
4302
|
constructor(dbPathOrDb) {
|
|
@@ -3638,7 +4306,7 @@ class AgentGraphStore {
|
|
|
3638
4306
|
const dbPath = dbPathOrDb || getAgentGraphDbPath();
|
|
3639
4307
|
if (dbPath !== ":memory:") {
|
|
3640
4308
|
const dir = resolve11(dbPath, "..");
|
|
3641
|
-
if (!
|
|
4309
|
+
if (!existsSync15(dir)) {
|
|
3642
4310
|
mkdirSync7(dir, { recursive: true });
|
|
3643
4311
|
}
|
|
3644
4312
|
}
|
|
@@ -4063,8 +4731,8 @@ function registerMultiAgentTools(router, spawner) {
|
|
|
4063
4731
|
}
|
|
4064
4732
|
|
|
4065
4733
|
// src/mcp/manager.ts
|
|
4066
|
-
import { existsSync as
|
|
4067
|
-
import { resolve as resolve12, dirname as dirname6, join as
|
|
4734
|
+
import { existsSync as existsSync16, readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync8 } from "fs";
|
|
4735
|
+
import { resolve as resolve12, dirname as dirname6, join as join9 } from "path";
|
|
4068
4736
|
|
|
4069
4737
|
// src/mcp/client.ts
|
|
4070
4738
|
class McpClient {
|
|
@@ -4262,7 +4930,7 @@ class McpClient {
|
|
|
4262
4930
|
}
|
|
4263
4931
|
|
|
4264
4932
|
// src/mcp/process-killer.ts
|
|
4265
|
-
import { spawnSync } from "child_process";
|
|
4933
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
4266
4934
|
|
|
4267
4935
|
class GlobalProcessRegistry {
|
|
4268
4936
|
static trackedProcesses = new Map;
|
|
@@ -4272,7 +4940,7 @@ class GlobalProcessRegistry {
|
|
|
4272
4940
|
return;
|
|
4273
4941
|
if (process.platform === "win32") {
|
|
4274
4942
|
try {
|
|
4275
|
-
|
|
4943
|
+
spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
4276
4944
|
stdio: "ignore",
|
|
4277
4945
|
windowsHide: true
|
|
4278
4946
|
});
|
|
@@ -4680,11 +5348,11 @@ class McpManager {
|
|
|
4680
5348
|
}
|
|
4681
5349
|
async loadConfigFile(filePath) {
|
|
4682
5350
|
const fullPath = resolve12(filePath);
|
|
4683
|
-
if (!
|
|
5351
|
+
if (!existsSync16(fullPath))
|
|
4684
5352
|
return;
|
|
4685
5353
|
this.loadedConfigFiles.add(fullPath);
|
|
4686
5354
|
try {
|
|
4687
|
-
const content =
|
|
5355
|
+
const content = readFileSync10(fullPath, "utf8");
|
|
4688
5356
|
const parsed = JSON.parse(content);
|
|
4689
5357
|
if (parsed.mcpServers) {
|
|
4690
5358
|
await this.loadConfig(parsed);
|
|
@@ -4935,13 +5603,13 @@ class McpManager {
|
|
|
4935
5603
|
saveServerToConfigFile(filePath, name, config) {
|
|
4936
5604
|
const fullPath = resolve12(filePath);
|
|
4937
5605
|
const dir = dirname6(fullPath);
|
|
4938
|
-
if (!
|
|
5606
|
+
if (!existsSync16(dir)) {
|
|
4939
5607
|
mkdirSync8(dir, { recursive: true });
|
|
4940
5608
|
}
|
|
4941
5609
|
let existing = { mcpServers: {} };
|
|
4942
|
-
if (
|
|
5610
|
+
if (existsSync16(fullPath)) {
|
|
4943
5611
|
try {
|
|
4944
|
-
const content =
|
|
5612
|
+
const content = readFileSync10(fullPath, "utf8");
|
|
4945
5613
|
existing = JSON.parse(content);
|
|
4946
5614
|
if (!existing.mcpServers)
|
|
4947
5615
|
existing.mcpServers = {};
|
|
@@ -4954,10 +5622,10 @@ class McpManager {
|
|
|
4954
5622
|
}
|
|
4955
5623
|
removeServerFromConfigFile(filePath, name) {
|
|
4956
5624
|
const fullPath = resolve12(filePath);
|
|
4957
|
-
if (!
|
|
5625
|
+
if (!existsSync16(fullPath))
|
|
4958
5626
|
return false;
|
|
4959
5627
|
try {
|
|
4960
|
-
const content =
|
|
5628
|
+
const content = readFileSync10(fullPath, "utf8");
|
|
4961
5629
|
const existing = JSON.parse(content);
|
|
4962
5630
|
if (existing.mcpServers && existing.mcpServers[name]) {
|
|
4963
5631
|
delete existing.mcpServers[name];
|
|
@@ -4984,11 +5652,11 @@ class McpManager {
|
|
|
4984
5652
|
}
|
|
4985
5653
|
}
|
|
4986
5654
|
getDefaultConfigFile(cwd = process.cwd()) {
|
|
4987
|
-
const workspaceConfig =
|
|
4988
|
-
if (
|
|
5655
|
+
const workspaceConfig = join9(cwd, ".mcp.json");
|
|
5656
|
+
if (existsSync16(workspaceConfig))
|
|
4989
5657
|
return workspaceConfig;
|
|
4990
|
-
const altConfig =
|
|
4991
|
-
if (
|
|
5658
|
+
const altConfig = join9(cwd, "mcp_config.json");
|
|
5659
|
+
if (existsSync16(altConfig))
|
|
4992
5660
|
return altConfig;
|
|
4993
5661
|
return workspaceConfig;
|
|
4994
5662
|
}
|
|
@@ -5009,7 +5677,7 @@ class McpManager {
|
|
|
5009
5677
|
|
|
5010
5678
|
// src/storage/sqlite-store.ts
|
|
5011
5679
|
import { Database as Database3 } from "bun:sqlite";
|
|
5012
|
-
import { existsSync as
|
|
5680
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
|
|
5013
5681
|
import { dirname as dirname7 } from "path";
|
|
5014
5682
|
class SqliteThreadStore {
|
|
5015
5683
|
db;
|
|
@@ -5017,7 +5685,7 @@ class SqliteThreadStore {
|
|
|
5017
5685
|
const effectivePath = dbPath || this.getDefaultDbPath();
|
|
5018
5686
|
if (effectivePath !== ":memory:") {
|
|
5019
5687
|
const dir = dirname7(effectivePath);
|
|
5020
|
-
if (!
|
|
5688
|
+
if (!existsSync17(dir)) {
|
|
5021
5689
|
mkdirSync9(dir, { recursive: true });
|
|
5022
5690
|
}
|
|
5023
5691
|
}
|
|
@@ -5273,8 +5941,8 @@ class SessionPersistenceManager {
|
|
|
5273
5941
|
}
|
|
5274
5942
|
|
|
5275
5943
|
// src/skills/loader.ts
|
|
5276
|
-
import { existsSync as
|
|
5277
|
-
import { resolve as resolve13, join as
|
|
5944
|
+
import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
|
|
5945
|
+
import { resolve as resolve13, join as join10 } from "path";
|
|
5278
5946
|
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/skills";
|
|
5279
5947
|
|
|
5280
5948
|
class SkillsLoader {
|
|
@@ -5344,7 +6012,7 @@ class SkillsLoader {
|
|
|
5344
6012
|
resolve13(cwd, "skills")
|
|
5345
6013
|
];
|
|
5346
6014
|
for (const cand of candidates) {
|
|
5347
|
-
if (
|
|
6015
|
+
if (existsSync18(cand) && !roots.includes(cand)) {
|
|
5348
6016
|
roots.push(cand);
|
|
5349
6017
|
}
|
|
5350
6018
|
}
|
|
@@ -5353,7 +6021,7 @@ class SkillsLoader {
|
|
|
5353
6021
|
roots.push(getGlobalSkillsDir());
|
|
5354
6022
|
}
|
|
5355
6023
|
roots.push(...this.customRoots.map((r) => resolve13(r)));
|
|
5356
|
-
return roots.filter((r) =>
|
|
6024
|
+
return roots.filter((r) => existsSync18(r));
|
|
5357
6025
|
}
|
|
5358
6026
|
discoverSkills(cwd, options) {
|
|
5359
6027
|
return this.listSkills(cwd, options);
|
|
@@ -5369,12 +6037,12 @@ class SkillsLoader {
|
|
|
5369
6037
|
const discovered = new Map;
|
|
5370
6038
|
for (const root of roots) {
|
|
5371
6039
|
try {
|
|
5372
|
-
const entries =
|
|
6040
|
+
const entries = readdirSync7(root, { withFileTypes: true });
|
|
5373
6041
|
for (const entry of entries) {
|
|
5374
6042
|
if (entry.isDirectory()) {
|
|
5375
|
-
const skillDir =
|
|
5376
|
-
const skillFilePath =
|
|
5377
|
-
if (
|
|
6043
|
+
const skillDir = join10(root, entry.name);
|
|
6044
|
+
const skillFilePath = join10(skillDir, "SKILL.md");
|
|
6045
|
+
if (existsSync18(skillFilePath)) {
|
|
5378
6046
|
const meta = this.parseSkillFrontmatter(skillFilePath, entry.name, root, cwd);
|
|
5379
6047
|
if (meta && !discovered.has(meta.name)) {
|
|
5380
6048
|
meta.enabled = !this.isSkillDisabled(meta.name);
|
|
@@ -5403,7 +6071,7 @@ class SkillsLoader {
|
|
|
5403
6071
|
if (!meta)
|
|
5404
6072
|
return null;
|
|
5405
6073
|
try {
|
|
5406
|
-
const raw =
|
|
6074
|
+
const raw = readFileSync11(meta.path, "utf8");
|
|
5407
6075
|
const { body } = this.extractFrontmatterAndBody(raw);
|
|
5408
6076
|
return {
|
|
5409
6077
|
...meta,
|
|
@@ -5415,7 +6083,7 @@ class SkillsLoader {
|
|
|
5415
6083
|
}
|
|
5416
6084
|
parseSkillFrontmatter(filePath, dirName, root, cwd) {
|
|
5417
6085
|
try {
|
|
5418
|
-
const raw =
|
|
6086
|
+
const raw = readFileSync11(filePath, "utf8");
|
|
5419
6087
|
const { attributes } = this.extractFrontmatterAndBody(raw);
|
|
5420
6088
|
let scope = "global";
|
|
5421
6089
|
const normPath = filePath.toLowerCase().replace(/\\/g, "/");
|
|
@@ -5497,8 +6165,8 @@ When tackling complex specialized tasks that match any of these skills, autonomo
|
|
|
5497
6165
|
}
|
|
5498
6166
|
|
|
5499
6167
|
// src/skills/installer.ts
|
|
5500
|
-
import { existsSync as
|
|
5501
|
-
import { resolve as resolve14, join as
|
|
6168
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync5, rmSync as rmSync2 } from "fs";
|
|
6169
|
+
import { resolve as resolve14, join as join11 } from "path";
|
|
5502
6170
|
async function installSkill(skillName, options) {
|
|
5503
6171
|
const cleanName = skillName.trim().toLowerCase().replace(/^@/, "");
|
|
5504
6172
|
if (!cleanName) {
|
|
@@ -5533,7 +6201,7 @@ async function installSkill(skillName, options) {
|
|
|
5533
6201
|
const scope = options.global ? "global" : "workspace";
|
|
5534
6202
|
const targetDir = options.global ? resolve14(getGlobalSkillsDir(), cleanName) : resolve14(options.cwd, ".agents", "skills", cleanName);
|
|
5535
6203
|
mkdirSync10(targetDir, { recursive: true });
|
|
5536
|
-
const targetFile =
|
|
6204
|
+
const targetFile = join11(targetDir, "SKILL.md");
|
|
5537
6205
|
let fileContent = skillData.content;
|
|
5538
6206
|
if (!fileContent.startsWith("---")) {
|
|
5539
6207
|
const desc = skillData.description ? `description: "${skillData.description.replace(/"/g, "\\\"")}"
|
|
@@ -5557,7 +6225,7 @@ ${fileContent}`;
|
|
|
5557
6225
|
function removeSkill(skillName, options) {
|
|
5558
6226
|
const cleanName = skillName.trim().toLowerCase().replace(/^@/, "");
|
|
5559
6227
|
const targetDir = options.global ? resolve14(getGlobalSkillsDir(), cleanName) : resolve14(options.cwd, ".agents", "skills", cleanName);
|
|
5560
|
-
if (!
|
|
6228
|
+
if (!existsSync19(targetDir)) {
|
|
5561
6229
|
return { success: false, targetDir, removed: false };
|
|
5562
6230
|
}
|
|
5563
6231
|
rmSync2(targetDir, { recursive: true, force: true });
|
|
@@ -5565,8 +6233,8 @@ function removeSkill(skillName, options) {
|
|
|
5565
6233
|
}
|
|
5566
6234
|
|
|
5567
6235
|
// src/memories/store.ts
|
|
5568
|
-
import { existsSync as
|
|
5569
|
-
import { resolve as resolve15, join as
|
|
6236
|
+
import { existsSync as existsSync20, readFileSync as readFileSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync11, readdirSync as readdirSync8 } from "fs";
|
|
6237
|
+
import { resolve as resolve15, join as join12, basename as basename2, dirname as dirname8 } from "path";
|
|
5570
6238
|
import { createHash } from "crypto";
|
|
5571
6239
|
class MemoryStore {
|
|
5572
6240
|
globalPath;
|
|
@@ -5578,7 +6246,7 @@ class MemoryStore {
|
|
|
5578
6246
|
findProjectRoot(cwd) {
|
|
5579
6247
|
let current = resolve15(cwd);
|
|
5580
6248
|
while (true) {
|
|
5581
|
-
if (
|
|
6249
|
+
if (existsSync20(join12(current, ".git"))) {
|
|
5582
6250
|
return current;
|
|
5583
6251
|
}
|
|
5584
6252
|
const parent = dirname8(current);
|
|
@@ -5590,14 +6258,14 @@ class MemoryStore {
|
|
|
5590
6258
|
}
|
|
5591
6259
|
getProjectSlug(cwd) {
|
|
5592
6260
|
const root = this.findProjectRoot(cwd);
|
|
5593
|
-
const folderName =
|
|
6261
|
+
const folderName = basename2(root).toLowerCase().replace(/[^a-z0-9_-]/g, "-") || "project";
|
|
5594
6262
|
const hash = createHash("sha256").update(resolve15(root)).digest("hex").slice(0, 6);
|
|
5595
6263
|
return `${folderName}-${hash}`;
|
|
5596
6264
|
}
|
|
5597
6265
|
getProjectMemoryDir(cwd) {
|
|
5598
6266
|
if (this.customWorkspacePath) {
|
|
5599
6267
|
const dir = resolve15(this.customWorkspacePath);
|
|
5600
|
-
if (!
|
|
6268
|
+
if (!existsSync20(dir)) {
|
|
5601
6269
|
try {
|
|
5602
6270
|
mkdirSync11(dir, { recursive: true });
|
|
5603
6271
|
} catch {}
|
|
@@ -5605,8 +6273,8 @@ class MemoryStore {
|
|
|
5605
6273
|
return dir;
|
|
5606
6274
|
}
|
|
5607
6275
|
const slug = this.getProjectSlug(cwd);
|
|
5608
|
-
const dir =
|
|
5609
|
-
if (!
|
|
6276
|
+
const dir = join12(getProjectsDir(), slug, "memory");
|
|
6277
|
+
if (!existsSync20(dir)) {
|
|
5610
6278
|
try {
|
|
5611
6279
|
mkdirSync11(dir, { recursive: true });
|
|
5612
6280
|
} catch {}
|
|
@@ -5614,7 +6282,7 @@ class MemoryStore {
|
|
|
5614
6282
|
return dir;
|
|
5615
6283
|
}
|
|
5616
6284
|
getMemoryIndexPath(cwd) {
|
|
5617
|
-
return
|
|
6285
|
+
return join12(this.getProjectMemoryDir(cwd), "MEMORY.md");
|
|
5618
6286
|
}
|
|
5619
6287
|
normalizeCategory(raw) {
|
|
5620
6288
|
const cat = raw.toLowerCase().trim();
|
|
@@ -5633,7 +6301,7 @@ class MemoryStore {
|
|
|
5633
6301
|
const sanitizedName = params.name.toLowerCase().trim().replace(/[^a-z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || `note_${Date.now()}`;
|
|
5634
6302
|
const memoryDir = this.getProjectMemoryDir(params.cwd);
|
|
5635
6303
|
const fileName = `${type}_${sanitizedName}.md`;
|
|
5636
|
-
const filePath =
|
|
6304
|
+
const filePath = join12(memoryDir, fileName);
|
|
5637
6305
|
const nowIso = new Date().toISOString();
|
|
5638
6306
|
const cleanContent = params.content.trim();
|
|
5639
6307
|
const desc = (params.description || cleanContent.split(`
|
|
@@ -5668,23 +6336,23 @@ class MemoryStore {
|
|
|
5668
6336
|
}
|
|
5669
6337
|
readTopicMemory(topicNameOrFile, cwd) {
|
|
5670
6338
|
const memoryDir = this.getProjectMemoryDir(cwd);
|
|
5671
|
-
let targetPath =
|
|
5672
|
-
if (!
|
|
6339
|
+
let targetPath = join12(memoryDir, topicNameOrFile);
|
|
6340
|
+
if (!existsSync20(targetPath)) {
|
|
5673
6341
|
if (!topicNameOrFile.endsWith(".md")) {
|
|
5674
|
-
targetPath =
|
|
6342
|
+
targetPath = join12(memoryDir, `${topicNameOrFile}.md`);
|
|
5675
6343
|
}
|
|
5676
6344
|
}
|
|
5677
|
-
if (!
|
|
5678
|
-
const files =
|
|
6345
|
+
if (!existsSync20(targetPath)) {
|
|
6346
|
+
const files = readdirSync8(memoryDir);
|
|
5679
6347
|
const match = files.find((f) => f.includes(topicNameOrFile));
|
|
5680
6348
|
if (match) {
|
|
5681
|
-
targetPath =
|
|
6349
|
+
targetPath = join12(memoryDir, match);
|
|
5682
6350
|
} else {
|
|
5683
6351
|
return null;
|
|
5684
6352
|
}
|
|
5685
6353
|
}
|
|
5686
6354
|
try {
|
|
5687
|
-
const raw =
|
|
6355
|
+
const raw = readFileSync12(targetPath, "utf8");
|
|
5688
6356
|
return this.parseTopicFile(raw, targetPath);
|
|
5689
6357
|
} catch {
|
|
5690
6358
|
return null;
|
|
@@ -5695,7 +6363,7 @@ class MemoryStore {
|
|
|
5695
6363
|
`);
|
|
5696
6364
|
let inFm = false;
|
|
5697
6365
|
let type = "project";
|
|
5698
|
-
let name =
|
|
6366
|
+
let name = basename2(filePath, ".md");
|
|
5699
6367
|
let description;
|
|
5700
6368
|
let modified = new Date().toISOString();
|
|
5701
6369
|
const bodyLines = [];
|
|
@@ -5739,13 +6407,13 @@ class MemoryStore {
|
|
|
5739
6407
|
}
|
|
5740
6408
|
syncMemoryIndex(cwd) {
|
|
5741
6409
|
const memoryDir = this.getProjectMemoryDir(cwd);
|
|
5742
|
-
const indexPath =
|
|
5743
|
-
const files =
|
|
6410
|
+
const indexPath = join12(memoryDir, "MEMORY.md");
|
|
6411
|
+
const files = existsSync20(memoryDir) ? readdirSync8(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md") : [];
|
|
5744
6412
|
const items = [];
|
|
5745
6413
|
for (const f of files) {
|
|
5746
6414
|
try {
|
|
5747
|
-
const full =
|
|
5748
|
-
const parsed = this.parseTopicFile(
|
|
6415
|
+
const full = join12(memoryDir, f);
|
|
6416
|
+
const parsed = this.parseTopicFile(readFileSync12(full, "utf8"), full);
|
|
5749
6417
|
items.push({
|
|
5750
6418
|
type: parsed.type,
|
|
5751
6419
|
name: parsed.name,
|
|
@@ -5771,10 +6439,10 @@ class MemoryStore {
|
|
|
5771
6439
|
}
|
|
5772
6440
|
loadMemoryIndex(cwd) {
|
|
5773
6441
|
const indexPath = this.getMemoryIndexPath(cwd);
|
|
5774
|
-
if (!
|
|
6442
|
+
if (!existsSync20(indexPath))
|
|
5775
6443
|
return "";
|
|
5776
6444
|
try {
|
|
5777
|
-
const raw =
|
|
6445
|
+
const raw = readFileSync12(indexPath, "utf8");
|
|
5778
6446
|
const byteLimit = 25 * 1024;
|
|
5779
6447
|
const sliced = raw.length > byteLimit ? raw.slice(0, byteLimit) : raw;
|
|
5780
6448
|
const lines = sliced.split(`
|
|
@@ -5787,14 +6455,14 @@ class MemoryStore {
|
|
|
5787
6455
|
}
|
|
5788
6456
|
listProjectMemories(cwd) {
|
|
5789
6457
|
const memoryDir = this.getProjectMemoryDir(cwd);
|
|
5790
|
-
if (!
|
|
6458
|
+
if (!existsSync20(memoryDir))
|
|
5791
6459
|
return [];
|
|
5792
|
-
const files =
|
|
6460
|
+
const files = readdirSync8(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
|
|
5793
6461
|
const list = [];
|
|
5794
6462
|
for (const f of files) {
|
|
5795
6463
|
try {
|
|
5796
|
-
const full =
|
|
5797
|
-
list.push(this.parseTopicFile(
|
|
6464
|
+
const full = join12(memoryDir, f);
|
|
6465
|
+
list.push(this.parseTopicFile(readFileSync12(full, "utf8"), full));
|
|
5798
6466
|
} catch {}
|
|
5799
6467
|
}
|
|
5800
6468
|
return list;
|
|
@@ -5845,8 +6513,8 @@ class MemoryStore {
|
|
|
5845
6513
|
}
|
|
5846
6514
|
|
|
5847
6515
|
// src/worktree/manager.ts
|
|
5848
|
-
import { resolve as resolve17, join as
|
|
5849
|
-
import { existsSync as
|
|
6516
|
+
import { resolve as resolve17, join as join13 } from "path";
|
|
6517
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync12, writeFileSync as writeFileSync7, readFileSync as readFileSync13 } from "fs";
|
|
5850
6518
|
|
|
5851
6519
|
// src/worktree/git.ts
|
|
5852
6520
|
import { resolve as resolve16 } from "path";
|
|
@@ -5996,7 +6664,7 @@ class WorktreeManager {
|
|
|
5996
6664
|
const branchName = options.branch || `groupy/${taskId}`;
|
|
5997
6665
|
const targetDir = options.worktreePath || (this.baseStorageDir ? resolve17(this.baseStorageDir, branchName.replace(/\//g, "_")) : resolve17(repoRoot, ".groupy", "worktrees", branchName.replace(/\//g, "_")));
|
|
5998
6666
|
const worktreeParent = resolve17(targetDir, "..");
|
|
5999
|
-
if (!
|
|
6667
|
+
if (!existsSync21(worktreeParent)) {
|
|
6000
6668
|
mkdirSync12(worktreeParent, { recursive: true });
|
|
6001
6669
|
}
|
|
6002
6670
|
const baseBranch = options.baseBranch || await getCurrentBranch(repoRoot);
|
|
@@ -6004,7 +6672,7 @@ class WorktreeManager {
|
|
|
6004
6672
|
if (!result.success) {
|
|
6005
6673
|
throw new Error(`Failed to create git worktree: ${result.error}`);
|
|
6006
6674
|
}
|
|
6007
|
-
const metaPath =
|
|
6675
|
+
const metaPath = join13(targetDir, "groupy-thread.json");
|
|
6008
6676
|
try {
|
|
6009
6677
|
writeFileSync7(metaPath, JSON.stringify({
|
|
6010
6678
|
version: 1,
|
|
@@ -6030,10 +6698,10 @@ class WorktreeManager {
|
|
|
6030
6698
|
return [];
|
|
6031
6699
|
const worktrees = await listWorktreesGit(repoRoot);
|
|
6032
6700
|
return worktrees.map((wt) => {
|
|
6033
|
-
const metaPath =
|
|
6034
|
-
if (
|
|
6701
|
+
const metaPath = join13(wt.path, "groupy-thread.json");
|
|
6702
|
+
if (existsSync21(metaPath)) {
|
|
6035
6703
|
try {
|
|
6036
|
-
const raw = JSON.parse(
|
|
6704
|
+
const raw = JSON.parse(readFileSync13(metaPath, "utf8"));
|
|
6037
6705
|
return { ...wt, threadId: raw.ownerThreadId || raw.threadId };
|
|
6038
6706
|
} catch {}
|
|
6039
6707
|
}
|
|
@@ -6729,7 +7397,7 @@ function parsePatch(oldSrc, newSrc, contextLines = 3) {
|
|
|
6729
7397
|
// package.json
|
|
6730
7398
|
var package_default = {
|
|
6731
7399
|
name: "@pikaa-ai/pikaa",
|
|
6732
|
-
version: "0.3.
|
|
7400
|
+
version: "0.3.28",
|
|
6733
7401
|
description: "PIKAA CLI - AI coding agent that runs locally in your terminal.",
|
|
6734
7402
|
main: "./dist/index.js",
|
|
6735
7403
|
module: "./dist/index.js",
|
|
@@ -6761,14 +7429,14 @@ var package_default = {
|
|
|
6761
7429
|
prepublishOnly: "bun run build:js"
|
|
6762
7430
|
},
|
|
6763
7431
|
optionalDependencies: {
|
|
6764
|
-
"@pikaa-ai/pikaa-linux-x64": "0.3.
|
|
6765
|
-
"@pikaa-ai/pikaa-linux-x64-musl": "0.3.
|
|
6766
|
-
"@pikaa-ai/pikaa-linux-arm64": "0.3.
|
|
6767
|
-
"@pikaa-ai/pikaa-linux-arm64-musl": "0.3.
|
|
6768
|
-
"@pikaa-ai/pikaa-darwin-x64": "0.3.
|
|
6769
|
-
"@pikaa-ai/pikaa-darwin-arm64": "0.3.
|
|
6770
|
-
"@pikaa-ai/pikaa-windows-x64": "0.3.
|
|
6771
|
-
"@pikaa-ai/pikaa-windows-arm64": "0.3.
|
|
7432
|
+
"@pikaa-ai/pikaa-linux-x64": "0.3.28",
|
|
7433
|
+
"@pikaa-ai/pikaa-linux-x64-musl": "0.3.28",
|
|
7434
|
+
"@pikaa-ai/pikaa-linux-arm64": "0.3.28",
|
|
7435
|
+
"@pikaa-ai/pikaa-linux-arm64-musl": "0.3.28",
|
|
7436
|
+
"@pikaa-ai/pikaa-darwin-x64": "0.3.28",
|
|
7437
|
+
"@pikaa-ai/pikaa-darwin-arm64": "0.3.28",
|
|
7438
|
+
"@pikaa-ai/pikaa-windows-x64": "0.3.28",
|
|
7439
|
+
"@pikaa-ai/pikaa-windows-arm64": "0.3.28"
|
|
6772
7440
|
},
|
|
6773
7441
|
keywords: [
|
|
6774
7442
|
"ai",
|
|
@@ -7973,900 +8641,537 @@ async function promptToolApproval(params) {
|
|
|
7973
8641
|
return;
|
|
7974
8642
|
}
|
|
7975
8643
|
if (_str?.toLowerCase() === "n") {
|
|
7976
|
-
cleanup("no");
|
|
7977
|
-
return;
|
|
7978
|
-
}
|
|
7979
|
-
if (_str?.toLowerCase() === "a") {
|
|
7980
|
-
cleanup("always");
|
|
7981
|
-
return;
|
|
7982
|
-
}
|
|
7983
|
-
};
|
|
7984
|
-
unsubscribe = addGlobalKeypressListener(onKeypress);
|
|
7985
|
-
render();
|
|
7986
|
-
});
|
|
7987
|
-
}
|
|
7988
|
-
async function promptUserQuestion(params) {
|
|
7989
|
-
const { question, options = [] } = params;
|
|
7990
|
-
const boxWidth = Math.min(process.stdout.columns ?? 80, 75);
|
|
7991
|
-
const border = "\u2500".repeat(Math.max(10, boxWidth - 20));
|
|
7992
|
-
console.log(`
|
|
7993
|
-
${style.cyan("\u250C\u2500\u2500")} ${style.bold("AI Question")} ${style.cyan(border)}`);
|
|
7994
|
-
const qLines = question.split(`
|
|
7995
|
-
`);
|
|
7996
|
-
for (const line of qLines) {
|
|
7997
|
-
console.log(` ${style.cyan("\u2502")} ${style.bold(line)}`);
|
|
7998
|
-
}
|
|
7999
|
-
if (options.length > 0) {
|
|
8000
|
-
console.log(` ${style.cyan("\u2502")}`);
|
|
8001
|
-
options.forEach((opt, idx) => {
|
|
8002
|
-
const numTag = style.cyan(`[${idx + 1}]`);
|
|
8003
|
-
const isRec = opt.includes("(Recommended)") || opt.includes("(recommended)");
|
|
8004
|
-
const optText = isRec ? opt.replace(/\(Recommended\)/i, style.green("(Recommended)")) : opt;
|
|
8005
|
-
console.log(` ${style.cyan("\u2502")} ${numTag} ${optText}`);
|
|
8006
|
-
});
|
|
8007
|
-
}
|
|
8008
|
-
console.log(` ${style.cyan("\u2514" + "\u2500".repeat(Math.max(10, boxWidth - 4)))}
|
|
8009
|
-
`);
|
|
8010
|
-
if (!process.stdin.isTTY || false || !process.stdin.readable) {
|
|
8011
|
-
return options[0] || "yes";
|
|
8012
|
-
}
|
|
8013
|
-
return new Promise((resolve) => {
|
|
8014
|
-
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
8015
|
-
const promptLabel = options.length > 0 ? `Select [1-${options.length}] or type custom response: ` : `Your response: `;
|
|
8016
|
-
rl.question(` ${style.bold(promptLabel)}`, (answer) => {
|
|
8017
|
-
rl.close();
|
|
8018
|
-
const trimmed = answer.trim();
|
|
8019
|
-
if (!trimmed) {
|
|
8020
|
-
const fallback = options[0] || "";
|
|
8021
|
-
console.log(style.dim(` \u21B3 Default: ${fallback || "(empty)"}
|
|
8022
|
-
`));
|
|
8023
|
-
resolve(fallback);
|
|
8024
|
-
return;
|
|
8025
|
-
}
|
|
8026
|
-
const num = parseInt(trimmed, 10);
|
|
8027
|
-
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
8028
|
-
const picked = options[num - 1];
|
|
8029
|
-
console.log(style.green(` \u2714 Selected: ${picked}
|
|
8030
|
-
`));
|
|
8031
|
-
resolve(picked);
|
|
8032
|
-
return;
|
|
8033
|
-
}
|
|
8034
|
-
if (options.length === 2) {
|
|
8035
|
-
if (/^(y|yes)$/i.test(trimmed)) {
|
|
8036
|
-
console.log(style.green(` \u2714 Selected: ${options[0]}
|
|
8037
|
-
`));
|
|
8038
|
-
resolve(options[0]);
|
|
8039
|
-
return;
|
|
8040
|
-
}
|
|
8041
|
-
if (/^(n|no)$/i.test(trimmed)) {
|
|
8042
|
-
console.log(style.green(` \u2714 Selected: ${options[1]}
|
|
8043
|
-
`));
|
|
8044
|
-
resolve(options[1]);
|
|
8045
|
-
return;
|
|
8046
|
-
}
|
|
8047
|
-
}
|
|
8048
|
-
console.log(style.green(` \u2714 Answer: ${trimmed}
|
|
8049
|
-
`));
|
|
8050
|
-
resolve(trimmed);
|
|
8051
|
-
});
|
|
8052
|
-
});
|
|
8053
|
-
}
|
|
8054
|
-
async function promptInteractiveList(config) {
|
|
8055
|
-
const {
|
|
8056
|
-
title,
|
|
8057
|
-
items,
|
|
8058
|
-
mode = "select",
|
|
8059
|
-
defaultIndex = 0,
|
|
8060
|
-
maxVisible = 8,
|
|
8061
|
-
emptyMessage = "No items available.",
|
|
8062
|
-
onToggle,
|
|
8063
|
-
onAction,
|
|
8064
|
-
customKeyHints
|
|
8065
|
-
} = config;
|
|
8066
|
-
if (!process.stdin.isTTY || false || !process.stdin.readable) {
|
|
8067
|
-
return {
|
|
8068
|
-
selectedIndex: defaultIndex,
|
|
8069
|
-
selectedItem: items[defaultIndex] || items[0],
|
|
8070
|
-
action: "select"
|
|
8071
|
-
};
|
|
8072
|
-
}
|
|
8073
|
-
if (items.length === 0) {
|
|
8074
|
-
console.log(`
|
|
8075
|
-
${style.dim(emptyMessage)}
|
|
8076
|
-
`);
|
|
8077
|
-
return { selectedIndex: -1, action: "close" };
|
|
8078
|
-
}
|
|
8079
|
-
return new Promise((resolve) => {
|
|
8080
|
-
let selectedIndex = Math.max(0, Math.min(defaultIndex, items.length - 1));
|
|
8081
|
-
let scrollTop = 0;
|
|
8082
|
-
let renderedLines = 0;
|
|
8083
|
-
const wasRaw = process.stdin.isRaw ?? false;
|
|
8084
|
-
ensureRawMode(true);
|
|
8085
|
-
process.stdout.write("\x1B[?25l");
|
|
8086
|
-
const BOX_WIDTH = Math.min(process.stdout.columns ?? 80, 76);
|
|
8087
|
-
const ensureVisible = () => {
|
|
8088
|
-
const visibleCount = Math.min(items.length, maxVisible);
|
|
8089
|
-
if (selectedIndex < scrollTop) {
|
|
8090
|
-
scrollTop = selectedIndex;
|
|
8091
|
-
} else if (selectedIndex >= scrollTop + visibleCount) {
|
|
8092
|
-
scrollTop = selectedIndex + 1 - visibleCount;
|
|
8093
|
-
}
|
|
8094
|
-
if (scrollTop < 0)
|
|
8095
|
-
scrollTop = 0;
|
|
8096
|
-
const maxScroll = Math.max(0, items.length - visibleCount);
|
|
8097
|
-
if (scrollTop > maxScroll)
|
|
8098
|
-
scrollTop = maxScroll;
|
|
8099
|
-
};
|
|
8100
|
-
const render = () => {
|
|
8101
|
-
ensureVisible();
|
|
8102
|
-
const visibleCount = Math.min(items.length, maxVisible);
|
|
8103
|
-
const visibleItems = items.slice(scrollTop, scrollTop + visibleCount);
|
|
8104
|
-
const lines = [];
|
|
8105
|
-
const headerBorder = "\u2500".repeat(Math.max(10, BOX_WIDTH - title.length - 8));
|
|
8106
|
-
lines.push(` \x1B[38;2;140;140;150m\u250C\u2500\u2500\x1B[0m ${style.bold(title)} \x1B[38;2;140;140;150m${headerBorder}\u2510\x1B[0m`);
|
|
8107
|
-
for (let i = 0;i < visibleItems.length; i++) {
|
|
8108
|
-
const item = visibleItems[i];
|
|
8109
|
-
const actualIdx = scrollTop + i;
|
|
8110
|
-
const isCurrent = actualIdx === selectedIndex;
|
|
8111
|
-
const marker = isCurrent ? "\x1B[38;2;217;119;87m\u276F\x1B[0m" : " ";
|
|
8112
|
-
let checkSymbol = "";
|
|
8113
|
-
if (mode === "toggle") {
|
|
8114
|
-
checkSymbol = item.checked ? "\x1B[38;2;78;169;111m[\u2714 ENABLED]\x1B[0m " : "\x1B[38;2;120;120;125m[\u25CB DISABLED]\x1B[0m";
|
|
8115
|
-
} else if (item.checked !== undefined) {
|
|
8116
|
-
checkSymbol = item.checked ? "\x1B[38;2;78;169;111m(\u25CF)\x1B[0m " : "\x1B[38;2;120;120;125m(\u25CB)\x1B[0m ";
|
|
8117
|
-
}
|
|
8118
|
-
const badgeStr = item.badge ? ` ${style.dim(`(${item.badge})`)}` : "";
|
|
8119
|
-
const labelStr = isCurrent ? style.bold(item.label) : item.label;
|
|
8120
|
-
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${marker} ${checkSymbol} ${labelStr}${badgeStr}`);
|
|
8121
|
-
if (item.description) {
|
|
8122
|
-
const descIndent = " ".repeat(mode === "toggle" ? 17 : 7);
|
|
8123
|
-
const maxDescLen = BOX_WIDTH - descIndent.length - 6;
|
|
8124
|
-
const trimmedDesc = item.description.length > maxDescLen ? item.description.slice(0, maxDescLen - 3) + "..." : item.description;
|
|
8125
|
-
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${descIndent}${style.dim(trimmedDesc)}`);
|
|
8126
|
-
}
|
|
8127
|
-
}
|
|
8128
|
-
const moreAbove = scrollTop;
|
|
8129
|
-
const moreBelow = Math.max(0, items.length - (scrollTop + visibleCount));
|
|
8130
|
-
let scrollInfo = "";
|
|
8131
|
-
if (moreAbove > 0 && moreBelow > 0) {
|
|
8132
|
-
scrollInfo = `[\u2191 ${moreAbove} more \xB7 \u2193 ${moreBelow} more]`;
|
|
8133
|
-
} else if (moreBelow > 0) {
|
|
8134
|
-
scrollInfo = `[\u2193 ${moreBelow} more below]`;
|
|
8135
|
-
} else if (moreAbove > 0) {
|
|
8136
|
-
scrollInfo = `[\u2191 ${moreAbove} more above]`;
|
|
8137
|
-
}
|
|
8138
|
-
const defaultHints = mode === "toggle" ? "\u2191/\u2193: navigate \xB7 Space: toggle \xB7 a: all \xB7 d: none \xB7 Enter/Esc: done" : "\u2191/\u2193: navigate \xB7 Enter: select \xB7 Esc: cancel";
|
|
8139
|
-
const hintText = customKeyHints || (scrollInfo ? `${scrollInfo} ${defaultHints}` : defaultHints);
|
|
8140
|
-
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m`);
|
|
8141
|
-
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${style.dim(hintText)}`);
|
|
8142
|
-
lines.push(` \x1B[38;2;140;140;150m\u2514\u2500\u2500${"\u2500".repeat(Math.max(10, BOX_WIDTH - 6))}\u2518\x1B[0m`);
|
|
8143
|
-
let buffer = "";
|
|
8144
|
-
if (renderedLines > 0) {
|
|
8145
|
-
buffer += `\x1B[${renderedLines}A\r`;
|
|
8146
|
-
}
|
|
8147
|
-
buffer += lines.map((l) => `\x1B[2K${l}`).join(`
|
|
8148
|
-
`) + `
|
|
8149
|
-
`;
|
|
8150
|
-
if (renderedLines > lines.length) {
|
|
8151
|
-
buffer += "\x1B[J";
|
|
8152
|
-
}
|
|
8153
|
-
process.stdout.write(buffer);
|
|
8154
|
-
renderedLines = lines.length;
|
|
8155
|
-
};
|
|
8156
|
-
let unsubscribe = null;
|
|
8157
|
-
const cleanup = (res) => {
|
|
8158
|
-
if (renderedLines > 0) {
|
|
8159
|
-
process.stdout.write(`\x1B[${renderedLines}A\r\x1B[J\x1B[?25h`);
|
|
8160
|
-
renderedLines = 0;
|
|
8161
|
-
} else {
|
|
8162
|
-
process.stdout.write("\x1B[?25h");
|
|
8163
|
-
}
|
|
8164
|
-
if (unsubscribe) {
|
|
8165
|
-
unsubscribe();
|
|
8166
|
-
unsubscribe = null;
|
|
8167
|
-
}
|
|
8168
|
-
ensureRawMode(wasRaw);
|
|
8169
|
-
resolve(res);
|
|
8170
|
-
};
|
|
8171
|
-
const onKeypress = async (_str, key) => {
|
|
8172
|
-
if (!key)
|
|
8173
|
-
return;
|
|
8174
|
-
if (key.ctrl && key.name === "c" || key.name === "escape" || _str === "q" || _str === "Q") {
|
|
8175
|
-
cleanup({
|
|
8176
|
-
selectedIndex,
|
|
8177
|
-
selectedItem: items[selectedIndex],
|
|
8178
|
-
action: "close"
|
|
8179
|
-
});
|
|
8180
|
-
return;
|
|
8181
|
-
}
|
|
8182
|
-
if (key.name === "up" || _str === "k") {
|
|
8183
|
-
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
8184
|
-
render();
|
|
8185
|
-
return;
|
|
8186
|
-
}
|
|
8187
|
-
if (key.name === "down" || _str === "j") {
|
|
8188
|
-
selectedIndex = (selectedIndex + 1) % items.length;
|
|
8189
|
-
render();
|
|
8190
|
-
return;
|
|
8191
|
-
}
|
|
8192
|
-
if (_str === " " || _str === "t" || _str === "T") {
|
|
8193
|
-
const item = items[selectedIndex];
|
|
8194
|
-
if (item) {
|
|
8195
|
-
item.checked = !item.checked;
|
|
8196
|
-
if (onToggle) {
|
|
8197
|
-
await onToggle(item, selectedIndex);
|
|
8198
|
-
}
|
|
8199
|
-
}
|
|
8200
|
-
render();
|
|
8201
|
-
return;
|
|
8202
|
-
}
|
|
8203
|
-
if (key.name === "return" || key.name === "enter") {
|
|
8204
|
-
if (mode === "toggle") {
|
|
8205
|
-
cleanup({
|
|
8206
|
-
selectedIndex,
|
|
8207
|
-
selectedItem: items[selectedIndex],
|
|
8208
|
-
action: "close"
|
|
8209
|
-
});
|
|
8210
|
-
} else {
|
|
8211
|
-
cleanup({
|
|
8212
|
-
selectedIndex,
|
|
8213
|
-
selectedItem: items[selectedIndex],
|
|
8214
|
-
action: "select"
|
|
8215
|
-
});
|
|
8216
|
-
}
|
|
8217
|
-
return;
|
|
8218
|
-
}
|
|
8219
|
-
if (_str && onAction) {
|
|
8220
|
-
const item = items[selectedIndex];
|
|
8221
|
-
const shouldExit = await onAction(_str.toLowerCase(), item, selectedIndex);
|
|
8222
|
-
if (shouldExit) {
|
|
8223
|
-
cleanup({
|
|
8224
|
-
selectedIndex,
|
|
8225
|
-
selectedItem: item,
|
|
8226
|
-
action: "custom",
|
|
8227
|
-
keyName: _str.toLowerCase()
|
|
8228
|
-
});
|
|
8229
|
-
return;
|
|
8230
|
-
}
|
|
8231
|
-
render();
|
|
8644
|
+
cleanup("no");
|
|
8645
|
+
return;
|
|
8646
|
+
}
|
|
8647
|
+
if (_str?.toLowerCase() === "a") {
|
|
8648
|
+
cleanup("always");
|
|
8649
|
+
return;
|
|
8232
8650
|
}
|
|
8233
8651
|
};
|
|
8234
8652
|
unsubscribe = addGlobalKeypressListener(onKeypress);
|
|
8235
8653
|
render();
|
|
8236
8654
|
});
|
|
8237
8655
|
}
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
{
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
description: "Hardcoded OpenAI API key detected in source code.",
|
|
8249
|
-
recommendation: "Store API keys in environment variables (e.g. process.env.OPENAI_API_KEY) or a secure vault."
|
|
8250
|
-
},
|
|
8251
|
-
{
|
|
8252
|
-
id: "SEC-002",
|
|
8253
|
-
category: "Secrets Leakage",
|
|
8254
|
-
severity: "CRITICAL",
|
|
8255
|
-
pattern: /AKIA[0-9A-Z]{16}/,
|
|
8256
|
-
description: "Exposed AWS Access Key ID found in source code.",
|
|
8257
|
-
recommendation: "Rotate the exposed key immediately and use IAM Roles or AWS environment credentials."
|
|
8258
|
-
},
|
|
8259
|
-
{
|
|
8260
|
-
id: "SEC-003",
|
|
8261
|
-
category: "Secrets Leakage",
|
|
8262
|
-
severity: "CRITICAL",
|
|
8263
|
-
pattern: /ghp_[0-9a-zA-Z]{36}/,
|
|
8264
|
-
description: "Exposed GitHub Personal Access Token (PAT) detected.",
|
|
8265
|
-
recommendation: "Revoke the token and inject credentials via GitHub Actions Secrets or environment variables."
|
|
8266
|
-
},
|
|
8267
|
-
{
|
|
8268
|
-
id: "SEC-004",
|
|
8269
|
-
category: "Secrets Leakage",
|
|
8270
|
-
severity: "CRITICAL",
|
|
8271
|
-
pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
8272
|
-
description: "Unencrypted private cryptographic key committed to source repository.",
|
|
8273
|
-
recommendation: "Remove private key from git history and manage certificates through a secret manager."
|
|
8274
|
-
},
|
|
8275
|
-
{
|
|
8276
|
-
id: "SEC-005",
|
|
8277
|
-
category: "Secrets Leakage",
|
|
8278
|
-
severity: "HIGH",
|
|
8279
|
-
pattern: /(?:postgres|mysql|mongodb(?:\+srv)?):\/\/[a-zA-Z0-9_.-]+:[^@\s"']+@[a-zA-Z0-9_.-]+/i,
|
|
8280
|
-
description: "Hardcoded database connection string containing plaintext credentials.",
|
|
8281
|
-
recommendation: "Extract connection URI into DATABASE_URL environment variable."
|
|
8282
|
-
},
|
|
8283
|
-
{
|
|
8284
|
-
id: "SEC-010",
|
|
8285
|
-
category: "Code Injection",
|
|
8286
|
-
severity: "HIGH",
|
|
8287
|
-
pattern: /\beval\s*\(/,
|
|
8288
|
-
description: "Use of dangerous `eval()` function permits arbitrary code execution.",
|
|
8289
|
-
recommendation: "Avoid eval(). Use structured JSON.parse() or dedicated domain-specific parsers.",
|
|
8290
|
-
fileExtensions: [".js", ".ts", ".jsx", ".tsx", ".py"]
|
|
8291
|
-
},
|
|
8292
|
-
{
|
|
8293
|
-
id: "SEC-011",
|
|
8294
|
-
category: "Command Injection",
|
|
8295
|
-
severity: "HIGH",
|
|
8296
|
-
pattern: /(?:child_process|cp)\.exec\s*\([^,)]*\+/,
|
|
8297
|
-
description: "Dynamic string concatenation in `child_process.exec()` creates command injection vectors.",
|
|
8298
|
-
recommendation: "Use `execFile` or `spawn` with an array of arguments rather than executing raw shell strings.",
|
|
8299
|
-
fileExtensions: [".js", ".ts"]
|
|
8300
|
-
},
|
|
8301
|
-
{
|
|
8302
|
-
id: "SEC-020",
|
|
8303
|
-
category: "Broken Authentication",
|
|
8304
|
-
severity: "MEDIUM",
|
|
8305
|
-
pattern: /jwt\.(?:sign|verify)\s*\([^,]+,\s*["'](?:secret|test|123456|dev|password)["']\s*\)/i,
|
|
8306
|
-
description: "Weak or hardcoded JWT secret key used in token signing/verification.",
|
|
8307
|
-
recommendation: "Use a high-entropy secret (at least 256 bits) loaded securely from environment variables.",
|
|
8308
|
-
fileExtensions: [".js", ".ts", ".py"]
|
|
8309
|
-
},
|
|
8310
|
-
{
|
|
8311
|
-
id: "SEC-021",
|
|
8312
|
-
category: "Broken Authentication",
|
|
8313
|
-
severity: "MEDIUM",
|
|
8314
|
-
pattern: /jwt\.decode\s*\(/,
|
|
8315
|
-
description: "`jwt.decode()` used without signature verification.",
|
|
8316
|
-
recommendation: "Use `jwt.verify()` with explicit algorithm pinning to validate token authenticity.",
|
|
8317
|
-
fileExtensions: [".js", ".ts"]
|
|
8318
|
-
},
|
|
8319
|
-
{
|
|
8320
|
-
id: "SEC-030",
|
|
8321
|
-
category: "SQL Injection",
|
|
8322
|
-
severity: "HIGH",
|
|
8323
|
-
pattern: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*(?:WHERE|VALUES)\s+.*["']\s*\+\s*[a-zA-Z0-9_.]+/i,
|
|
8324
|
-
description: "Unparameterized raw SQL string concatenation detected.",
|
|
8325
|
-
recommendation: "Use parameterized queries or ORM bindings (e.g. `$1`, `?`, or named parameters).",
|
|
8326
|
-
fileExtensions: [".js", ".ts", ".py", ".go", ".rs"]
|
|
8327
|
-
},
|
|
8328
|
-
{
|
|
8329
|
-
id: "SEC-040",
|
|
8330
|
-
category: "SSRF",
|
|
8331
|
-
severity: "HIGH",
|
|
8332
|
-
pattern: /169\.254\.169\.254/,
|
|
8333
|
-
description: "Direct reference to AWS/Cloud instance metadata IP address (169.254.169.254).",
|
|
8334
|
-
recommendation: "Restrict outbound HTTP access to metadata endpoints and enforce egress IP filtering."
|
|
8656
|
+
async function promptUserQuestion(params) {
|
|
8657
|
+
const { question, options = [] } = params;
|
|
8658
|
+
const boxWidth = Math.min(process.stdout.columns ?? 80, 75);
|
|
8659
|
+
const border = "\u2500".repeat(Math.max(10, boxWidth - 20));
|
|
8660
|
+
console.log(`
|
|
8661
|
+
${style.cyan("\u250C\u2500\u2500")} ${style.bold("AI Question")} ${style.cyan(border)}`);
|
|
8662
|
+
const qLines = question.split(`
|
|
8663
|
+
`);
|
|
8664
|
+
for (const line of qLines) {
|
|
8665
|
+
console.log(` ${style.cyan("\u2502")} ${style.bold(line)}`);
|
|
8335
8666
|
}
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
"
|
|
8343
|
-
|
|
8344
|
-
".turbo",
|
|
8345
|
-
"vendor",
|
|
8346
|
-
"target",
|
|
8347
|
-
"tests",
|
|
8348
|
-
"__tests__"
|
|
8349
|
-
]);
|
|
8350
|
-
var IGNORED_EXTS = new Set([
|
|
8351
|
-
".md",
|
|
8352
|
-
".mdx",
|
|
8353
|
-
".txt",
|
|
8354
|
-
".log",
|
|
8355
|
-
".svg",
|
|
8356
|
-
".png",
|
|
8357
|
-
".jpg",
|
|
8358
|
-
".jpeg",
|
|
8359
|
-
".ico",
|
|
8360
|
-
".woff",
|
|
8361
|
-
".woff2"
|
|
8362
|
-
]);
|
|
8363
|
-
var IGNORED_FILES = new Set([
|
|
8364
|
-
"package-lock.json",
|
|
8365
|
-
"bun.lock",
|
|
8366
|
-
"bun.lockb",
|
|
8367
|
-
"yarn.lock",
|
|
8368
|
-
"pnpm-lock.yaml",
|
|
8369
|
-
"scanner.ts"
|
|
8370
|
-
]);
|
|
8371
|
-
async function runSecurityScan(targetDir, options = {}) {
|
|
8372
|
-
const startTime = performance.now();
|
|
8373
|
-
const root = resolve18(targetDir);
|
|
8374
|
-
const isDirectTestDir = targetDir.includes("test") || Boolean(options.includeTests);
|
|
8375
|
-
const maxFiles = options.maxFiles || 2000;
|
|
8376
|
-
const findings = [];
|
|
8377
|
-
let scannedCount = 0;
|
|
8378
|
-
function walk(current) {
|
|
8379
|
-
if (scannedCount >= maxFiles || !existsSync20(current))
|
|
8380
|
-
return;
|
|
8381
|
-
let entries;
|
|
8382
|
-
try {
|
|
8383
|
-
entries = readdirSync8(current);
|
|
8384
|
-
} catch {
|
|
8385
|
-
return;
|
|
8386
|
-
}
|
|
8387
|
-
for (const entry of entries) {
|
|
8388
|
-
if (scannedCount >= maxFiles)
|
|
8389
|
-
break;
|
|
8390
|
-
const fullPath = join12(current, entry);
|
|
8391
|
-
let stat;
|
|
8392
|
-
try {
|
|
8393
|
-
stat = statSync5(fullPath);
|
|
8394
|
-
} catch {
|
|
8395
|
-
continue;
|
|
8396
|
-
}
|
|
8397
|
-
if (stat.isDirectory()) {
|
|
8398
|
-
if (!entry.startsWith(".agent-worktrees") && (!IGNORED_DIRS.has(entry) || isDirectTestDir && (entry === "tests" || entry === "__tests__"))) {
|
|
8399
|
-
walk(fullPath);
|
|
8400
|
-
}
|
|
8401
|
-
} else if (stat.isFile()) {
|
|
8402
|
-
if (IGNORED_FILES.has(entry) || stat.size > 2 * 1024 * 1024) {
|
|
8403
|
-
continue;
|
|
8404
|
-
}
|
|
8405
|
-
const ext = entry.includes(".") ? `.${entry.split(".").pop().toLowerCase()}` : "";
|
|
8406
|
-
if (IGNORED_EXTS.has(ext)) {
|
|
8407
|
-
continue;
|
|
8408
|
-
}
|
|
8409
|
-
if (options.fileExtensions && options.fileExtensions.length > 0) {
|
|
8410
|
-
if (!options.fileExtensions.includes(ext))
|
|
8411
|
-
continue;
|
|
8412
|
-
}
|
|
8413
|
-
scannedCount++;
|
|
8414
|
-
scanFile(fullPath, root, ext, findings);
|
|
8415
|
-
}
|
|
8416
|
-
}
|
|
8667
|
+
if (options.length > 0) {
|
|
8668
|
+
console.log(` ${style.cyan("\u2502")}`);
|
|
8669
|
+
options.forEach((opt, idx) => {
|
|
8670
|
+
const numTag = style.cyan(`[${idx + 1}]`);
|
|
8671
|
+
const isRec = opt.includes("(Recommended)") || opt.includes("(recommended)");
|
|
8672
|
+
const optText = isRec ? opt.replace(/\(Recommended\)/i, style.green("(Recommended)")) : opt;
|
|
8673
|
+
console.log(` ${style.cyan("\u2502")} ${numTag} ${optText}`);
|
|
8674
|
+
});
|
|
8417
8675
|
}
|
|
8418
|
-
|
|
8419
|
-
let content;
|
|
8420
|
-
try {
|
|
8421
|
-
content = readFileSync12(filePath, "utf8");
|
|
8422
|
-
} catch {
|
|
8423
|
-
return;
|
|
8424
|
-
}
|
|
8425
|
-
const lines = content.split(`
|
|
8676
|
+
console.log(` ${style.cyan("\u2514" + "\u2500".repeat(Math.max(10, boxWidth - 4)))}
|
|
8426
8677
|
`);
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8678
|
+
if (!process.stdin.isTTY || false || !process.stdin.readable) {
|
|
8679
|
+
return options[0] || "yes";
|
|
8680
|
+
}
|
|
8681
|
+
return new Promise((resolve) => {
|
|
8682
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
8683
|
+
const promptLabel = options.length > 0 ? `Select [1-${options.length}] or type custom response: ` : `Your response: `;
|
|
8684
|
+
rl.question(` ${style.bold(promptLabel)}`, (answer) => {
|
|
8685
|
+
rl.close();
|
|
8686
|
+
const trimmed = answer.trim();
|
|
8687
|
+
if (!trimmed) {
|
|
8688
|
+
const fallback = options[0] || "";
|
|
8689
|
+
console.log(style.dim(` \u21B3 Default: ${fallback || "(empty)"}
|
|
8690
|
+
`));
|
|
8691
|
+
resolve(fallback);
|
|
8692
|
+
return;
|
|
8693
|
+
}
|
|
8694
|
+
const num = parseInt(trimmed, 10);
|
|
8695
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
8696
|
+
const picked = options[num - 1];
|
|
8697
|
+
console.log(style.green(` \u2714 Selected: ${picked}
|
|
8698
|
+
`));
|
|
8699
|
+
resolve(picked);
|
|
8700
|
+
return;
|
|
8701
|
+
}
|
|
8702
|
+
if (options.length === 2) {
|
|
8703
|
+
if (/^(y|yes)$/i.test(trimmed)) {
|
|
8704
|
+
console.log(style.green(` \u2714 Selected: ${options[0]}
|
|
8705
|
+
`));
|
|
8706
|
+
resolve(options[0]);
|
|
8707
|
+
return;
|
|
8435
8708
|
}
|
|
8436
|
-
if (
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
filePath: relPath,
|
|
8442
|
-
lineNumber: lineIdx + 1,
|
|
8443
|
-
snippet: line.trim().slice(0, 120),
|
|
8444
|
-
description: rule.description,
|
|
8445
|
-
recommendation: rule.recommendation
|
|
8446
|
-
});
|
|
8709
|
+
if (/^(n|no)$/i.test(trimmed)) {
|
|
8710
|
+
console.log(style.green(` \u2714 Selected: ${options[1]}
|
|
8711
|
+
`));
|
|
8712
|
+
resolve(options[1]);
|
|
8713
|
+
return;
|
|
8447
8714
|
}
|
|
8448
8715
|
}
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
critical: findings.filter((f) => f.severity === "CRITICAL").length,
|
|
8455
|
-
high: findings.filter((f) => f.severity === "HIGH").length,
|
|
8456
|
-
medium: findings.filter((f) => f.severity === "MEDIUM").length,
|
|
8457
|
-
low: findings.filter((f) => f.severity === "LOW").length
|
|
8458
|
-
};
|
|
8459
|
-
return {
|
|
8460
|
-
scannedFiles: scannedCount,
|
|
8461
|
-
findings,
|
|
8462
|
-
durationMs,
|
|
8463
|
-
summary
|
|
8464
|
-
};
|
|
8716
|
+
console.log(style.green(` \u2714 Answer: ${trimmed}
|
|
8717
|
+
`));
|
|
8718
|
+
resolve(trimmed);
|
|
8719
|
+
});
|
|
8720
|
+
});
|
|
8465
8721
|
}
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/web-search";
|
|
8485
|
-
var WEB_SEARCH_MCP_SERVER_PATH = resolve20(__dirname, "server.ts");
|
|
8486
|
-
|
|
8487
|
-
// src/mcp/servers/sqlite/index.ts
|
|
8488
|
-
import { resolve as resolve21 } from "path";
|
|
8489
|
-
|
|
8490
|
-
// src/mcp/servers/sqlite/server.ts
|
|
8491
|
-
if (false) {}
|
|
8492
|
-
|
|
8493
|
-
// src/mcp/servers/sqlite/index.ts
|
|
8494
|
-
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/sqlite";
|
|
8495
|
-
var SQLITE_MCP_SERVER_PATH = resolve21(__dirname, "server.ts");
|
|
8496
|
-
|
|
8497
|
-
// src/init/project-analyzer.ts
|
|
8498
|
-
import { existsSync as existsSync21, readFileSync as readFileSync13, readdirSync as readdirSync9 } from "fs";
|
|
8499
|
-
import { join as join13, basename as basename3 } from "path";
|
|
8500
|
-
|
|
8501
|
-
class ProjectAnalyzer {
|
|
8502
|
-
cwd;
|
|
8503
|
-
constructor(cwd = process.cwd()) {
|
|
8504
|
-
this.cwd = cwd;
|
|
8722
|
+
async function promptInteractiveList(config) {
|
|
8723
|
+
const {
|
|
8724
|
+
title,
|
|
8725
|
+
items,
|
|
8726
|
+
mode = "select",
|
|
8727
|
+
defaultIndex = 0,
|
|
8728
|
+
maxVisible = 8,
|
|
8729
|
+
emptyMessage = "No items available.",
|
|
8730
|
+
onToggle,
|
|
8731
|
+
onAction,
|
|
8732
|
+
customKeyHints
|
|
8733
|
+
} = config;
|
|
8734
|
+
if (!process.stdin.isTTY || false || !process.stdin.readable) {
|
|
8735
|
+
return {
|
|
8736
|
+
selectedIndex: defaultIndex,
|
|
8737
|
+
selectedItem: items[defaultIndex] || items[0],
|
|
8738
|
+
action: "select"
|
|
8739
|
+
};
|
|
8505
8740
|
}
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8741
|
+
if (items.length === 0) {
|
|
8742
|
+
console.log(`
|
|
8743
|
+
${style.dim(emptyMessage)}
|
|
8744
|
+
`);
|
|
8745
|
+
return { selectedIndex: -1, action: "close" };
|
|
8746
|
+
}
|
|
8747
|
+
return new Promise((resolve) => {
|
|
8748
|
+
let selectedIndex = Math.max(0, Math.min(defaultIndex, items.length - 1));
|
|
8749
|
+
let scrollTop = 0;
|
|
8750
|
+
let renderedLines = 0;
|
|
8751
|
+
const wasRaw = process.stdin.isRaw ?? false;
|
|
8752
|
+
ensureRawMode(true);
|
|
8753
|
+
process.stdout.write("\x1B[?25l");
|
|
8754
|
+
const BOX_WIDTH = Math.min(process.stdout.columns ?? 80, 76);
|
|
8755
|
+
const ensureVisible = () => {
|
|
8756
|
+
const visibleCount = Math.min(items.length, maxVisible);
|
|
8757
|
+
if (selectedIndex < scrollTop) {
|
|
8758
|
+
scrollTop = selectedIndex;
|
|
8759
|
+
} else if (selectedIndex >= scrollTop + visibleCount) {
|
|
8760
|
+
scrollTop = selectedIndex + 1 - visibleCount;
|
|
8761
|
+
}
|
|
8762
|
+
if (scrollTop < 0)
|
|
8763
|
+
scrollTop = 0;
|
|
8764
|
+
const maxScroll = Math.max(0, items.length - visibleCount);
|
|
8765
|
+
if (scrollTop > maxScroll)
|
|
8766
|
+
scrollTop = maxScroll;
|
|
8767
|
+
};
|
|
8768
|
+
const render = () => {
|
|
8769
|
+
ensureVisible();
|
|
8770
|
+
const visibleCount = Math.min(items.length, maxVisible);
|
|
8771
|
+
const visibleItems = items.slice(scrollTop, scrollTop + visibleCount);
|
|
8772
|
+
const lines = [];
|
|
8773
|
+
const headerBorder = "\u2500".repeat(Math.max(10, BOX_WIDTH - title.length - 8));
|
|
8774
|
+
lines.push(` \x1B[38;2;140;140;150m\u250C\u2500\u2500\x1B[0m ${style.bold(title)} \x1B[38;2;140;140;150m${headerBorder}\u2510\x1B[0m`);
|
|
8775
|
+
for (let i = 0;i < visibleItems.length; i++) {
|
|
8776
|
+
const item = visibleItems[i];
|
|
8777
|
+
const actualIdx = scrollTop + i;
|
|
8778
|
+
const isCurrent = actualIdx === selectedIndex;
|
|
8779
|
+
const marker = isCurrent ? "\x1B[38;2;217;119;87m\u276F\x1B[0m" : " ";
|
|
8780
|
+
let checkSymbol = "";
|
|
8781
|
+
if (mode === "toggle") {
|
|
8782
|
+
checkSymbol = item.checked ? "\x1B[38;2;78;169;111m[\u2714 ENABLED]\x1B[0m " : "\x1B[38;2;120;120;125m[\u25CB DISABLED]\x1B[0m";
|
|
8783
|
+
} else if (item.checked !== undefined) {
|
|
8784
|
+
checkSymbol = item.checked ? "\x1B[38;2;78;169;111m(\u25CF)\x1B[0m " : "\x1B[38;2;120;120;125m(\u25CB)\x1B[0m ";
|
|
8543
8785
|
}
|
|
8544
|
-
const
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
if (allDeps.vue)
|
|
8553
|
-
frameworks.push("Vue.js");
|
|
8554
|
-
if (allDeps.svelte || allDeps["@sveltejs/kit"])
|
|
8555
|
-
frameworks.push("Svelte");
|
|
8556
|
-
if (allDeps.astro)
|
|
8557
|
-
frameworks.push("Astro");
|
|
8558
|
-
if (allDeps.vite)
|
|
8559
|
-
frameworks.push("Vite");
|
|
8560
|
-
if (allDeps.express)
|
|
8561
|
-
frameworks.push("Express");
|
|
8562
|
-
if (allDeps.hono)
|
|
8563
|
-
frameworks.push("Hono");
|
|
8564
|
-
if (allDeps.fastify)
|
|
8565
|
-
frameworks.push("Fastify");
|
|
8566
|
-
if (allDeps["@nestjs/core"])
|
|
8567
|
-
frameworks.push("NestJS");
|
|
8568
|
-
if (allDeps.tailwindcss)
|
|
8569
|
-
frameworks.push("TailwindCSS");
|
|
8570
|
-
if (allDeps["lucide-react"] || allDeps.lucide)
|
|
8571
|
-
frameworks.push("Lucide Icons");
|
|
8572
|
-
if (allDeps.zustand)
|
|
8573
|
-
frameworks.push("Zustand");
|
|
8574
|
-
if (allDeps["@tanstack/react-query"])
|
|
8575
|
-
frameworks.push("TanStack Query");
|
|
8576
|
-
if (allDeps.oxlint)
|
|
8577
|
-
frameworks.push("Oxlint");
|
|
8578
|
-
if (allDeps.eslint)
|
|
8579
|
-
frameworks.push("ESLint");
|
|
8580
|
-
if (allDeps.vitest)
|
|
8581
|
-
frameworks.push("Vitest");
|
|
8582
|
-
if (allDeps.jest)
|
|
8583
|
-
frameworks.push("Jest");
|
|
8584
|
-
if (allDeps.playwright || allDeps["@playwright/test"])
|
|
8585
|
-
frameworks.push("Playwright");
|
|
8586
|
-
if (pkg.type === "module") {
|
|
8587
|
-
codeConventions.push("Use ES modules (`import/export`), not CommonJS (`require`).");
|
|
8786
|
+
const badgeStr = item.badge ? ` ${style.dim(`(${item.badge})`)}` : "";
|
|
8787
|
+
const labelStr = isCurrent ? style.bold(item.label) : item.label;
|
|
8788
|
+
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${marker} ${checkSymbol} ${labelStr}${badgeStr}`);
|
|
8789
|
+
if (item.description) {
|
|
8790
|
+
const descIndent = " ".repeat(mode === "toggle" ? 17 : 7);
|
|
8791
|
+
const maxDescLen = BOX_WIDTH - descIndent.length - 6;
|
|
8792
|
+
const trimmedDesc = item.description.length > maxDescLen ? item.description.slice(0, maxDescLen - 3) + "..." : item.description;
|
|
8793
|
+
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${descIndent}${style.dim(trimmedDesc)}`);
|
|
8588
8794
|
}
|
|
8589
|
-
}
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8795
|
+
}
|
|
8796
|
+
const moreAbove = scrollTop;
|
|
8797
|
+
const moreBelow = Math.max(0, items.length - (scrollTop + visibleCount));
|
|
8798
|
+
let scrollInfo = "";
|
|
8799
|
+
if (moreAbove > 0 && moreBelow > 0) {
|
|
8800
|
+
scrollInfo = `[\u2191 ${moreAbove} more \xB7 \u2193 ${moreBelow} more]`;
|
|
8801
|
+
} else if (moreBelow > 0) {
|
|
8802
|
+
scrollInfo = `[\u2193 ${moreBelow} more below]`;
|
|
8803
|
+
} else if (moreAbove > 0) {
|
|
8804
|
+
scrollInfo = `[\u2191 ${moreAbove} more above]`;
|
|
8805
|
+
}
|
|
8806
|
+
const defaultHints = mode === "toggle" ? "\u2191/\u2193: navigate \xB7 Space: toggle \xB7 a: all \xB7 d: none \xB7 Enter/Esc: done" : "\u2191/\u2193: navigate \xB7 Enter: select \xB7 Esc: cancel";
|
|
8807
|
+
const hintText = customKeyHints || (scrollInfo ? `${scrollInfo} ${defaultHints}` : defaultHints);
|
|
8808
|
+
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m`);
|
|
8809
|
+
lines.push(` \x1B[38;2;140;140;150m\u2502\x1B[0m ${style.dim(hintText)}`);
|
|
8810
|
+
lines.push(` \x1B[38;2;140;140;150m\u2514\u2500\u2500${"\u2500".repeat(Math.max(10, BOX_WIDTH - 6))}\u2518\x1B[0m`);
|
|
8811
|
+
let buffer = "";
|
|
8812
|
+
if (renderedLines > 0) {
|
|
8813
|
+
buffer += `\x1B[${renderedLines}A\r`;
|
|
8814
|
+
}
|
|
8815
|
+
buffer += lines.map((l) => `\x1B[2K${l}`).join(`
|
|
8816
|
+
`) + `
|
|
8817
|
+
`;
|
|
8818
|
+
if (renderedLines > lines.length) {
|
|
8819
|
+
buffer += "\x1B[J";
|
|
8820
|
+
}
|
|
8821
|
+
process.stdout.write(buffer);
|
|
8822
|
+
renderedLines = lines.length;
|
|
8823
|
+
};
|
|
8824
|
+
let unsubscribe = null;
|
|
8825
|
+
const cleanup = (res) => {
|
|
8826
|
+
if (renderedLines > 0) {
|
|
8827
|
+
process.stdout.write(`\x1B[${renderedLines}A\r\x1B[J\x1B[?25h`);
|
|
8828
|
+
renderedLines = 0;
|
|
8829
|
+
} else {
|
|
8830
|
+
process.stdout.write("\x1B[?25h");
|
|
8831
|
+
}
|
|
8832
|
+
if (unsubscribe) {
|
|
8833
|
+
unsubscribe();
|
|
8834
|
+
unsubscribe = null;
|
|
8835
|
+
}
|
|
8836
|
+
ensureRawMode(wasRaw);
|
|
8837
|
+
resolve(res);
|
|
8838
|
+
};
|
|
8839
|
+
const onKeypress = async (_str, key) => {
|
|
8840
|
+
if (!key)
|
|
8841
|
+
return;
|
|
8842
|
+
if (key.ctrl && key.name === "c" || key.name === "escape" || _str === "q" || _str === "Q") {
|
|
8843
|
+
cleanup({
|
|
8844
|
+
selectedIndex,
|
|
8845
|
+
selectedItem: items[selectedIndex],
|
|
8846
|
+
action: "close"
|
|
8847
|
+
});
|
|
8848
|
+
return;
|
|
8849
|
+
}
|
|
8850
|
+
if (key.name === "up" || _str === "k") {
|
|
8851
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
8852
|
+
render();
|
|
8853
|
+
return;
|
|
8854
|
+
}
|
|
8855
|
+
if (key.name === "down" || _str === "j") {
|
|
8856
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
8857
|
+
render();
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
if (_str === " " || _str === "t" || _str === "T") {
|
|
8861
|
+
const item = items[selectedIndex];
|
|
8862
|
+
if (item) {
|
|
8863
|
+
item.checked = !item.checked;
|
|
8864
|
+
if (onToggle) {
|
|
8865
|
+
await onToggle(item, selectedIndex);
|
|
8866
|
+
}
|
|
8597
8867
|
}
|
|
8598
|
-
|
|
8599
|
-
|
|
8868
|
+
render();
|
|
8869
|
+
return;
|
|
8870
|
+
}
|
|
8871
|
+
if (key.name === "return" || key.name === "enter") {
|
|
8872
|
+
if (mode === "toggle") {
|
|
8873
|
+
cleanup({
|
|
8874
|
+
selectedIndex,
|
|
8875
|
+
selectedItem: items[selectedIndex],
|
|
8876
|
+
action: "close"
|
|
8877
|
+
});
|
|
8878
|
+
} else {
|
|
8879
|
+
cleanup({
|
|
8880
|
+
selectedIndex,
|
|
8881
|
+
selectedItem: items[selectedIndex],
|
|
8882
|
+
action: "select"
|
|
8883
|
+
});
|
|
8600
8884
|
}
|
|
8601
|
-
|
|
8602
|
-
}
|
|
8603
|
-
const cargoPath = join13(this.cwd, "Cargo.toml");
|
|
8604
|
-
if (existsSync21(cargoPath)) {
|
|
8605
|
-
try {
|
|
8606
|
-
commands.dev = commands.dev || "cargo run";
|
|
8607
|
-
commands.build = commands.build || "cargo build";
|
|
8608
|
-
commands.test = commands.test || "cargo test";
|
|
8609
|
-
commands.lint = commands.lint || "cargo clippy";
|
|
8610
|
-
frameworks.push("Rust Cargo");
|
|
8611
|
-
} catch {}
|
|
8612
|
-
}
|
|
8613
|
-
const goModPath = join13(this.cwd, "go.mod");
|
|
8614
|
-
if (existsSync21(goModPath)) {
|
|
8615
|
-
try {
|
|
8616
|
-
commands.dev = commands.dev || "go run .";
|
|
8617
|
-
commands.build = commands.build || "go build ./...";
|
|
8618
|
-
commands.test = commands.test || "go test ./...";
|
|
8619
|
-
commands.lint = commands.lint || "golangci-lint run";
|
|
8620
|
-
frameworks.push("Go Modules");
|
|
8621
|
-
} catch {}
|
|
8622
|
-
}
|
|
8623
|
-
const pyprojectPath = join13(this.cwd, "pyproject.toml");
|
|
8624
|
-
const requirementsPath = join13(this.cwd, "requirements.txt");
|
|
8625
|
-
if (existsSync21(pyprojectPath) || existsSync21(requirementsPath)) {
|
|
8626
|
-
commands.test = commands.test || "pytest";
|
|
8627
|
-
commands.lint = commands.lint || "ruff check .";
|
|
8628
|
-
if (existsSync21(join13(this.cwd, "uv.lock"))) {
|
|
8629
|
-
frameworks.push("uv");
|
|
8630
|
-
commands.test = "uv run pytest";
|
|
8631
|
-
} else if (existsSync21(join13(this.cwd, "poetry.lock"))) {
|
|
8632
|
-
frameworks.push("Poetry");
|
|
8633
|
-
commands.test = "poetry run pytest";
|
|
8885
|
+
return;
|
|
8634
8886
|
}
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
architectureNotes.push("Reusable UI presentation components live in `src/components/`.");
|
|
8649
|
-
}
|
|
8650
|
-
if (existsSync21(join13(this.cwd, "src/types.ts")) || existsSync21(join13(this.cwd, "src/types"))) {
|
|
8651
|
-
architectureNotes.push("Shared TypeScript data models and interfaces are defined in `src/types`.");
|
|
8652
|
-
}
|
|
8653
|
-
if (existsSync21(join13(this.cwd, ".env.example"))) {
|
|
8654
|
-
architectureNotes.push("Environment configuration template is in `.env.example`.");
|
|
8655
|
-
}
|
|
8656
|
-
if (commands.typecheck || commands.lint || commands.test) {
|
|
8657
|
-
const checks = [];
|
|
8658
|
-
if (commands.typecheck)
|
|
8659
|
-
checks.push(`typecheck (\`${commands.typecheck}\`)`);
|
|
8660
|
-
if (commands.lint)
|
|
8661
|
-
checks.push(`lint (\`${commands.lint}\`)`);
|
|
8662
|
-
if (commands.test)
|
|
8663
|
-
checks.push(`tests (\`${commands.test}\`)`);
|
|
8664
|
-
codeConventions.push(`Run ${checks.join(" and ")} before concluding any major code edits.`);
|
|
8665
|
-
}
|
|
8666
|
-
const instructionFiles = ["AGENTS.md", "CLAUDE.md", ".agents.md", "AGENTS.override.md"];
|
|
8667
|
-
let hasExistingInstructions = false;
|
|
8668
|
-
let existingInstructionFile;
|
|
8669
|
-
for (const f of instructionFiles) {
|
|
8670
|
-
if (existsSync21(join13(this.cwd, f))) {
|
|
8671
|
-
hasExistingInstructions = true;
|
|
8672
|
-
existingInstructionFile = f;
|
|
8673
|
-
break;
|
|
8887
|
+
if (_str && onAction) {
|
|
8888
|
+
const item = items[selectedIndex];
|
|
8889
|
+
const shouldExit = await onAction(_str.toLowerCase(), item, selectedIndex);
|
|
8890
|
+
if (shouldExit) {
|
|
8891
|
+
cleanup({
|
|
8892
|
+
selectedIndex,
|
|
8893
|
+
selectedItem: item,
|
|
8894
|
+
action: "custom",
|
|
8895
|
+
keyName: _str.toLowerCase()
|
|
8896
|
+
});
|
|
8897
|
+
return;
|
|
8898
|
+
}
|
|
8899
|
+
render();
|
|
8674
8900
|
}
|
|
8675
|
-
}
|
|
8676
|
-
return {
|
|
8677
|
-
projectName,
|
|
8678
|
-
description,
|
|
8679
|
-
languages,
|
|
8680
|
-
packageManager,
|
|
8681
|
-
frameworks,
|
|
8682
|
-
infrastructure,
|
|
8683
|
-
commands,
|
|
8684
|
-
architectureNotes,
|
|
8685
|
-
codeConventions,
|
|
8686
|
-
hasExistingInstructions,
|
|
8687
|
-
existingInstructionFile
|
|
8688
8901
|
};
|
|
8902
|
+
unsubscribe = addGlobalKeypressListener(onKeypress);
|
|
8903
|
+
render();
|
|
8904
|
+
});
|
|
8905
|
+
}
|
|
8906
|
+
|
|
8907
|
+
// src/security/scanner.ts
|
|
8908
|
+
import { existsSync as existsSync22, readdirSync as readdirSync9, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
|
|
8909
|
+
import { join as join14, relative as relative2, resolve as resolve18 } from "path";
|
|
8910
|
+
var SECURITY_RULES = [
|
|
8911
|
+
{
|
|
8912
|
+
id: "SEC-001",
|
|
8913
|
+
category: "Secrets Leakage",
|
|
8914
|
+
severity: "CRITICAL",
|
|
8915
|
+
pattern: /sk-(?:proj-)?[A-Za-z0-9-_]{20,}/,
|
|
8916
|
+
description: "Hardcoded OpenAI API key detected in source code.",
|
|
8917
|
+
recommendation: "Store API keys in environment variables (e.g. process.env.OPENAI_API_KEY) or a secure vault."
|
|
8918
|
+
},
|
|
8919
|
+
{
|
|
8920
|
+
id: "SEC-002",
|
|
8921
|
+
category: "Secrets Leakage",
|
|
8922
|
+
severity: "CRITICAL",
|
|
8923
|
+
pattern: /AKIA[0-9A-Z]{16}/,
|
|
8924
|
+
description: "Exposed AWS Access Key ID found in source code.",
|
|
8925
|
+
recommendation: "Rotate the exposed key immediately and use IAM Roles or AWS environment credentials."
|
|
8926
|
+
},
|
|
8927
|
+
{
|
|
8928
|
+
id: "SEC-003",
|
|
8929
|
+
category: "Secrets Leakage",
|
|
8930
|
+
severity: "CRITICAL",
|
|
8931
|
+
pattern: /ghp_[0-9a-zA-Z]{36}/,
|
|
8932
|
+
description: "Exposed GitHub Personal Access Token (PAT) detected.",
|
|
8933
|
+
recommendation: "Revoke the token and inject credentials via GitHub Actions Secrets or environment variables."
|
|
8934
|
+
},
|
|
8935
|
+
{
|
|
8936
|
+
id: "SEC-004",
|
|
8937
|
+
category: "Secrets Leakage",
|
|
8938
|
+
severity: "CRITICAL",
|
|
8939
|
+
pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
8940
|
+
description: "Unencrypted private cryptographic key committed to source repository.",
|
|
8941
|
+
recommendation: "Remove private key from git history and manage certificates through a secret manager."
|
|
8942
|
+
},
|
|
8943
|
+
{
|
|
8944
|
+
id: "SEC-005",
|
|
8945
|
+
category: "Secrets Leakage",
|
|
8946
|
+
severity: "HIGH",
|
|
8947
|
+
pattern: /(?:postgres|mysql|mongodb(?:\+srv)?):\/\/[a-zA-Z0-9_.-]+:[^@\s"']+@[a-zA-Z0-9_.-]+/i,
|
|
8948
|
+
description: "Hardcoded database connection string containing plaintext credentials.",
|
|
8949
|
+
recommendation: "Extract connection URI into DATABASE_URL environment variable."
|
|
8950
|
+
},
|
|
8951
|
+
{
|
|
8952
|
+
id: "SEC-010",
|
|
8953
|
+
category: "Code Injection",
|
|
8954
|
+
severity: "HIGH",
|
|
8955
|
+
pattern: /\beval\s*\(/,
|
|
8956
|
+
description: "Use of dangerous `eval()` function permits arbitrary code execution.",
|
|
8957
|
+
recommendation: "Avoid eval(). Use structured JSON.parse() or dedicated domain-specific parsers.",
|
|
8958
|
+
fileExtensions: [".js", ".ts", ".jsx", ".tsx", ".py"]
|
|
8959
|
+
},
|
|
8960
|
+
{
|
|
8961
|
+
id: "SEC-011",
|
|
8962
|
+
category: "Command Injection",
|
|
8963
|
+
severity: "HIGH",
|
|
8964
|
+
pattern: /(?:child_process|cp)\.exec\s*\([^,)]*\+/,
|
|
8965
|
+
description: "Dynamic string concatenation in `child_process.exec()` creates command injection vectors.",
|
|
8966
|
+
recommendation: "Use `execFile` or `spawn` with an array of arguments rather than executing raw shell strings.",
|
|
8967
|
+
fileExtensions: [".js", ".ts"]
|
|
8968
|
+
},
|
|
8969
|
+
{
|
|
8970
|
+
id: "SEC-020",
|
|
8971
|
+
category: "Broken Authentication",
|
|
8972
|
+
severity: "MEDIUM",
|
|
8973
|
+
pattern: /jwt\.(?:sign|verify)\s*\([^,]+,\s*["'](?:secret|test|123456|dev|password)["']\s*\)/i,
|
|
8974
|
+
description: "Weak or hardcoded JWT secret key used in token signing/verification.",
|
|
8975
|
+
recommendation: "Use a high-entropy secret (at least 256 bits) loaded securely from environment variables.",
|
|
8976
|
+
fileExtensions: [".js", ".ts", ".py"]
|
|
8977
|
+
},
|
|
8978
|
+
{
|
|
8979
|
+
id: "SEC-021",
|
|
8980
|
+
category: "Broken Authentication",
|
|
8981
|
+
severity: "MEDIUM",
|
|
8982
|
+
pattern: /jwt\.decode\s*\(/,
|
|
8983
|
+
description: "`jwt.decode()` used without signature verification.",
|
|
8984
|
+
recommendation: "Use `jwt.verify()` with explicit algorithm pinning to validate token authenticity.",
|
|
8985
|
+
fileExtensions: [".js", ".ts"]
|
|
8986
|
+
},
|
|
8987
|
+
{
|
|
8988
|
+
id: "SEC-030",
|
|
8989
|
+
category: "SQL Injection",
|
|
8990
|
+
severity: "HIGH",
|
|
8991
|
+
pattern: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*(?:WHERE|VALUES)\s+.*["']\s*\+\s*[a-zA-Z0-9_.]+/i,
|
|
8992
|
+
description: "Unparameterized raw SQL string concatenation detected.",
|
|
8993
|
+
recommendation: "Use parameterized queries or ORM bindings (e.g. `$1`, `?`, or named parameters).",
|
|
8994
|
+
fileExtensions: [".js", ".ts", ".py", ".go", ".rs"]
|
|
8995
|
+
},
|
|
8996
|
+
{
|
|
8997
|
+
id: "SEC-040",
|
|
8998
|
+
category: "SSRF",
|
|
8999
|
+
severity: "HIGH",
|
|
9000
|
+
pattern: /169\.254\.169\.254/,
|
|
9001
|
+
description: "Direct reference to AWS/Cloud instance metadata IP address (169.254.169.254).",
|
|
9002
|
+
recommendation: "Restrict outbound HTTP access to metadata endpoints and enforce egress IP filtering."
|
|
8689
9003
|
}
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
}
|
|
8741
|
-
}
|
|
8742
|
-
lines.push("- Prefer targeted edits over whole-file rewrites.");
|
|
8743
|
-
lines.push("- When fixing errors, address the root cause rather than suppressing compiler warnings.");
|
|
8744
|
-
lines.push("");
|
|
8745
|
-
return lines.join(`
|
|
8746
|
-
`);
|
|
8747
|
-
}
|
|
8748
|
-
extractReadmeMetadata() {
|
|
8749
|
-
const readmeFiles = ["README.md", "readme.md", "README.MD"];
|
|
8750
|
-
for (const file of readmeFiles) {
|
|
8751
|
-
const fullPath = join13(this.cwd, file);
|
|
8752
|
-
if (existsSync21(fullPath)) {
|
|
8753
|
-
try {
|
|
8754
|
-
const content = readFileSync13(fullPath, "utf8");
|
|
8755
|
-
const lines = content.split(`
|
|
8756
|
-
`);
|
|
8757
|
-
let title;
|
|
8758
|
-
let description;
|
|
8759
|
-
for (const line of lines) {
|
|
8760
|
-
const trimmed = line.trim();
|
|
8761
|
-
if (!title && trimmed.startsWith("# ")) {
|
|
8762
|
-
title = trimmed.replace(/^#\s+/, "").trim();
|
|
8763
|
-
continue;
|
|
8764
|
-
}
|
|
8765
|
-
if (title && !description && trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("```") && !trimmed.startsWith("[")) {
|
|
8766
|
-
description = trimmed;
|
|
8767
|
-
break;
|
|
8768
|
-
}
|
|
8769
|
-
}
|
|
8770
|
-
return { title, description };
|
|
8771
|
-
} catch {}
|
|
8772
|
-
}
|
|
9004
|
+
];
|
|
9005
|
+
var IGNORED_DIRS = new Set([
|
|
9006
|
+
"node_modules",
|
|
9007
|
+
".git",
|
|
9008
|
+
"dist",
|
|
9009
|
+
"build",
|
|
9010
|
+
"coverage",
|
|
9011
|
+
".next",
|
|
9012
|
+
".turbo",
|
|
9013
|
+
"vendor",
|
|
9014
|
+
"target",
|
|
9015
|
+
"tests",
|
|
9016
|
+
"__tests__"
|
|
9017
|
+
]);
|
|
9018
|
+
var IGNORED_EXTS = new Set([
|
|
9019
|
+
".md",
|
|
9020
|
+
".mdx",
|
|
9021
|
+
".txt",
|
|
9022
|
+
".log",
|
|
9023
|
+
".svg",
|
|
9024
|
+
".png",
|
|
9025
|
+
".jpg",
|
|
9026
|
+
".jpeg",
|
|
9027
|
+
".ico",
|
|
9028
|
+
".woff",
|
|
9029
|
+
".woff2"
|
|
9030
|
+
]);
|
|
9031
|
+
var IGNORED_FILES = new Set([
|
|
9032
|
+
"package-lock.json",
|
|
9033
|
+
"bun.lock",
|
|
9034
|
+
"bun.lockb",
|
|
9035
|
+
"yarn.lock",
|
|
9036
|
+
"pnpm-lock.yaml",
|
|
9037
|
+
"scanner.ts"
|
|
9038
|
+
]);
|
|
9039
|
+
async function runSecurityScan(targetDir, options = {}) {
|
|
9040
|
+
const startTime = performance.now();
|
|
9041
|
+
const root = resolve18(targetDir);
|
|
9042
|
+
const isDirectTestDir = targetDir.includes("test") || Boolean(options.includeTests);
|
|
9043
|
+
const maxFiles = options.maxFiles || 2000;
|
|
9044
|
+
const findings = [];
|
|
9045
|
+
let scannedCount = 0;
|
|
9046
|
+
function walk(current) {
|
|
9047
|
+
if (scannedCount >= maxFiles || !existsSync22(current))
|
|
9048
|
+
return;
|
|
9049
|
+
let entries;
|
|
9050
|
+
try {
|
|
9051
|
+
entries = readdirSync9(current);
|
|
9052
|
+
} catch {
|
|
9053
|
+
return;
|
|
8773
9054
|
}
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
9055
|
+
for (const entry of entries) {
|
|
9056
|
+
if (scannedCount >= maxFiles)
|
|
9057
|
+
break;
|
|
9058
|
+
const fullPath = join14(current, entry);
|
|
9059
|
+
let stat;
|
|
8779
9060
|
try {
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
9061
|
+
stat = statSync6(fullPath);
|
|
9062
|
+
} catch {
|
|
9063
|
+
continue;
|
|
9064
|
+
}
|
|
9065
|
+
if (stat.isDirectory()) {
|
|
9066
|
+
if (!entry.startsWith(".agent-worktrees") && (!IGNORED_DIRS.has(entry) || isDirectTestDir && (entry === "tests" || entry === "__tests__"))) {
|
|
9067
|
+
walk(fullPath);
|
|
8783
9068
|
}
|
|
8784
|
-
}
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
return basename3(match[1]);
|
|
8800
|
-
} catch {}
|
|
8801
|
-
}
|
|
8802
|
-
return basename3(this.cwd);
|
|
8803
|
-
}
|
|
8804
|
-
detectLanguages() {
|
|
8805
|
-
const langs = new Set;
|
|
8806
|
-
if (existsSync21(join13(this.cwd, "tsconfig.json")) || this.hasFileWithExtension(".ts", ".tsx")) {
|
|
8807
|
-
langs.add("TypeScript");
|
|
8808
|
-
}
|
|
8809
|
-
if (existsSync21(join13(this.cwd, "package.json")) || this.hasFileWithExtension(".js", ".jsx", ".mjs")) {
|
|
8810
|
-
langs.add("JavaScript");
|
|
8811
|
-
}
|
|
8812
|
-
if (existsSync21(join13(this.cwd, "Cargo.toml")) || this.hasFileWithExtension(".rs")) {
|
|
8813
|
-
langs.add("Rust");
|
|
8814
|
-
}
|
|
8815
|
-
if (existsSync21(join13(this.cwd, "go.mod")) || this.hasFileWithExtension(".go")) {
|
|
8816
|
-
langs.add("Go");
|
|
8817
|
-
}
|
|
8818
|
-
if (existsSync21(join13(this.cwd, "pyproject.toml")) || existsSync21(join13(this.cwd, "requirements.txt")) || this.hasFileWithExtension(".py")) {
|
|
8819
|
-
langs.add("Python");
|
|
8820
|
-
}
|
|
8821
|
-
if (existsSync21(join13(this.cwd, "pom.xml")) || existsSync21(join13(this.cwd, "build.gradle")) || this.hasFileWithExtension(".java")) {
|
|
8822
|
-
langs.add("Java");
|
|
8823
|
-
}
|
|
8824
|
-
if (existsSync21(join13(this.cwd, "CMakeLists.txt")) || this.hasFileWithExtension(".cpp", ".c", ".h", ".hpp")) {
|
|
8825
|
-
langs.add("C/C++");
|
|
9069
|
+
} else if (stat.isFile()) {
|
|
9070
|
+
if (IGNORED_FILES.has(entry) || stat.size > 2 * 1024 * 1024) {
|
|
9071
|
+
continue;
|
|
9072
|
+
}
|
|
9073
|
+
const ext = entry.includes(".") ? `.${entry.split(".").pop().toLowerCase()}` : "";
|
|
9074
|
+
if (IGNORED_EXTS.has(ext)) {
|
|
9075
|
+
continue;
|
|
9076
|
+
}
|
|
9077
|
+
if (options.fileExtensions && options.fileExtensions.length > 0) {
|
|
9078
|
+
if (!options.fileExtensions.includes(ext))
|
|
9079
|
+
continue;
|
|
9080
|
+
}
|
|
9081
|
+
scannedCount++;
|
|
9082
|
+
scanFile(fullPath, root, ext, findings);
|
|
9083
|
+
}
|
|
8826
9084
|
}
|
|
8827
|
-
return Array.from(langs);
|
|
8828
|
-
}
|
|
8829
|
-
detectPackageManager() {
|
|
8830
|
-
if (existsSync21(join13(this.cwd, "bun.lockb")) || existsSync21(join13(this.cwd, "bun.lock")))
|
|
8831
|
-
return "bun";
|
|
8832
|
-
if (existsSync21(join13(this.cwd, "pnpm-lock.yaml")))
|
|
8833
|
-
return "pnpm";
|
|
8834
|
-
if (existsSync21(join13(this.cwd, "yarn.lock")))
|
|
8835
|
-
return "yarn";
|
|
8836
|
-
if (existsSync21(join13(this.cwd, "package-lock.json")))
|
|
8837
|
-
return "npm";
|
|
8838
|
-
if (existsSync21(join13(this.cwd, "Cargo.lock")) || existsSync21(join13(this.cwd, "Cargo.toml")))
|
|
8839
|
-
return "cargo";
|
|
8840
|
-
if (existsSync21(join13(this.cwd, "uv.lock")))
|
|
8841
|
-
return "uv";
|
|
8842
|
-
if (existsSync21(join13(this.cwd, "poetry.lock")))
|
|
8843
|
-
return "poetry";
|
|
8844
|
-
if (existsSync21(join13(this.cwd, "go.sum")) || existsSync21(join13(this.cwd, "go.mod")))
|
|
8845
|
-
return "go";
|
|
8846
|
-
if (existsSync21(join13(this.cwd, "package.json")))
|
|
8847
|
-
return "npm";
|
|
8848
|
-
return;
|
|
8849
9085
|
}
|
|
8850
|
-
|
|
9086
|
+
function scanFile(filePath, baseRoot, ext, out) {
|
|
9087
|
+
let content;
|
|
8851
9088
|
try {
|
|
8852
|
-
|
|
8853
|
-
return entries.some((e) => exts.some((ext) => e.endsWith(ext)));
|
|
9089
|
+
content = readFileSync14(filePath, "utf8");
|
|
8854
9090
|
} catch {
|
|
8855
|
-
return
|
|
9091
|
+
return;
|
|
9092
|
+
}
|
|
9093
|
+
const lines = content.split(`
|
|
9094
|
+
`);
|
|
9095
|
+
const relPath = relative2(baseRoot, filePath);
|
|
9096
|
+
for (let lineIdx = 0;lineIdx < lines.length; lineIdx++) {
|
|
9097
|
+
const line = lines[lineIdx];
|
|
9098
|
+
if (line.includes("SECURITY_RULES") || line.includes("pattern: /"))
|
|
9099
|
+
continue;
|
|
9100
|
+
for (const rule of SECURITY_RULES) {
|
|
9101
|
+
if (rule.fileExtensions && ext && !rule.fileExtensions.includes(ext)) {
|
|
9102
|
+
continue;
|
|
9103
|
+
}
|
|
9104
|
+
if (rule.pattern.test(line)) {
|
|
9105
|
+
out.push({
|
|
9106
|
+
id: rule.id,
|
|
9107
|
+
category: rule.category,
|
|
9108
|
+
severity: rule.severity,
|
|
9109
|
+
filePath: relPath,
|
|
9110
|
+
lineNumber: lineIdx + 1,
|
|
9111
|
+
snippet: line.trim().slice(0, 120),
|
|
9112
|
+
description: rule.description,
|
|
9113
|
+
recommendation: rule.recommendation
|
|
9114
|
+
});
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
8856
9117
|
}
|
|
8857
9118
|
}
|
|
9119
|
+
walk(root);
|
|
9120
|
+
const durationMs = performance.now() - startTime;
|
|
9121
|
+
const summary = {
|
|
9122
|
+
critical: findings.filter((f) => f.severity === "CRITICAL").length,
|
|
9123
|
+
high: findings.filter((f) => f.severity === "HIGH").length,
|
|
9124
|
+
medium: findings.filter((f) => f.severity === "MEDIUM").length,
|
|
9125
|
+
low: findings.filter((f) => f.severity === "LOW").length
|
|
9126
|
+
};
|
|
9127
|
+
return {
|
|
9128
|
+
scannedFiles: scannedCount,
|
|
9129
|
+
findings,
|
|
9130
|
+
durationMs,
|
|
9131
|
+
summary
|
|
9132
|
+
};
|
|
8858
9133
|
}
|
|
9134
|
+
|
|
9135
|
+
// src/mcp/servers/chrome-devtools/index.ts
|
|
9136
|
+
import { resolve as resolve19 } from "path";
|
|
9137
|
+
|
|
9138
|
+
// src/mcp/servers/chrome-devtools/server.ts
|
|
9139
|
+
if (false) {}
|
|
9140
|
+
|
|
9141
|
+
// src/mcp/servers/chrome-devtools/index.ts
|
|
9142
|
+
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/chrome-devtools";
|
|
9143
|
+
var CHROME_DEVTOOLS_MCP_SERVER_PATH = resolve19(__dirname, "server.ts");
|
|
9144
|
+
|
|
9145
|
+
// src/mcp/servers/web-search/index.ts
|
|
9146
|
+
import { resolve as resolve20 } from "path";
|
|
9147
|
+
|
|
9148
|
+
// src/mcp/servers/web-search/server.ts
|
|
9149
|
+
if (false) {}
|
|
9150
|
+
|
|
9151
|
+
// src/mcp/servers/web-search/index.ts
|
|
9152
|
+
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/web-search";
|
|
9153
|
+
var WEB_SEARCH_MCP_SERVER_PATH = resolve20(__dirname, "server.ts");
|
|
9154
|
+
|
|
9155
|
+
// src/mcp/servers/sqlite/index.ts
|
|
9156
|
+
import { resolve as resolve21 } from "path";
|
|
9157
|
+
|
|
9158
|
+
// src/mcp/servers/sqlite/server.ts
|
|
9159
|
+
if (false) {}
|
|
9160
|
+
|
|
9161
|
+
// src/mcp/servers/sqlite/index.ts
|
|
9162
|
+
var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/sqlite";
|
|
9163
|
+
var SQLITE_MCP_SERVER_PATH = resolve21(__dirname, "server.ts");
|
|
8859
9164
|
// src/init/init-command.ts
|
|
8860
|
-
import { existsSync as
|
|
8861
|
-
import { join as
|
|
9165
|
+
import { existsSync as existsSync23, writeFileSync as writeFileSync8 } from "fs";
|
|
9166
|
+
import { join as join15 } from "path";
|
|
8862
9167
|
function runProjectInit(options = {}) {
|
|
8863
9168
|
const cwd = options.cwd || process.cwd();
|
|
8864
9169
|
const filename = options.filename || "AGENTS.md";
|
|
8865
|
-
const targetPath =
|
|
9170
|
+
const targetPath = join15(cwd, filename);
|
|
8866
9171
|
const analyzer = new ProjectAnalyzer(cwd);
|
|
8867
9172
|
const analysis = analyzer.analyze();
|
|
8868
9173
|
const content = analyzer.generateAgentsMarkdown(analysis);
|
|
8869
|
-
const alreadyExists =
|
|
9174
|
+
const alreadyExists = existsSync23(targetPath);
|
|
8870
9175
|
writeFileSync8(targetPath, content, "utf8");
|
|
8871
9176
|
return {
|
|
8872
9177
|
success: true,
|
|
@@ -10452,9 +10757,9 @@ class MarkdownHighlighter {
|
|
|
10452
10757
|
}
|
|
10453
10758
|
|
|
10454
10759
|
// src/cli/update-checker.ts
|
|
10455
|
-
import { existsSync as
|
|
10760
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
10456
10761
|
import { homedir as homedir4 } from "os";
|
|
10457
|
-
import { join as
|
|
10762
|
+
import { join as join16 } from "path";
|
|
10458
10763
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
10459
10764
|
function parseSemver(v) {
|
|
10460
10765
|
const clean = v.replace(/^v/, "").trim();
|
|
@@ -10475,8 +10780,8 @@ function isNewerVersion(current, remote) {
|
|
|
10475
10780
|
return remPatch > curPatch;
|
|
10476
10781
|
}
|
|
10477
10782
|
function getUpdateCachePath() {
|
|
10478
|
-
const baseDir = process.env.PIKAA_HOME || process.env.GROUPY_HOME ||
|
|
10479
|
-
return
|
|
10783
|
+
const baseDir = process.env.PIKAA_HOME || process.env.GROUPY_HOME || join16(homedir4(), ".pikaa");
|
|
10784
|
+
return join16(baseDir, "update-cache.json");
|
|
10480
10785
|
}
|
|
10481
10786
|
async function fetchLatestNpmVersion(packageName, timeoutMs = 1500) {
|
|
10482
10787
|
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
@@ -10509,9 +10814,9 @@ async function checkForUpdates(options = {}) {
|
|
|
10509
10814
|
const cachePath = options.cachePath || getUpdateCachePath();
|
|
10510
10815
|
const now = Date.now();
|
|
10511
10816
|
let cached = null;
|
|
10512
|
-
if (!options.force &&
|
|
10817
|
+
if (!options.force && existsSync24(cachePath)) {
|
|
10513
10818
|
try {
|
|
10514
|
-
const raw = JSON.parse(
|
|
10819
|
+
const raw = JSON.parse(readFileSync15(cachePath, "utf8"));
|
|
10515
10820
|
if (raw && typeof raw.lastChecked === "number" && typeof raw.latestVersion === "string") {
|
|
10516
10821
|
cached = raw;
|
|
10517
10822
|
if (now - cached.lastChecked < CHECK_INTERVAL_MS) {
|
|
@@ -10539,8 +10844,8 @@ async function checkForUpdates(options = {}) {
|
|
|
10539
10844
|
return null;
|
|
10540
10845
|
}
|
|
10541
10846
|
try {
|
|
10542
|
-
const parentDir =
|
|
10543
|
-
if (!
|
|
10847
|
+
const parentDir = join16(cachePath, "..");
|
|
10848
|
+
if (!existsSync24(parentDir)) {
|
|
10544
10849
|
mkdirSync13(parentDir, { recursive: true });
|
|
10545
10850
|
}
|
|
10546
10851
|
const cacheData = {
|
|
@@ -10679,6 +10984,25 @@ class CliRepl {
|
|
|
10679
10984
|
formatTaskProgressPlan(msg.plan, msg.explanation);
|
|
10680
10985
|
this.spinner.start("Executing next step...", this.turnStartTime);
|
|
10681
10986
|
break;
|
|
10987
|
+
case "VerificationStarted":
|
|
10988
|
+
this.spinner.stop();
|
|
10989
|
+
console.log(style.cyan(`
|
|
10990
|
+
\uD83D\uDD0D Auto-Verification: Running '${msg.command}'...`));
|
|
10991
|
+
this.spinner.start(`Verifying changes with ${msg.command}...`, this.turnStartTime);
|
|
10992
|
+
break;
|
|
10993
|
+
case "VerificationCompleted":
|
|
10994
|
+
this.spinner.stop();
|
|
10995
|
+
if (msg.success) {
|
|
10996
|
+
console.log(style.green(` \u2713 Auto-Verification passed: ${msg.command} (${msg.durationMs ?? 0}ms)`));
|
|
10997
|
+
} else {
|
|
10998
|
+
console.log(style.red(` \u2717 Auto-Verification failed: ${msg.command} (${msg.durationMs ?? 0}ms)`));
|
|
10999
|
+
}
|
|
11000
|
+
break;
|
|
11001
|
+
case "SelfHealingStarted":
|
|
11002
|
+
this.spinner.stop();
|
|
11003
|
+
console.log(style.yellow(` \uD83E\uDE79 Self-Healing Loop triggered (Attempt ${msg.attempt}/${msg.maxAttempts}): Auto-fixing detected errors...`));
|
|
11004
|
+
this.spinner.start(`Self-healing [${msg.attempt}/${msg.maxAttempts}]...`, this.turnStartTime);
|
|
11005
|
+
break;
|
|
10682
11006
|
case "TurnCompleted":
|
|
10683
11007
|
if (this.reasoningStarted) {
|
|
10684
11008
|
console.log(style.dim(`
|
|
@@ -10983,7 +11307,7 @@ async function main() {
|
|
|
10983
11307
|
resolve22(cwd, "mcp_config.json")
|
|
10984
11308
|
].filter(Boolean);
|
|
10985
11309
|
for (const cfg of candidateConfigs) {
|
|
10986
|
-
if (
|
|
11310
|
+
if (existsSync25(cfg)) {
|
|
10987
11311
|
try {
|
|
10988
11312
|
await mcpManager.loadConfigFile(cfg);
|
|
10989
11313
|
mcpManager.registerToolsIntoRouter(tools);
|