@algolia/wizard 0.83.0 → 0.84.0-rc.202.644
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/main.js
CHANGED
|
@@ -884,20 +884,28 @@ function useScrollWindow({
|
|
|
884
884
|
revealIndex
|
|
885
885
|
};
|
|
886
886
|
}
|
|
887
|
-
function ScrollView({ scroll, children }) {
|
|
888
|
-
return /* @__PURE__ */ jsxs5(
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
scroll.
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
887
|
+
function ScrollView({ scroll, grow = true, children }) {
|
|
888
|
+
return /* @__PURE__ */ jsxs5(
|
|
889
|
+
Box6,
|
|
890
|
+
{
|
|
891
|
+
ref: scroll.viewportRef,
|
|
892
|
+
flexDirection: "column",
|
|
893
|
+
flexGrow: grow ? 1 : 0,
|
|
894
|
+
children: [
|
|
895
|
+
scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
|
|
896
|
+
"\u2191 ",
|
|
897
|
+
scroll.hiddenAbove,
|
|
898
|
+
" more"
|
|
899
|
+
] }),
|
|
900
|
+
children,
|
|
901
|
+
scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
|
|
902
|
+
"\u2193 ",
|
|
903
|
+
scroll.hiddenBelow,
|
|
904
|
+
" more"
|
|
905
|
+
] })
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
);
|
|
901
909
|
}
|
|
902
910
|
|
|
903
911
|
// src/ui/Review.tsx
|
|
@@ -1082,6 +1090,7 @@ function CommandApproval({
|
|
|
1082
1090
|
if (scroll.measured) setWidth(scroll.width);
|
|
1083
1091
|
}, [scroll.measured, scroll.width]);
|
|
1084
1092
|
const scrollable = scroll.maxOffset > 0;
|
|
1093
|
+
const grow = width === 0 || scrollable;
|
|
1085
1094
|
useInput3(
|
|
1086
1095
|
(input) => {
|
|
1087
1096
|
if (input === "k") scroll.scrollBy(-1);
|
|
@@ -1104,10 +1113,10 @@ function CommandApproval({
|
|
|
1104
1113
|
borderBottom: false,
|
|
1105
1114
|
borderRight: false,
|
|
1106
1115
|
width: "100%",
|
|
1107
|
-
flexGrow: 1,
|
|
1116
|
+
flexGrow: grow ? 1 : 0,
|
|
1108
1117
|
flexShrink: 1,
|
|
1109
1118
|
minHeight: 3,
|
|
1110
|
-
children: /* @__PURE__ */ jsx6(ScrollView, { scroll, children: visible.map((line, i) => /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate", children: [
|
|
1119
|
+
children: /* @__PURE__ */ jsx6(ScrollView, { scroll, grow, children: visible.map((line, i) => /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate", children: [
|
|
1111
1120
|
line.prefix ? /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: line.prefix }) : null,
|
|
1112
1121
|
/* @__PURE__ */ jsx6(Text8, { color: line.color, children: line.text })
|
|
1113
1122
|
] }, `cmd-${scroll.offset + i}`)) })
|
|
@@ -1483,7 +1492,7 @@ function PromptInput() {
|
|
|
1483
1492
|
}
|
|
1484
1493
|
|
|
1485
1494
|
// src/workflows/default.ts
|
|
1486
|
-
import { z as
|
|
1495
|
+
import { z as z30 } from "zod";
|
|
1487
1496
|
|
|
1488
1497
|
// src/core/orchestrator.ts
|
|
1489
1498
|
import "zod";
|
|
@@ -2306,7 +2315,7 @@ async function ensureApplication(resumeAppId) {
|
|
|
2306
2315
|
// package.json
|
|
2307
2316
|
var package_default = {
|
|
2308
2317
|
name: "@algolia/wizard",
|
|
2309
|
-
version: "0.
|
|
2318
|
+
version: "0.84.0-rc.202.644",
|
|
2310
2319
|
description: "Magically implement Algolia functionality in your codebase",
|
|
2311
2320
|
type: "module",
|
|
2312
2321
|
engines: {
|
|
@@ -2797,6 +2806,22 @@ async function listIndices() {
|
|
|
2797
2806
|
|
|
2798
2807
|
// src/actions/selectIndex.ts
|
|
2799
2808
|
var CREATE_NEW_INDEX = "Create a new index\u2026";
|
|
2809
|
+
var MAX_INDEX_NAME_LENGTH = 255;
|
|
2810
|
+
function indexNameError(name) {
|
|
2811
|
+
if (!name) return "Index name cannot be empty.";
|
|
2812
|
+
if (name.length > MAX_INDEX_NAME_LENGTH)
|
|
2813
|
+
return `Index name cannot be longer than ${MAX_INDEX_NAME_LENGTH} characters.`;
|
|
2814
|
+
if (name.startsWith(".")) return "Index name cannot start with '.'.";
|
|
2815
|
+
if (/[/~,\\`&|;$*]/.test(name))
|
|
2816
|
+
return "Index name cannot contain any of these characters: / ~ , \\ ` & | ; $ *";
|
|
2817
|
+
if (/[\p{Cc}]/u.test(name))
|
|
2818
|
+
return "Index name cannot contain control characters.";
|
|
2819
|
+
if (/[\u{10000}-\u{10FFFF}]/u.test(name))
|
|
2820
|
+
return "Index name cannot contain characters outside the Basic Multilingual Plane.";
|
|
2821
|
+
if (/\.(txt|bin|idx)$/.test(name) && !name.endsWith(".indice.bin"))
|
|
2822
|
+
return "Index name cannot end with .txt, .bin, or .idx.";
|
|
2823
|
+
return void 0;
|
|
2824
|
+
}
|
|
2800
2825
|
var selectIndexStep = async (ctx) => {
|
|
2801
2826
|
await requireApplication(ctx.appId);
|
|
2802
2827
|
const indices = await listIndices();
|
|
@@ -2824,6 +2849,7 @@ var selectIndexStep = async (ctx) => {
|
|
|
2824
2849
|
throw new Error("selectIndex received an unexpected non-text result");
|
|
2825
2850
|
}
|
|
2826
2851
|
let candidate;
|
|
2852
|
+
let isNewName = true;
|
|
2827
2853
|
if (!hasIndices) {
|
|
2828
2854
|
candidate = selection.trim();
|
|
2829
2855
|
} else if (selection === CREATE_NEW_INDEX) {
|
|
@@ -2838,9 +2864,11 @@ var selectIndexStep = async (ctx) => {
|
|
|
2838
2864
|
candidate = name.trim();
|
|
2839
2865
|
} else {
|
|
2840
2866
|
candidate = selection;
|
|
2867
|
+
isNewName = false;
|
|
2841
2868
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2869
|
+
const nameError = isNewName ? indexNameError(candidate) : void 0;
|
|
2870
|
+
if (nameError) {
|
|
2871
|
+
error = nameError;
|
|
2844
2872
|
continue;
|
|
2845
2873
|
}
|
|
2846
2874
|
chosen = candidate;
|
|
@@ -2856,6 +2884,7 @@ import {
|
|
|
2856
2884
|
Output as Output3,
|
|
2857
2885
|
APICallError,
|
|
2858
2886
|
NoOutputGeneratedError,
|
|
2887
|
+
RetryError,
|
|
2859
2888
|
stepCountIs
|
|
2860
2889
|
} from "ai";
|
|
2861
2890
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
@@ -2869,6 +2898,58 @@ import { tool } from "ai";
|
|
|
2869
2898
|
import z6 from "zod";
|
|
2870
2899
|
import { readdir } from "node:fs/promises";
|
|
2871
2900
|
|
|
2901
|
+
// src/lib/tools/context.ts
|
|
2902
|
+
var DEFAULT_TOOL_LIMITS = {
|
|
2903
|
+
list: 10,
|
|
2904
|
+
search: 20,
|
|
2905
|
+
read: 20,
|
|
2906
|
+
match: 100,
|
|
2907
|
+
shell: 50
|
|
2908
|
+
};
|
|
2909
|
+
var EMPTY_TOOL_COUNTS = {
|
|
2910
|
+
list: 0,
|
|
2911
|
+
search: 0,
|
|
2912
|
+
read: 0,
|
|
2913
|
+
shell: 0
|
|
2914
|
+
};
|
|
2915
|
+
var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2916
|
+
async function refuseByDefault() {
|
|
2917
|
+
return "reject";
|
|
2918
|
+
}
|
|
2919
|
+
async function emptyShellEnv() {
|
|
2920
|
+
await Promise.resolve();
|
|
2921
|
+
return {};
|
|
2922
|
+
}
|
|
2923
|
+
function createShellContext(overrides = {}) {
|
|
2924
|
+
return {
|
|
2925
|
+
env: emptyShellEnv,
|
|
2926
|
+
secretEnv: emptyShellEnv,
|
|
2927
|
+
timeoutMs: DEFAULT_SHELL_TIMEOUT_MS,
|
|
2928
|
+
executions: [],
|
|
2929
|
+
approve: refuseByDefault,
|
|
2930
|
+
...overrides
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), shell2 = createShellContext()) {
|
|
2934
|
+
return {
|
|
2935
|
+
root: cwd,
|
|
2936
|
+
cwd,
|
|
2937
|
+
limits: { ...limits },
|
|
2938
|
+
counts: { ...EMPTY_TOOL_COUNTS },
|
|
2939
|
+
shell: shell2,
|
|
2940
|
+
reviewed: []
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
function readLimitRefusal(kind, limit) {
|
|
2944
|
+
return `Refused: ${kind} limit (${limit}) reached. This limit is not a failure, so do not report fail because of it. Finish the task now with the information you already have. If the task changes files, write or edit every file that the task still needs. Then report success, and name any gap in the summary.`;
|
|
2945
|
+
}
|
|
2946
|
+
function readLimitReached(ctx) {
|
|
2947
|
+
return ctx.counts.read > ctx.limits.read || ctx.counts.search > ctx.limits.search || ctx.counts.list > ctx.limits.list;
|
|
2948
|
+
}
|
|
2949
|
+
function resetToolCounts(ctx) {
|
|
2950
|
+
Object.assign(ctx.counts, EMPTY_TOOL_COUNTS);
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2872
2953
|
// src/lib/tools/path.ts
|
|
2873
2954
|
import { constants } from "node:fs";
|
|
2874
2955
|
import { lstat, mkdir as mkdir3, open } from "node:fs/promises";
|
|
@@ -2950,7 +3031,7 @@ function listFilesTool(ctx) {
|
|
|
2950
3031
|
execute: async ({ path = "." }) => {
|
|
2951
3032
|
logger.info({ path }, "called listFiles tool");
|
|
2952
3033
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2953
|
-
return
|
|
3034
|
+
return readLimitRefusal("list", ctx.limits.list);
|
|
2954
3035
|
}
|
|
2955
3036
|
const resolved2 = resolveInRoot(ctx, path);
|
|
2956
3037
|
if (!resolved2.ok) return resolved2.error;
|
|
@@ -3026,6 +3107,9 @@ function isSecretEnvFile(filePath) {
|
|
|
3026
3107
|
const name = basename(filePath);
|
|
3027
3108
|
return !/\.(example|sample|template)$/.test(name);
|
|
3028
3109
|
}
|
|
3110
|
+
function secretEnvWriteRefusal(filePath) {
|
|
3111
|
+
return `Refused: ${filePath} holds secrets and cannot be written directly. If a writeCredentials tool is available, use it to set Algolia credentials. If not, tell the user to set the value manually \u2014 do not reproduce the secret value in your response.`;
|
|
3112
|
+
}
|
|
3029
3113
|
|
|
3030
3114
|
// src/lib/tools/utils/truncateText.ts
|
|
3031
3115
|
var HEAD_CHARS = 4e3;
|
|
@@ -3095,7 +3179,7 @@ function readFileTool(ctx) {
|
|
|
3095
3179
|
}),
|
|
3096
3180
|
execute: async ({ filePath, readLines }) => {
|
|
3097
3181
|
if (++ctx.counts.read > ctx.limits.read) {
|
|
3098
|
-
return
|
|
3182
|
+
return readLimitRefusal("read", ctx.limits.read);
|
|
3099
3183
|
}
|
|
3100
3184
|
logger.info({ filePath, readLines }, "called readFile tool");
|
|
3101
3185
|
const resolved2 = resolveInRoot(ctx, filePath);
|
|
@@ -3116,7 +3200,7 @@ import { tool as tool5 } from "ai";
|
|
|
3116
3200
|
import z10 from "zod";
|
|
3117
3201
|
function writeFileTool(ctx) {
|
|
3118
3202
|
return tool5({
|
|
3119
|
-
description: "Write content to a file at the given path, overwriting it. Writes to secret env files (.env, .env.local, etc.) are refused by this tool \u2014 call it anyway and the tool will tell you how to proceed.",
|
|
3203
|
+
description: "Write content to a file at the given path, overwriting it. Use it to create a file or to replace all of its content; to change part of an existing file, use editFile. Writes to secret env files (.env, .env.local, etc.) are refused by this tool \u2014 call it anyway and the tool will tell you how to proceed.",
|
|
3120
3204
|
inputSchema: z10.object({
|
|
3121
3205
|
filePath: z10.string().describe("Path to the file to write"),
|
|
3122
3206
|
content: z10.string().describe("Content to write to the file")
|
|
@@ -3126,7 +3210,7 @@ function writeFileTool(ctx) {
|
|
|
3126
3210
|
const resolved2 = resolveInRoot(ctx, filePath);
|
|
3127
3211
|
if (resolved2.ok === false) return resolved2.error;
|
|
3128
3212
|
if (isSecretEnvFile(resolved2.target)) {
|
|
3129
|
-
return
|
|
3213
|
+
return secretEnvWriteRefusal(filePath);
|
|
3130
3214
|
}
|
|
3131
3215
|
try {
|
|
3132
3216
|
const writeError = await safeWriteFile(ctx, resolved2.target, content);
|
|
@@ -3140,22 +3224,107 @@ function writeFileTool(ctx) {
|
|
|
3140
3224
|
});
|
|
3141
3225
|
}
|
|
3142
3226
|
|
|
3143
|
-
// src/lib/tools/
|
|
3227
|
+
// src/lib/tools/editFile.ts
|
|
3144
3228
|
import { tool as tool6 } from "ai";
|
|
3145
|
-
import
|
|
3146
|
-
import { readFile as
|
|
3229
|
+
import z11 from "zod";
|
|
3230
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
3231
|
+
var SNIPPET_CONTEXT_LINES = 3;
|
|
3232
|
+
var SNIPPET_MAX_LINES = 40;
|
|
3233
|
+
function countOccurrences(content, search2) {
|
|
3234
|
+
let count = 0;
|
|
3235
|
+
let index = content.indexOf(search2);
|
|
3236
|
+
while (index !== -1) {
|
|
3237
|
+
count += 1;
|
|
3238
|
+
index = content.indexOf(search2, index + search2.length);
|
|
3239
|
+
}
|
|
3240
|
+
return count;
|
|
3241
|
+
}
|
|
3242
|
+
function replaceFirst(content, search2, replacement) {
|
|
3243
|
+
const index = content.indexOf(search2);
|
|
3244
|
+
return content.slice(0, index) + replacement + content.slice(index + search2.length);
|
|
3245
|
+
}
|
|
3246
|
+
function editedSnippet(updated, editIndex, newString) {
|
|
3247
|
+
const lines = updated.split("\n");
|
|
3248
|
+
const editStart = updated.slice(0, editIndex).split("\n").length - 1;
|
|
3249
|
+
const editEnd = updated.slice(0, editIndex + newString.length).split("\n").length - (newString.endsWith("\n") ? 1 : 0);
|
|
3250
|
+
const from = Math.max(0, editStart - SNIPPET_CONTEXT_LINES);
|
|
3251
|
+
const to = Math.min(
|
|
3252
|
+
lines.length,
|
|
3253
|
+
editEnd + SNIPPET_CONTEXT_LINES,
|
|
3254
|
+
from + SNIPPET_MAX_LINES
|
|
3255
|
+
);
|
|
3256
|
+
return `readLines [${from}, ${to}] now contains:
|
|
3257
|
+
${truncateText(lines.slice(from, to).join("\n"))}`;
|
|
3258
|
+
}
|
|
3259
|
+
function editFileTool(ctx) {
|
|
3260
|
+
return tool6({
|
|
3261
|
+
description: "Edit an existing file by replacing an exact string with a new string. Prefer this over writeFile for any change to an existing file: send only the changed part, not the whole file. oldString must match the file content exactly, including whitespace and indentation, and must be unique unless replaceAll is true. Include enough surrounding lines to make it unique. Read the file first so the match is exact. A single edit returns the edited lines with some context, so do not read the file again to check the edit.",
|
|
3262
|
+
inputSchema: z11.object({
|
|
3263
|
+
filePath: z11.string().describe("Path to the file to edit"),
|
|
3264
|
+
oldString: z11.string().describe("Exact text to replace"),
|
|
3265
|
+
newString: z11.string().describe("Text to put in place of oldString"),
|
|
3266
|
+
replaceAll: z11.boolean().optional().describe("Replace every occurrence of oldString (default false)")
|
|
3267
|
+
}),
|
|
3268
|
+
execute: async ({ filePath, oldString, newString, replaceAll }) => {
|
|
3269
|
+
logger.info({ filePath, replaceAll }, "called editFile tool");
|
|
3270
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
3271
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
3272
|
+
if (isSecretEnvFile(resolved2.target)) {
|
|
3273
|
+
return secretEnvWriteRefusal(filePath);
|
|
3274
|
+
}
|
|
3275
|
+
if (oldString === "") {
|
|
3276
|
+
return "Error: oldString is empty. To create a file, use writeFile.";
|
|
3277
|
+
}
|
|
3278
|
+
if (oldString === newString) {
|
|
3279
|
+
return "Error: oldString and newString are identical. No change made.";
|
|
3280
|
+
}
|
|
3281
|
+
try {
|
|
3282
|
+
const unsafe = await validateSafeWriteTarget(ctx, resolved2.target);
|
|
3283
|
+
if (unsafe) return unsafe;
|
|
3284
|
+
let content;
|
|
3285
|
+
try {
|
|
3286
|
+
content = await readFile5(resolved2.target, "utf8");
|
|
3287
|
+
} catch (err) {
|
|
3288
|
+
if (err.code === "ENOENT") {
|
|
3289
|
+
return `Error: ${filePath} does not exist. To create it, use writeFile.`;
|
|
3290
|
+
}
|
|
3291
|
+
throw err;
|
|
3292
|
+
}
|
|
3293
|
+
const matches = countOccurrences(content, oldString);
|
|
3294
|
+
if (matches === 0) {
|
|
3295
|
+
return `Error: oldString not found in ${filePath}. Read the file again and copy the exact text, including whitespace.`;
|
|
3296
|
+
}
|
|
3297
|
+
if (matches > 1 && !replaceAll) {
|
|
3298
|
+
return `Error: oldString matches ${matches} places in ${filePath}. Add surrounding lines to make it unique, or set replaceAll to true.`;
|
|
3299
|
+
}
|
|
3300
|
+
const updated = replaceAll ? content.split(oldString).join(newString) : replaceFirst(content, oldString, newString);
|
|
3301
|
+
const writeError = await safeWriteFile(ctx, resolved2.target, updated);
|
|
3302
|
+
if (writeError) return writeError;
|
|
3303
|
+
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
3304
|
+
return matches > 1 ? `Edited ${filePath} (${matches} replacements)` : `Edited ${filePath}. ${editedSnippet(updated, content.indexOf(oldString), newString)}`;
|
|
3305
|
+
} catch (err) {
|
|
3306
|
+
return `Error editing ${filePath}: ${err.message}`;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
3313
|
+
import { tool as tool7 } from "ai";
|
|
3314
|
+
import z14 from "zod";
|
|
3315
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
3147
3316
|
import { relative as relative3 } from "node:path";
|
|
3148
3317
|
|
|
3149
3318
|
// src/lib/algoliaApiKey.ts
|
|
3150
|
-
import { z as
|
|
3319
|
+
import { z as z13 } from "zod";
|
|
3151
3320
|
|
|
3152
3321
|
// src/lib/keychain.ts
|
|
3153
3322
|
import { createHash } from "node:crypto";
|
|
3154
3323
|
import { deletePassword, getPassword, setPassword } from "cross-keychain";
|
|
3155
|
-
import { z as
|
|
3324
|
+
import { z as z12 } from "zod";
|
|
3156
3325
|
var SERVICE = "algolia-wizard";
|
|
3157
3326
|
var account = (userId) => `api-keys-${createHash("sha256").update(userId).digest("hex")}`;
|
|
3158
|
-
var storedKeysSchema =
|
|
3327
|
+
var storedKeysSchema = z12.record(z12.string(), z12.string());
|
|
3159
3328
|
function entryId(kind, index, appId) {
|
|
3160
3329
|
return `${kind}:${appId}:${index}`;
|
|
3161
3330
|
}
|
|
@@ -3249,9 +3418,9 @@ var WRITE_ACLS = [
|
|
|
3249
3418
|
"editSettings",
|
|
3250
3419
|
"listIndexes"
|
|
3251
3420
|
];
|
|
3252
|
-
var createdKeySchema =
|
|
3253
|
-
key:
|
|
3254
|
-
value:
|
|
3421
|
+
var createdKeySchema = z13.object({
|
|
3422
|
+
key: z13.string().min(1).optional(),
|
|
3423
|
+
value: z13.string().min(1).optional()
|
|
3255
3424
|
}).transform((o) => o.key ?? o.value);
|
|
3256
3425
|
async function createKey(index, acls, description) {
|
|
3257
3426
|
logger.info({ index, acls }, "creating an API key");
|
|
@@ -3347,7 +3516,7 @@ async function resolveSearchOnlyKey(index, appId) {
|
|
|
3347
3516
|
|
|
3348
3517
|
// src/lib/gitignore.ts
|
|
3349
3518
|
import { execFile } from "node:child_process";
|
|
3350
|
-
import { lstat as lstat2, readFile as
|
|
3519
|
+
import { lstat as lstat2, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
3351
3520
|
import { join as join7, relative as relative2 } from "node:path";
|
|
3352
3521
|
var GIT_ENV_OVERRIDES = [
|
|
3353
3522
|
"GIT_DIR",
|
|
@@ -3410,7 +3579,7 @@ async function ensureGitIgnored(root, target) {
|
|
|
3410
3579
|
);
|
|
3411
3580
|
return "unknown";
|
|
3412
3581
|
}
|
|
3413
|
-
const existing = link ? await
|
|
3582
|
+
const existing = link ? await readFile6(gitIgnore, "utf8") : "";
|
|
3414
3583
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
3415
3584
|
await writeFile3(gitIgnore, `${existing}${prefix}${pattern}
|
|
3416
3585
|
`, "utf8");
|
|
@@ -3457,10 +3626,10 @@ function upsertEnv(content, name, value) {
|
|
|
3457
3626
|
);
|
|
3458
3627
|
}
|
|
3459
3628
|
function writeCredentialsTool(ctx) {
|
|
3460
|
-
return
|
|
3629
|
+
return tool7({
|
|
3461
3630
|
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself. If the script or app that reads these credentials lives in a subdirectory (e.g. a package in a monorepo), an env file at the repo root is the wrong default \u2014 a script only loads env vars from its own directory (or one it's explicitly configured to read), so check every directory from the script's own up to the repo root, not just those two: its own directory, each ancestor in between (a shared workspace-level directory above the immediate package is common), and the root. Use whichever of those already holds real credentials; only fall back to the repo root when none of them do. Never invent a brand-new file in one of those directories when a real one already exists in another \u2014 that leaves the real one stale and the new one wrong. Listing just the script's own directory and the very top-level root is not enough to find a workspace-level file in between; check the intermediate ones too. If instructions describe a location that doesn't match the project you actually find (e.g. a path outside the repo, or a convention the project doesn't follow), don't stop and ask before doing anything \u2014 call this tool on the real, in-repo file the script actually reads (that's always the safe default), then note the mismatch afterward. Ending your turn with only a question and no call to this tool leaves the project unconfigured.`,
|
|
3462
|
-
inputSchema:
|
|
3463
|
-
filePath:
|
|
3631
|
+
inputSchema: z14.object({
|
|
3632
|
+
filePath: z14.string().describe(
|
|
3464
3633
|
'Path to the env file to write credentials into, relative to the repo root (e.g. ".env", or "packages/api/.env" when the consuming script lives in that package)'
|
|
3465
3634
|
)
|
|
3466
3635
|
}),
|
|
@@ -3478,7 +3647,7 @@ function writeCredentialsTool(ctx) {
|
|
|
3478
3647
|
const targetError = await validateSafeWriteTarget(ctx, resolved2.target);
|
|
3479
3648
|
if (targetError) return targetError;
|
|
3480
3649
|
try {
|
|
3481
|
-
existing = await
|
|
3650
|
+
existing = await readFile7(resolved2.target, "utf8");
|
|
3482
3651
|
} catch (err) {
|
|
3483
3652
|
if (err.code !== "ENOENT") throw err;
|
|
3484
3653
|
}
|
|
@@ -3567,9 +3736,9 @@ async function gitIgnoreOutcome(ctx, target) {
|
|
|
3567
3736
|
|
|
3568
3737
|
// src/lib/tools/searchFiles.ts
|
|
3569
3738
|
import { execFile as execFile2 } from "node:child_process";
|
|
3570
|
-
import { tool as
|
|
3571
|
-
import
|
|
3572
|
-
import { lstat as lstat3, readdir as readdir2, readFile as
|
|
3739
|
+
import { tool as tool8 } from "ai";
|
|
3740
|
+
import z15 from "zod";
|
|
3741
|
+
import { lstat as lstat3, readdir as readdir2, readFile as readFile8, realpath, stat as stat2 } from "node:fs/promises";
|
|
3573
3742
|
import { isAbsolute as isAbsolute2, join as join8, relative as relative4, sep as sep2 } from "node:path";
|
|
3574
3743
|
import { performance } from "node:perf_hooks";
|
|
3575
3744
|
import { Worker } from "node:worker_threads";
|
|
@@ -3940,16 +4109,16 @@ function formatSearchResult(matches, skippedFiles, deadlineReached, workerFailed
|
|
|
3940
4109
|
return lines.length > 0 ? lines.join("\n") : "No matches";
|
|
3941
4110
|
}
|
|
3942
4111
|
function searchFilesTool(ctx) {
|
|
3943
|
-
return
|
|
4112
|
+
return tool8({
|
|
3944
4113
|
description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
|
|
3945
|
-
inputSchema:
|
|
3946
|
-
query:
|
|
3947
|
-
path:
|
|
4114
|
+
inputSchema: z15.object({
|
|
4115
|
+
query: z15.string().describe("JavaScript RegExp pattern to search for"),
|
|
4116
|
+
path: z15.string().optional().describe("Directory to search in (default: cwd)")
|
|
3948
4117
|
}),
|
|
3949
4118
|
execute: async ({ query, path = "." }) => {
|
|
3950
4119
|
logger.info({ query, path }, "called searchFiles tool");
|
|
3951
4120
|
if (++ctx.counts.search > ctx.limits.search) {
|
|
3952
|
-
return
|
|
4121
|
+
return readLimitRefusal("search", ctx.limits.search);
|
|
3953
4122
|
}
|
|
3954
4123
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
3955
4124
|
return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
|
|
@@ -3992,7 +4161,7 @@ function searchFilesTool(ctx) {
|
|
|
3992
4161
|
try {
|
|
3993
4162
|
const filePath = await safeSearchPath(rootPath, file);
|
|
3994
4163
|
if (filePath === null) continue;
|
|
3995
|
-
content = await
|
|
4164
|
+
content = await readFile8(filePath, "utf8");
|
|
3996
4165
|
} catch {
|
|
3997
4166
|
continue;
|
|
3998
4167
|
}
|
|
@@ -4079,16 +4248,16 @@ function searchFilesTool(ctx) {
|
|
|
4079
4248
|
}
|
|
4080
4249
|
|
|
4081
4250
|
// src/lib/tools/runShell.ts
|
|
4082
|
-
import { tool as
|
|
4251
|
+
import { tool as tool10, generateText, Output } from "ai";
|
|
4083
4252
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
4084
|
-
import
|
|
4253
|
+
import z17 from "zod";
|
|
4085
4254
|
import { relative as relative6 } from "node:path";
|
|
4086
4255
|
|
|
4087
4256
|
// src/lib/tools/reviewScript.ts
|
|
4088
|
-
import { tool as
|
|
4089
|
-
import
|
|
4257
|
+
import { tool as tool9 } from "ai";
|
|
4258
|
+
import z16 from "zod";
|
|
4090
4259
|
import { createHash as createHash2 } from "node:crypto";
|
|
4091
|
-
import { readFile as
|
|
4260
|
+
import { readFile as readFile9, stat as stat3 } from "node:fs/promises";
|
|
4092
4261
|
import { basename as basename2, relative as relative5, resolve as resolve3 } from "node:path";
|
|
4093
4262
|
|
|
4094
4263
|
// src/lib/tools/utils/prompt.ts
|
|
@@ -4103,7 +4272,7 @@ function serializePrompt(work) {
|
|
|
4103
4272
|
// src/lib/tools/reviewScript.ts
|
|
4104
4273
|
var reviewedCommandApprovals = /* @__PURE__ */ new WeakMap();
|
|
4105
4274
|
async function fileDigest(filePath) {
|
|
4106
|
-
const content = await
|
|
4275
|
+
const content = await readFile9(filePath);
|
|
4107
4276
|
return createHash2("sha256").update(content).digest("hex");
|
|
4108
4277
|
}
|
|
4109
4278
|
function grantReviewedCommandApproval(ctx, approval) {
|
|
@@ -4207,18 +4376,18 @@ async function reviewedCommandApprovalIsCurrent(approval) {
|
|
|
4207
4376
|
return currentDigest === approval.digest;
|
|
4208
4377
|
}
|
|
4209
4378
|
function reviewScriptTool(ctx) {
|
|
4210
|
-
return
|
|
4379
|
+
return tool9({
|
|
4211
4380
|
description: "Call this as soon as the ingestion script is finished and before you run it. It summarizes the records that will be written, then asks the user to review the script and approve the exact command that will run it, so a matching runShell call needs no second approval.",
|
|
4212
|
-
inputSchema:
|
|
4213
|
-
filePath:
|
|
4381
|
+
inputSchema: z16.object({
|
|
4382
|
+
filePath: z16.string().describe(
|
|
4214
4383
|
"Path to the script to review, relative to the project root."
|
|
4215
4384
|
),
|
|
4216
|
-
command:
|
|
4217
|
-
cwd:
|
|
4385
|
+
command: z16.string().min(1).describe("The exact command that runShell will use to run the script."),
|
|
4386
|
+
cwd: z16.string().describe(
|
|
4218
4387
|
"The exact working directory that runShell will use, relative to the current tool working directory."
|
|
4219
4388
|
),
|
|
4220
|
-
entityName:
|
|
4221
|
-
usesGeneratedRecords:
|
|
4389
|
+
entityName: z16.string().trim().min(1).max(100).regex(/^[^\r\n]+$/).describe("The exact entity name selected by the user."),
|
|
4390
|
+
usesGeneratedRecords: z16.boolean().describe(
|
|
4222
4391
|
"Whether the script reads generated sample records instead of the selected real data source."
|
|
4223
4392
|
)
|
|
4224
4393
|
}),
|
|
@@ -4309,54 +4478,6 @@ function redactSecrets(output, secrets) {
|
|
|
4309
4478
|
|
|
4310
4479
|
// src/lib/tools/utils/runShell.ts
|
|
4311
4480
|
import { spawn as spawn2 } from "node:child_process";
|
|
4312
|
-
|
|
4313
|
-
// src/lib/tools/context.ts
|
|
4314
|
-
var DEFAULT_TOOL_LIMITS = {
|
|
4315
|
-
list: 10,
|
|
4316
|
-
search: 20,
|
|
4317
|
-
read: 20,
|
|
4318
|
-
match: 100,
|
|
4319
|
-
shell: 50
|
|
4320
|
-
};
|
|
4321
|
-
var EMPTY_TOOL_COUNTS = {
|
|
4322
|
-
list: 0,
|
|
4323
|
-
search: 0,
|
|
4324
|
-
read: 0,
|
|
4325
|
-
shell: 0
|
|
4326
|
-
};
|
|
4327
|
-
var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4328
|
-
async function refuseByDefault() {
|
|
4329
|
-
return "reject";
|
|
4330
|
-
}
|
|
4331
|
-
async function emptyShellEnv() {
|
|
4332
|
-
await Promise.resolve();
|
|
4333
|
-
return {};
|
|
4334
|
-
}
|
|
4335
|
-
function createShellContext(overrides = {}) {
|
|
4336
|
-
return {
|
|
4337
|
-
env: emptyShellEnv,
|
|
4338
|
-
secretEnv: emptyShellEnv,
|
|
4339
|
-
timeoutMs: DEFAULT_SHELL_TIMEOUT_MS,
|
|
4340
|
-
executions: [],
|
|
4341
|
-
approve: refuseByDefault,
|
|
4342
|
-
...overrides
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
|
-
function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), shell2 = createShellContext()) {
|
|
4346
|
-
return {
|
|
4347
|
-
root: cwd,
|
|
4348
|
-
cwd,
|
|
4349
|
-
limits: { ...limits },
|
|
4350
|
-
counts: { ...EMPTY_TOOL_COUNTS },
|
|
4351
|
-
shell: shell2,
|
|
4352
|
-
reviewed: []
|
|
4353
|
-
};
|
|
4354
|
-
}
|
|
4355
|
-
function resetToolCounts(ctx) {
|
|
4356
|
-
Object.assign(ctx.counts, EMPTY_TOOL_COUNTS);
|
|
4357
|
-
}
|
|
4358
|
-
|
|
4359
|
-
// src/lib/tools/utils/runShell.ts
|
|
4360
4481
|
var SIGKILL_DELAY_MS = 5e3;
|
|
4361
4482
|
function runShell(command, opts) {
|
|
4362
4483
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
@@ -4618,9 +4739,9 @@ function fastPathSafety(command) {
|
|
|
4618
4739
|
if (results.every((r) => r === true)) return true;
|
|
4619
4740
|
return void 0;
|
|
4620
4741
|
}
|
|
4621
|
-
var commandSafetySchema =
|
|
4622
|
-
safe:
|
|
4623
|
-
reason:
|
|
4742
|
+
var commandSafetySchema = z17.object({
|
|
4743
|
+
safe: z17.boolean(),
|
|
4744
|
+
reason: z17.string().describe("One short sentence explaining the verdict.")
|
|
4624
4745
|
});
|
|
4625
4746
|
function defaultCreateModel() {
|
|
4626
4747
|
const token = getAuthToken();
|
|
@@ -4661,17 +4782,17 @@ async function classifyCommandSafety(createModel, command, cwd, explanation, app
|
|
|
4661
4782
|
prompt: [
|
|
4662
4783
|
"A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
|
|
4663
4784
|
"Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
|
|
4664
|
-
"Never judge a command SAFE if it runs, imports, requires, or otherwise
|
|
4785
|
+
"Never judge a command SAFE if it runs, imports, requires, or otherwise executes an ingestion script or any code under `.algolia-wizard/`. This rule includes shell wrappers, inline code, variable indirection, and dry-run or check descriptions, and no earlier approval overrides it. Type-checking, parsing, or compiling such a file without running it (e.g. `tsc --noEmit <file>`, `node --check <file>`) does not execute it: judge that check unsafe the first time, and SAFE when the precedent rule at the end applies.",
|
|
4665
4786
|
"Three broad categories are safe, and most commands you will see fall into one of them:",
|
|
4666
4787
|
"(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
|
|
4667
4788
|
'(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
|
|
4668
4789
|
"(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
|
|
4669
4790
|
"Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with &&, ;, a pipe, or a line break (LF or CRLF) is only safe if every command segment is safe on its own. Treat each line as a separate command segment, even when the first line is safe.",
|
|
4670
|
-
"One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
|
|
4791
|
+
"One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe. This runner rule yields to the precedent rule at the end: a later invocation of an approved runner and package (e.g. `npx tsc ...`) is SAFE only when it has the same action and side-effect profile as the approved one.",
|
|
4671
4792
|
...approvedHistory.length > 0 ? [
|
|
4672
4793
|
"The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
|
|
4673
4794
|
approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
|
|
4674
|
-
"
|
|
4795
|
+
"Precedent rule: if the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, flag, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent. A different package name is NOT trivial for a dependency install or a runner (npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx): approving `npm install algoliasearch` does not approve `npm install <other-pkg>`, because installing runs lifecycle scripts. This overrides categories (1)-(3), the dependency install and upgrade rule, the runner rule, and the `.algolia-wizard/` check rule above. Precedent never makes SAFE a command that executes code under `.algolia-wizard/`. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
|
|
4675
4796
|
] : [],
|
|
4676
4797
|
`Command: ${command}`,
|
|
4677
4798
|
`Working directory: ${cwd}`,
|
|
@@ -4716,7 +4837,9 @@ async function resolveSecretShellEnv(ctx, command) {
|
|
|
4716
4837
|
});
|
|
4717
4838
|
}
|
|
4718
4839
|
async function runAndRecord(ctx, command, environment) {
|
|
4719
|
-
useWizard.getState().pushNotice({
|
|
4840
|
+
useWizard.getState().pushNotice({
|
|
4841
|
+
messages: [`Running: ${command.replace(/\s*\n\s*/g, " ").trim()}`]
|
|
4842
|
+
});
|
|
4720
4843
|
const run2 = await (ctx.shell.run ?? runShell)(command, {
|
|
4721
4844
|
cwd: environment.cwd,
|
|
4722
4845
|
env: environment.values,
|
|
@@ -4789,16 +4912,16 @@ async function runUnreviewedCommand(ctx, command, cwd) {
|
|
|
4789
4912
|
return runAndRecord(ctx, command, { cwd, values: env, secrets });
|
|
4790
4913
|
}
|
|
4791
4914
|
function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
4792
|
-
return
|
|
4915
|
+
return tool10({
|
|
4793
4916
|
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. Do not use this to find or read files \u2014 use listFiles, searchFiles, and readFile instead of shell file-listing, reading, or search commands (ls/find/tree/cat/head/tail/grep/rg and similar). This tool refuses all of those outright: the dedicated tools enforce the project-root boundary and secret redaction that a raw shell read would bypass, and they won't count against this tool's command budget. The user approves any command that could change the project before it runs, so write a clear `explanation`. A command judged read-only (inspection, or running tests/typecheck/lint without an autofix flag, in any language) runs immediately without approval. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
4794
|
-
inputSchema:
|
|
4795
|
-
command:
|
|
4917
|
+
inputSchema: z17.object({
|
|
4918
|
+
command: z17.string().describe(
|
|
4796
4919
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
4797
4920
|
),
|
|
4798
|
-
cwd:
|
|
4921
|
+
cwd: z17.string().optional().describe(
|
|
4799
4922
|
"Directory to run in, relative to the project root. Defaults to the project root."
|
|
4800
4923
|
),
|
|
4801
|
-
explanation:
|
|
4924
|
+
explanation: z17.string().describe(
|
|
4802
4925
|
"One short line telling the user what this command does and why, including any side effect (e.g. writes records to Algolia). This is what they approve against."
|
|
4803
4926
|
)
|
|
4804
4927
|
}),
|
|
@@ -4854,10 +4977,10 @@ function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
|
4854
4977
|
}
|
|
4855
4978
|
|
|
4856
4979
|
// src/lib/tools/generateRecord.ts
|
|
4857
|
-
import { tool as
|
|
4980
|
+
import { tool as tool11, generateText as generateText2, Output as Output2, NoObjectGeneratedError } from "ai";
|
|
4858
4981
|
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
4859
4982
|
import { nanoid as nanoid2 } from "nanoid";
|
|
4860
|
-
import
|
|
4983
|
+
import z18 from "zod";
|
|
4861
4984
|
var DATA_DIR = ".algolia-wizard/data";
|
|
4862
4985
|
var MAX_RECORDS = 100;
|
|
4863
4986
|
var BATCH_SIZE = 10;
|
|
@@ -4874,20 +4997,20 @@ function defaultCreateModel2() {
|
|
|
4874
4997
|
});
|
|
4875
4998
|
}
|
|
4876
4999
|
function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
4877
|
-
return
|
|
5000
|
+
return tool11({
|
|
4878
5001
|
description: "Generate realistic sample records for an entity and write them to a JSON file. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
|
|
4879
|
-
inputSchema:
|
|
4880
|
-
entityName:
|
|
4881
|
-
attributes:
|
|
4882
|
-
count:
|
|
4883
|
-
hint:
|
|
5002
|
+
inputSchema: z18.object({
|
|
5003
|
+
entityName: z18.string().describe("Name of the entity to generate records for."),
|
|
5004
|
+
attributes: z18.array(z18.string()).describe("Attribute names each record must contain."),
|
|
5005
|
+
count: z18.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
|
|
5006
|
+
hint: z18.string().optional().describe("Optional context to steer realistic values.")
|
|
4884
5007
|
}),
|
|
4885
5008
|
execute: async ({ entityName, attributes, count, hint }) => {
|
|
4886
5009
|
logger.info({ entityName, count }, "called generateRecord tool");
|
|
4887
5010
|
try {
|
|
4888
5011
|
const anthropic = createModel();
|
|
4889
|
-
const value =
|
|
4890
|
-
const recordSchema2 =
|
|
5012
|
+
const value = z18.union([z18.string(), z18.number(), z18.boolean(), z18.null()]);
|
|
5013
|
+
const recordSchema2 = z18.object(
|
|
4891
5014
|
Object.fromEntries(attributes.map((attr) => [attr, value]))
|
|
4892
5015
|
);
|
|
4893
5016
|
const generateBatch = async (batchCount) => {
|
|
@@ -4909,8 +5032,8 @@ function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
|
4909
5032
|
}
|
|
4910
5033
|
},
|
|
4911
5034
|
output: Output2.object({
|
|
4912
|
-
schema:
|
|
4913
|
-
records:
|
|
5035
|
+
schema: z18.object({
|
|
5036
|
+
records: z18.array(recordSchema2).length(batchCount)
|
|
4914
5037
|
})
|
|
4915
5038
|
}),
|
|
4916
5039
|
prompt: [
|
|
@@ -4981,13 +5104,13 @@ function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
|
4981
5104
|
}
|
|
4982
5105
|
|
|
4983
5106
|
// src/lib/tools/notifyUser.ts
|
|
4984
|
-
import { tool as
|
|
4985
|
-
import
|
|
5107
|
+
import { tool as tool12 } from "ai";
|
|
5108
|
+
import z19 from "zod";
|
|
4986
5109
|
function notifyUserTool() {
|
|
4987
|
-
return
|
|
5110
|
+
return tool12({
|
|
4988
5111
|
description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work. Only the latest update is shown on screen, in one line, so keep it under 140 characters.`,
|
|
4989
|
-
inputSchema:
|
|
4990
|
-
message:
|
|
5112
|
+
inputSchema: z19.object({
|
|
5113
|
+
message: z19.string().max(140).describe(
|
|
4991
5114
|
"Short, plain-language description of what you are doing now. Under 140 characters \u2014 only the latest update is shown, on one line."
|
|
4992
5115
|
)
|
|
4993
5116
|
}),
|
|
@@ -5025,6 +5148,7 @@ function createTools(ctx, { output, tools }) {
|
|
|
5025
5148
|
reportStatus: withLogging("reportStatus", reportStatusTool(output)),
|
|
5026
5149
|
readFile: withLogging("readFile", readFileTool(ctx)),
|
|
5027
5150
|
writeFile: withLogging("writeFile", writeFileTool(ctx)),
|
|
5151
|
+
editFile: withLogging("editFile", editFileTool(ctx)),
|
|
5028
5152
|
writeCredentials: withLogging(
|
|
5029
5153
|
"writeCredentials",
|
|
5030
5154
|
writeCredentialsTool(ctx)
|
|
@@ -5052,9 +5176,19 @@ var FS_READ_TOOLS = [
|
|
|
5052
5176
|
var MISSING_REPORT_STATUS_ERROR_MESSAGE = "Agent finished without calling reportStatus";
|
|
5053
5177
|
var MISSING_REPORT_USER_MESSAGE = "This step ran into a problem finishing. Run the wizard again to retry it.";
|
|
5054
5178
|
var REPORT_STATUS_RETRIES = 2;
|
|
5179
|
+
var STEP_LIMIT_INSTRUCTION = "When reportStatus is the only tool available, you reached the step limit. This limit is not a failure, so do not report fail because of it: report status=success with the work done so far, and name the remaining work in the summary.";
|
|
5180
|
+
var RECENT_STEPS_LOGGED = 3;
|
|
5181
|
+
var LOGGED_CHARS = 300;
|
|
5055
5182
|
var ATTEMPT_NUMBER_OFFSET = 1;
|
|
5056
5183
|
var INITIAL_ATTEMPT = 0;
|
|
5057
5184
|
var PROVIDER_ERROR_USER_MESSAGE = "The AI service had trouble responding. Run the wizard again to retry this step.";
|
|
5185
|
+
var SESSION_TIMEOUT_USER_MESSAGE = "The wizard session timed out after a period of inactivity. Run the wizard again to retry this step.";
|
|
5186
|
+
var PROXY_AUTH_REJECTED_HEADER2 = "x-wizard-proxy-auth";
|
|
5187
|
+
var PROXY_REJECTION_BODY = "You shall not pass!";
|
|
5188
|
+
function isProxyInactivityRejection(err) {
|
|
5189
|
+
const cause = RetryError.isInstance(err) ? err.lastError : err;
|
|
5190
|
+
return APICallError.isInstance(cause) && cause.statusCode === 401 && cause.responseBody === PROXY_REJECTION_BODY && cause.responseHeaders?.[PROXY_AUTH_REJECTED_HEADER2] === void 0;
|
|
5191
|
+
}
|
|
5058
5192
|
var MissingReportError = class extends Error {
|
|
5059
5193
|
atStepLimit;
|
|
5060
5194
|
constructor(atStepLimit) {
|
|
@@ -5062,6 +5196,13 @@ var MissingReportError = class extends Error {
|
|
|
5062
5196
|
this.atStepLimit = atStepLimit;
|
|
5063
5197
|
}
|
|
5064
5198
|
};
|
|
5199
|
+
var AgentReportedFailureError = class extends Error {
|
|
5200
|
+
reason;
|
|
5201
|
+
constructor(reason) {
|
|
5202
|
+
super(`Agent reported failure: ${reason ?? "no reason given"}`);
|
|
5203
|
+
this.reason = reason;
|
|
5204
|
+
}
|
|
5205
|
+
};
|
|
5065
5206
|
function isMissingReportRetry(kind) {
|
|
5066
5207
|
return kind === "missingReport" || kind === "missingReportAtStepLimit";
|
|
5067
5208
|
}
|
|
@@ -5134,6 +5275,7 @@ async function runAgentAttempt(req, attempt, retryReason) {
|
|
|
5134
5275
|
"When you need to read or search multiple files, issue those tool calls together in one step rather than one at a time."
|
|
5135
5276
|
] : [],
|
|
5136
5277
|
"Call notifyUser whenever you start a new phase of work or your focus shifts (e.g. moving from reading code to writing files) \u2014 a short, plain-language, high-level update. Do not call it for every tool use.",
|
|
5278
|
+
STEP_LIMIT_INSTRUCTION,
|
|
5137
5279
|
// Last so a retry does not bust the cached tools+system prefix from
|
|
5138
5280
|
// the first attempt. Inspect-and-continue, not start over: implement
|
|
5139
5281
|
// shares the worktree and toolContext across attempts.
|
|
@@ -5181,7 +5323,11 @@ async function runAgentAttempt(req, attempt, retryReason) {
|
|
|
5181
5323
|
stopWhen: [
|
|
5182
5324
|
hasToolCall("reportStatus"),
|
|
5183
5325
|
stepCountIs(profile.maxSteps)
|
|
5184
|
-
]
|
|
5326
|
+
],
|
|
5327
|
+
prepareStep: ({ stepNumber }) => stepNumber >= profile.maxSteps - 1 ? { activeTools: ["reportStatus"] } : void 0,
|
|
5328
|
+
// Not in the settings type, but forwarded to streamText, whose default prints over the UI.
|
|
5329
|
+
...{ onError: () => {
|
|
5330
|
+
} }
|
|
5185
5331
|
});
|
|
5186
5332
|
const stream = await agent.stream({
|
|
5187
5333
|
prompt: "Follow system instructions"
|
|
@@ -5208,6 +5354,9 @@ async function runAgentAttempt(req, attempt, retryReason) {
|
|
|
5208
5354
|
}
|
|
5209
5355
|
if (streamError !== void 0) {
|
|
5210
5356
|
logger.error({ err: streamError }, "agent stream reported an error");
|
|
5357
|
+
if (isProxyInactivityRejection(streamError)) {
|
|
5358
|
+
throw new Error(SESSION_TIMEOUT_USER_MESSAGE, { cause: streamError });
|
|
5359
|
+
}
|
|
5211
5360
|
throw streamError;
|
|
5212
5361
|
}
|
|
5213
5362
|
const end = Date.now();
|
|
@@ -5257,19 +5406,33 @@ async function runAgentAttempt(req, attempt, retryReason) {
|
|
|
5257
5406
|
}
|
|
5258
5407
|
const result = report.output;
|
|
5259
5408
|
if (result.status !== "success") {
|
|
5260
|
-
|
|
5261
|
-
|
|
5409
|
+
const steps = await stream.steps;
|
|
5410
|
+
logger.warn(
|
|
5411
|
+
{
|
|
5412
|
+
reason: result.reason,
|
|
5413
|
+
stepCount: steps.length,
|
|
5414
|
+
recentSteps: steps.slice(-RECENT_STEPS_LOGGED).map((s) => ({
|
|
5415
|
+
text: s.text?.slice(0, LOGGED_CHARS),
|
|
5416
|
+
toolCalls: s.toolCalls?.map((c) => c.toolName),
|
|
5417
|
+
toolResults: s.toolResults?.map((r) => ({
|
|
5418
|
+
tool: r.toolName,
|
|
5419
|
+
output: JSON.stringify(r.output)?.slice(0, LOGGED_CHARS)
|
|
5420
|
+
}))
|
|
5421
|
+
}))
|
|
5422
|
+
},
|
|
5423
|
+
"agent reported failure"
|
|
5262
5424
|
);
|
|
5425
|
+
throw new AgentReportedFailureError(result.reason);
|
|
5263
5426
|
}
|
|
5264
5427
|
return result.output;
|
|
5265
5428
|
}
|
|
5266
5429
|
|
|
5267
5430
|
// src/actions/detectLanguage.ts
|
|
5268
|
-
import
|
|
5269
|
-
var detectLanguageSchema =
|
|
5270
|
-
languages:
|
|
5271
|
-
frameworks:
|
|
5272
|
-
publicEnvVarPrefix:
|
|
5431
|
+
import z22 from "zod";
|
|
5432
|
+
var detectLanguageSchema = z22.object({
|
|
5433
|
+
languages: z22.array(z22.object({ name: z22.string(), version: z22.string() })),
|
|
5434
|
+
frameworks: z22.array(z22.object({ name: z22.string(), version: z22.string() })),
|
|
5435
|
+
publicEnvVarPrefix: z22.string().regex(/^([A-Z0-9]+_)*$/).describe(
|
|
5273
5436
|
`The prefix a bundler/framework requires for an env var to reach client-side code (e.g. "NEXT_PUBLIC_", "VITE_", "NUXT_PUBLIC_"), uppercase with a trailing underscore when non-empty. Prefer the project's own existing convention over a framework default. Empty string when the project has no client-side env var exposure (e.g. a backend-only project).`
|
|
5274
5437
|
)
|
|
5275
5438
|
});
|
|
@@ -5329,31 +5492,31 @@ var detectLanguage = async () => {
|
|
|
5329
5492
|
};
|
|
5330
5493
|
|
|
5331
5494
|
// src/actions/analyzeCodebase.ts
|
|
5332
|
-
import
|
|
5495
|
+
import z23 from "zod";
|
|
5333
5496
|
var READONLY_TOOLS = [
|
|
5334
5497
|
"listFiles",
|
|
5335
5498
|
"changeDirectory",
|
|
5336
5499
|
"readFile",
|
|
5337
5500
|
"searchFiles"
|
|
5338
5501
|
];
|
|
5339
|
-
var ingestionAnalysisSchema =
|
|
5340
|
-
ingestionAnalysis:
|
|
5341
|
-
|
|
5342
|
-
name:
|
|
5343
|
-
paths:
|
|
5502
|
+
var ingestionAnalysisSchema = z23.object({
|
|
5503
|
+
ingestionAnalysis: z23.array(
|
|
5504
|
+
z23.object({
|
|
5505
|
+
name: z23.string(),
|
|
5506
|
+
paths: z23.array(z23.string()),
|
|
5344
5507
|
// indexable fields the agent found for this entity
|
|
5345
|
-
attributes:
|
|
5508
|
+
attributes: z23.array(z23.string())
|
|
5346
5509
|
})
|
|
5347
5510
|
)
|
|
5348
5511
|
});
|
|
5349
|
-
var searchImplementationAnalysisSchema =
|
|
5350
|
-
searchImplementationAnalysis:
|
|
5512
|
+
var searchImplementationAnalysisSchema = z23.object({
|
|
5513
|
+
searchImplementationAnalysis: z23.string()
|
|
5351
5514
|
});
|
|
5352
|
-
var verificationSchema =
|
|
5353
|
-
verification:
|
|
5515
|
+
var verificationSchema = z23.object({
|
|
5516
|
+
verification: z23.array(z23.string())
|
|
5354
5517
|
});
|
|
5355
5518
|
var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
|
|
5356
|
-
var analyzeCodebaseSchema =
|
|
5519
|
+
var analyzeCodebaseSchema = z23.object({
|
|
5357
5520
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
5358
5521
|
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
|
|
5359
5522
|
verification: verificationSchema.shape.verification.optional(),
|
|
@@ -5475,8 +5638,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
|
|
|
5475
5638
|
}
|
|
5476
5639
|
|
|
5477
5640
|
// src/actions/confirmLanguage.ts
|
|
5478
|
-
import
|
|
5479
|
-
var confirmLanguageSchema =
|
|
5641
|
+
import z25 from "zod";
|
|
5642
|
+
var confirmLanguageSchema = z25.object({
|
|
5480
5643
|
languages: detectLanguageSchema.shape.languages
|
|
5481
5644
|
});
|
|
5482
5645
|
async function confirmLanguage(ctx) {
|
|
@@ -5497,8 +5660,8 @@ async function confirmLanguage(ctx) {
|
|
|
5497
5660
|
}
|
|
5498
5661
|
|
|
5499
5662
|
// src/actions/confirmFramework.ts
|
|
5500
|
-
import
|
|
5501
|
-
var confirmFrameworkSchema =
|
|
5663
|
+
import z26 from "zod";
|
|
5664
|
+
var confirmFrameworkSchema = z26.object({
|
|
5502
5665
|
frameworks: detectLanguageSchema.shape.frameworks
|
|
5503
5666
|
});
|
|
5504
5667
|
var CURATED_FRAMEWORKS = [
|
|
@@ -5636,8 +5799,8 @@ async function promptUser(ctx, params) {
|
|
|
5636
5799
|
}
|
|
5637
5800
|
|
|
5638
5801
|
// src/actions/confirmEntities.ts
|
|
5639
|
-
import
|
|
5640
|
-
var confirmEntitiesSchema =
|
|
5802
|
+
import z27 from "zod";
|
|
5803
|
+
var confirmEntitiesSchema = z27.object({
|
|
5641
5804
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
5642
5805
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
5643
5806
|
confirmedEntities: confirmedEntitiesFieldSchema,
|
|
@@ -5788,15 +5951,15 @@ async function confirmEntities(ctx) {
|
|
|
5788
5951
|
}
|
|
5789
5952
|
|
|
5790
5953
|
// src/actions/review.ts
|
|
5791
|
-
import { z as
|
|
5792
|
-
var reviewSchema =
|
|
5954
|
+
import { z as z28 } from "zod";
|
|
5955
|
+
var reviewSchema = z28.object({
|
|
5793
5956
|
// Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
|
|
5794
5957
|
// not one entry per workflow step — a step's raw output can be a long,
|
|
5795
5958
|
// multi-paragraph blob (see implement.ts's summaries.join), and mirroring
|
|
5796
5959
|
// that 1:1 is what made the old per-step summary an unreadable wall of text.
|
|
5797
|
-
summaryPoints:
|
|
5798
|
-
reviewPrompt:
|
|
5799
|
-
nextSteps:
|
|
5960
|
+
summaryPoints: z28.array(z28.string()),
|
|
5961
|
+
reviewPrompt: z28.string(),
|
|
5962
|
+
nextSteps: z28.array(z28.string())
|
|
5800
5963
|
});
|
|
5801
5964
|
function formatCompletedSteps(steps) {
|
|
5802
5965
|
if (!steps.length) return "(no prior steps completed)";
|
|
@@ -5831,9 +5994,17 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
5831
5994
|
};
|
|
5832
5995
|
|
|
5833
5996
|
// src/actions/implement.ts
|
|
5834
|
-
import
|
|
5835
|
-
import { access, mkdir as mkdir5, readFile as
|
|
5836
|
-
import {
|
|
5997
|
+
import z29 from "zod";
|
|
5998
|
+
import { access, mkdir as mkdir5, readdir as readdir3, readFile as readFile10 } from "node:fs/promises";
|
|
5999
|
+
import {
|
|
6000
|
+
dirname as dirname5,
|
|
6001
|
+
extname,
|
|
6002
|
+
isAbsolute as isAbsolute4,
|
|
6003
|
+
join as join11,
|
|
6004
|
+
relative as relative7,
|
|
6005
|
+
resolve as resolve5,
|
|
6006
|
+
sep as sep3
|
|
6007
|
+
} from "node:path";
|
|
5837
6008
|
|
|
5838
6009
|
// src/lib/git.ts
|
|
5839
6010
|
import { execFile as execFile3 } from "node:child_process";
|
|
@@ -5913,6 +6084,12 @@ function toRootRelative(p) {
|
|
|
5913
6084
|
return p.replace(/^\/+/, "");
|
|
5914
6085
|
}
|
|
5915
6086
|
|
|
6087
|
+
// src/lib/dashboard.ts
|
|
6088
|
+
function browseUrl(appId, indexName) {
|
|
6089
|
+
const base = appId ? `https://dashboard.algolia.com/apps/${appId}/explorer/browse` : "https://dashboard.algolia.com/explorer/browse";
|
|
6090
|
+
return `${base}/${encodeURIComponent(indexName)}`;
|
|
6091
|
+
}
|
|
6092
|
+
|
|
5916
6093
|
// src/lib/algoliaDocs.ts
|
|
5917
6094
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
5918
6095
|
import { dirname as dirname4, join as join10 } from "node:path";
|
|
@@ -5972,36 +6149,36 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
5972
6149
|
}
|
|
5973
6150
|
|
|
5974
6151
|
// src/actions/implement.ts
|
|
5975
|
-
var implementSchema =
|
|
5976
|
-
summary:
|
|
5977
|
-
ingestCommand:
|
|
5978
|
-
ingestScriptRan:
|
|
5979
|
-
ingestRecordCount:
|
|
5980
|
-
ingestDurationMs:
|
|
5981
|
-
ingestionSource:
|
|
5982
|
-
searchConfig:
|
|
5983
|
-
filePath:
|
|
5984
|
-
vars:
|
|
5985
|
-
|
|
5986
|
-
name:
|
|
5987
|
-
value:
|
|
6152
|
+
var implementSchema = z29.object({
|
|
6153
|
+
summary: z29.string(),
|
|
6154
|
+
ingestCommand: z29.string().optional(),
|
|
6155
|
+
ingestScriptRan: z29.boolean().optional(),
|
|
6156
|
+
ingestRecordCount: z29.number().optional(),
|
|
6157
|
+
ingestDurationMs: z29.number().optional(),
|
|
6158
|
+
ingestionSource: z29.enum(["local", "fileUpload", "generated"]),
|
|
6159
|
+
searchConfig: z29.object({
|
|
6160
|
+
filePath: z29.string().optional(),
|
|
6161
|
+
vars: z29.array(
|
|
6162
|
+
z29.object({
|
|
6163
|
+
name: z29.string(),
|
|
6164
|
+
value: z29.string()
|
|
5988
6165
|
})
|
|
5989
6166
|
)
|
|
5990
6167
|
}).optional()
|
|
5991
6168
|
});
|
|
5992
|
-
var implementationOutputSchema =
|
|
5993
|
-
summary:
|
|
5994
|
-
ingestCommand:
|
|
6169
|
+
var implementationOutputSchema = z29.object({
|
|
6170
|
+
summary: z29.string(),
|
|
6171
|
+
ingestCommand: z29.string().optional(),
|
|
5995
6172
|
// Only for the search use case: the path of whatever module the agent
|
|
5996
6173
|
// defined the Algolia config constants in, so the wizard can check it
|
|
5997
6174
|
// won't end up gitignored (it's public, meant to be committed).
|
|
5998
|
-
searchConfigFile:
|
|
6175
|
+
searchConfigFile: z29.string().optional()
|
|
5999
6176
|
});
|
|
6000
|
-
var validationOutputSchema =
|
|
6001
|
-
summary:
|
|
6002
|
-
sufficient:
|
|
6003
|
-
additionalInstructions:
|
|
6004
|
-
unrelatedFailure:
|
|
6177
|
+
var validationOutputSchema = z29.object({
|
|
6178
|
+
summary: z29.string(),
|
|
6179
|
+
sufficient: z29.boolean(),
|
|
6180
|
+
additionalInstructions: z29.string().optional(),
|
|
6181
|
+
unrelatedFailure: z29.string().optional()
|
|
6005
6182
|
});
|
|
6006
6183
|
var MAX_IMPLEMENT_VALIDATION_ATTEMPTS = 3;
|
|
6007
6184
|
var FIRST_IMPLEMENTATION_ATTEMPT = 1;
|
|
@@ -6013,8 +6190,8 @@ var INGESTION_SOURCE = {
|
|
|
6013
6190
|
fileUpload: "Import a data file (CSV, JSON, TSV)",
|
|
6014
6191
|
generated: "Generate sample data"
|
|
6015
6192
|
};
|
|
6016
|
-
var BUILD_CHECK_PROMPT = "Run type-check
|
|
6017
|
-
var MISSING_DEPENDENCIES_PROMPT = "Dependencies are not installed. Install them and run type-check
|
|
6193
|
+
var BUILD_CHECK_PROMPT = "Run type-check now? This can take several minutes on a large repository.";
|
|
6194
|
+
var MISSING_DEPENDENCIES_PROMPT = "Dependencies are not installed. Install them and run type-check now?";
|
|
6018
6195
|
var UNRELATED_FAILURE_PROMPT = "These checks are still failing and appear unrelated to the changes. Continue the investigation?";
|
|
6019
6196
|
var FILE_UPLOAD_PATH_PROMPT = "Enter the path to your JSON, CSV, or TSV file (relative to the project root, or absolute):";
|
|
6020
6197
|
var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
|
|
@@ -6053,10 +6230,12 @@ function baseInstructions(input) {
|
|
|
6053
6230
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
6054
6231
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
6055
6232
|
`Commands run through a shell on ${process.platform}. Write commands that work there.`,
|
|
6056
|
-
"Use the project's own tooling for every command \u2014 its package manager
|
|
6233
|
+
"Use the project's own tooling for every command \u2014 its package manager and task runner.",
|
|
6234
|
+
"Never run a linter or formatter, and do not fix lint or formatting issues. The developer did not ask for them.",
|
|
6057
6235
|
"runShell needs the developer to approve each command, so give every call a clear `explanation` naming what it does and any side effect. If a command is rejected, do not retry it \u2014 take a different approach or report the limitation."
|
|
6058
6236
|
];
|
|
6059
6237
|
}
|
|
6238
|
+
var FILE_EDIT_INSTRUCTION = "To change an existing file, call editFile with only the changed part. Use writeFile only to create a file; never rewrite an existing file with writeFile.";
|
|
6060
6239
|
function sourceSpecificInstructions(input) {
|
|
6061
6240
|
const byLine = {
|
|
6062
6241
|
local: [
|
|
@@ -6089,15 +6268,23 @@ function algoliaClientDoc(input) {
|
|
|
6089
6268
|
doc
|
|
6090
6269
|
];
|
|
6091
6270
|
}
|
|
6271
|
+
var LARGE_RECORD_PLAN = /^(grow|elevate|premium)/i;
|
|
6272
|
+
function recordSizeInstruction(plan) {
|
|
6273
|
+
const limit = plan && LARGE_RECORD_PLAN.test(plan.trim()) ? "Algolia rejects a record larger than 100 KB with a status 400 error, and the average record size in the index must stay under 10 KB. Keep every record well under 100 KB and the average under 10 KB" : "Algolia rejects a record larger than 10 KB with a status 400 error. Keep every record under 10 KB";
|
|
6274
|
+
return `${limit}: split long text fields (for example an article body) into several records that share a parent identifier, or truncate them. Never push the whole text of a long document as one record. Check the size of the largest source item before you write the script.`;
|
|
6275
|
+
}
|
|
6092
6276
|
function ingestionInstructions(input) {
|
|
6093
6277
|
return [
|
|
6094
6278
|
...input.confirmed && input.confirmed.length ? [
|
|
6095
6279
|
`Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
|
|
6280
|
+
FILE_EDIT_INSTRUCTION,
|
|
6096
6281
|
`Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
6097
6282
|
`Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
|
|
6098
6283
|
`Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
|
|
6099
6284
|
"Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
|
|
6100
6285
|
"After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
|
|
6286
|
+
recordSizeInstruction(input.appPlan),
|
|
6287
|
+
"When you split one source item into several records, keep the fields that the search UI shows (title, description, URL parts) on every record. After you push the records, the script must set the index settings with attributeForDistinct set to the parent identifier and distinct set to true, so that a search returns each source item once.",
|
|
6101
6288
|
...algoliaClientDoc(input),
|
|
6102
6289
|
"Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
|
|
6103
6290
|
`If the script loads its env vars from a file (e.g. via dotenv or an equivalent for its language) rather than the process environment directly, decide that up front and call writeCredentials on that file before you finish the script \u2014 do not wait to discover the need for it by having a writeFile call refused.`,
|
|
@@ -6127,7 +6314,7 @@ function searchInstructions(input) {
|
|
|
6127
6314
|
const placementMessage = "Place content-specific search beside the heading or controls for the corresponding browse/listing task. Use shared navigation only for genuinely app-wide search and only when it fits the available space and responsive layout without crowding primary actions.";
|
|
6128
6315
|
const replacementMessage = "If a search control with the same content scope and purpose already exists, replace its usage with the new component while preserving its intentional placement, and delete the implementation it supersedes. Do not remove or replace unrelated filters, navigation, or differently scoped search.";
|
|
6129
6316
|
const accessibilityInstructions = [
|
|
6130
|
-
"Target WCAG 2.2 AA. Use semantic search structure and give the input a persistent accessible name: prefer a visible associated label and use the project's visually-hidden utility where the layout has no room for one. A placeholder or a labeled search landmark does not name the input. When the input uses aria-labelledby, every referenced element must exist and contain meaningful, content-specific text. Preserve a widget's generated label when it provides one and make it content-specific through supported options
|
|
6317
|
+
"Target WCAG 2.2 AA. Use semantic search structure and give the input a persistent accessible name: prefer a visible associated label and use the project's visually-hidden utility where the layout has no room for one. A placeholder or a labeled search landmark does not name the input. When the input uses aria-labelledby, every referenced element must exist and contain meaningful, content-specific text. Preserve a widget's generated label when it provides one and make it content-specific through supported options. If the SDK reference says that no option changes the generated label, accept that label as a known widget limitation: do not rebuild the widget headless, do not patch generated DOM, and name the gap in the summary.",
|
|
6131
6318
|
"Preserve widget-provided combobox, popup, and item roles, relationships, and keyboard behavior; for a headless widget, implement the documented semantics instead of inventing different ARIA. Tab must reach the input and controls, Arrow keys move through results, Enter selects, and Escape dismisses without trapping or losing focus.",
|
|
6132
6319
|
"Meet 4.5:1 contrast for normal text, including input values, placeholders, and result text, and 3:1 for large text, meaningful icons, control boundaries, and focus indicators against adjacent colors. Keep focus visible and unobscured, and never communicate active, loading, empty, or error states by color alone.",
|
|
6133
6320
|
"Keep loading, empty, and error status text perceivable to assistive technology. Do not assume a widget announces these states: use documented announcements when present, otherwise add a persistent polite live region driven by the current search status, results, and errors.",
|
|
@@ -6139,22 +6326,66 @@ function searchInstructions(input) {
|
|
|
6139
6326
|
indexedContentMessage ?? "",
|
|
6140
6327
|
`Build the search UI for ${input.searchUiTarget}.`,
|
|
6141
6328
|
"Create search UI only. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
|
|
6329
|
+
FILE_EDIT_INSTRUCTION,
|
|
6330
|
+
EXISTING_COMPONENT_INSTRUCTION,
|
|
6142
6331
|
mountCandidateMessage,
|
|
6143
6332
|
placementMessage,
|
|
6144
6333
|
replacementMessage,
|
|
6145
6334
|
"Inspect the chosen mount area at the app's existing breakpoints before writing CSS. Preserve its visual hierarchy, alignment, spacing, and primary actions rather than forcing the search control into an arbitrary gap.",
|
|
6146
6335
|
...packageManagedInstructions,
|
|
6336
|
+
CSP_INSTRUCTION,
|
|
6147
6337
|
...accessibilityInstructions,
|
|
6148
6338
|
"Style the search surfaces through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens): give the results panel and the element that actually draws the input's border and background \u2014 for a widget, the form or wrapper it renders around the bare input \u2014 non-transparent surfaces, keep that frame distinguishable from the background behind it in every supported theme, and never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
|
|
6149
6339
|
"The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it `min-width: min(320px, calc(100vw - 2rem))` independent of the input, cap its maximum width to the viewport, and anchor it to the input edge it opens from so widening does not push it off-screen.",
|
|
6150
6340
|
"Match the application styles closely, including light, dark, and high-contrast themes the project already supports.",
|
|
6151
|
-
"Before reporting, mount the component
|
|
6341
|
+
"Before reporting, mount the component. Do not run a type check, build, or test command: the validation pass runs the type check, after the developer agrees to it. If a tool limit is reached or an error remains unresolved, keep the implementation, report status=success, and name the remaining issue in the summary so validation can drive the next pass. Report status=fail only when no search implementation can be written, with the concrete blocker as the reason.",
|
|
6152
6342
|
"The summary should be extremely concise; do not mention manual testing steps."
|
|
6153
6343
|
];
|
|
6154
6344
|
}
|
|
6345
|
+
var EXISTING_COMPONENT_INSTRUCTION = "A search component for this content can already exist, for example from an earlier run. Do not trust it: compare the record fields that it reads with the real records, and rewrite it when they differ. Do not report success while the UI reads a field that no record has.";
|
|
6346
|
+
var CSP_INSTRUCTION = 'Search for a Content-Security-Policy (next.config, middleware, proxy, server headers, or a meta tag). If the project sets one, it blocks Algolia until you allow it: add https://*.algolia.net, https://*.algolianet.com, and https://insights.algolia.io to connect-src, and load search-insights from npm as the SDK reference describes instead of from jsDelivr. Without this, every search fails with "Unreachable hosts" and the widget stays empty.';
|
|
6347
|
+
var INGEST_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6348
|
+
".sh",
|
|
6349
|
+
".py",
|
|
6350
|
+
".js",
|
|
6351
|
+
".mjs",
|
|
6352
|
+
".cjs",
|
|
6353
|
+
".ts",
|
|
6354
|
+
".rb",
|
|
6355
|
+
".php",
|
|
6356
|
+
".go"
|
|
6357
|
+
]);
|
|
6358
|
+
async function findIngestionScript(repoRoot, ingestDir, ingestCommand) {
|
|
6359
|
+
const fromCommand = ingestCommand?.split(/\s+/).map((token) => token.replace(/^["']|["']$/g, "")).find((token) => token.startsWith(`${ingestDir}/`));
|
|
6360
|
+
if (fromCommand) {
|
|
6361
|
+
try {
|
|
6362
|
+
await access(join11(repoRoot, fromCommand));
|
|
6363
|
+
return fromCommand;
|
|
6364
|
+
} catch {
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
try {
|
|
6368
|
+
const entries = await readdir3(join11(repoRoot, ingestDir), {
|
|
6369
|
+
withFileTypes: true
|
|
6370
|
+
});
|
|
6371
|
+
const name = entries.filter(
|
|
6372
|
+
(entry) => entry.isFile() && !entry.name.startsWith(".") && INGEST_SCRIPT_EXTENSIONS.has(extname(entry.name))
|
|
6373
|
+
).map((entry) => entry.name).sort()[0];
|
|
6374
|
+
return name && `${ingestDir}/${name}`;
|
|
6375
|
+
} catch {
|
|
6376
|
+
return void 0;
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
function recordSourceInstruction(scriptPath) {
|
|
6380
|
+
return `The records in the index come from the ingestion script "${scriptPath}" (repo root). Read that one file to learn each record's objectID, its fields, and how they map to the app's data and URLs. Do not search the backend, api, or scripts folders for them, and do not use shell reads (sed, grep, cat) to explore. Read the fields with their exact attribute names from the records: a dotted name such as "copy.en.title" is one flat attribute, so read it as item["copy.en.title"]. Use only the fields of the current locale. When the script splits one item into several records that share a parent identifier, the index returns each item once, so show one result per item.`;
|
|
6381
|
+
}
|
|
6382
|
+
function recordFieldCheckInstruction(scriptPath) {
|
|
6383
|
+
return `Read the ingestion script "${scriptPath}" (repo root). Check that every record field that the search UI reads (title, description, link parts) exists in the records with that exact attribute name. A dotted name such as "copy.en.title" is one flat attribute. If the UI reads a field that no record has, set sufficient=false and name the wrong field and the right one in additionalInstructions. Do not use shell reads to explore.`;
|
|
6384
|
+
}
|
|
6155
6385
|
function packageSearchInstructions(input) {
|
|
6156
6386
|
const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
|
|
6157
6387
|
return [
|
|
6388
|
+
"To check an installed package version, call readFile on node_modules/<package>/package.json. Do not otherwise explore node_modules, and do not run node -e or other scripts to discover a package API. If the reference below does not cover a detail, choose the documented option and name the gap in the summary.",
|
|
6158
6389
|
...doc ? [
|
|
6159
6390
|
"Follow the Algolia SDK reference below for client setup, search UI wiring, and Insights instrumentation \u2014 Insights is required, not optional; prefer the reference over prior knowledge:",
|
|
6160
6391
|
doc
|
|
@@ -6188,25 +6419,30 @@ function validationInstructions(input) {
|
|
|
6188
6419
|
"Validate the Algolia search implementation by inspecting the project source.",
|
|
6189
6420
|
`Indexed content context: ${JSON.stringify(input.findings.confirmedEntities?.[0] ?? null)}.`,
|
|
6190
6421
|
`The confirmed placement candidate was "${input.searchLocation ?? "unknown"}", but verify the actual mount rather than assuming the candidate was used.`,
|
|
6191
|
-
"Inspect the generated search component, its actual mount file, that file's parent layout and sibling controls, and the relevant content UI. Do not rely only on
|
|
6422
|
+
"Inspect the generated search component, its actual mount file, that file's parent layout and sibling controls, and the relevant content UI. Do not rely only on type checks or tests.",
|
|
6192
6423
|
"Confirm that the application renders exactly one search bar. Check for an old search bar and duplicate mounts.",
|
|
6424
|
+
"If the project sets a Content-Security-Policy, confirm that connect-src allows https://*.algolia.net, https://*.algolianet.com, and https://insights.algolia.io, and that search-insights is loaded from npm. Otherwise set sufficient=false.",
|
|
6193
6425
|
"Confirm content-specific search is placed with the corresponding content or task controls. Shared navigation is valid only for genuinely app-wide search that fits the visual hierarchy and responsive layout without crowding primary actions.",
|
|
6194
6426
|
"Confirm the input has a persistent accessible name; widget-provided combobox, popup, and item relationships and keyboard behavior are preserved; and loading, empty, and error status text is perceivable to assistive technology.",
|
|
6195
6427
|
"Inspect the CSS that actually applies to the generated DOM. Confirm the results panel and the element drawing the input frame have non-transparent surfaces, the input frame is distinguishable from its surrounding background in every supported theme, text meets 4.5:1 contrast, meaningful boundaries and focus indicators meet 3:1, and no state relies on color alone.",
|
|
6196
6428
|
"Confirm the search experience reflows without horizontal page scrolling at 320 CSS pixels, keeps its panel on-screen, preserves visible focus, and respects reduced-motion preferences for any added animation.",
|
|
6429
|
+
"If the project imports an InstantSearch theme and overrides its palette, confirm that the palette overrides (--ais-*-rgb) win over the theme: they must use a higher specificity than the theme, for example `:root:root`, and for a dark mode `:root:root.dark` or `:root:root[data-theme=dark]`, because the theme sets its own blue in `:root` and in `:root[data-theme=dark], .dark`. `:root:root` alone ties with `:root[data-theme=dark]`. If an override can lose to the theme, set sufficient=false, because the search icon and focus ring then show the theme blue instead of the app accent color.",
|
|
6430
|
+
"When the project has a package.json, confirm that the theme CSS of the search widget (for InstantSearch, a theme from instantsearch.css) is imported once, from the component or the global stylesheet. If it is installed but not imported, set sufficient=false.",
|
|
6197
6431
|
"Confirm that the search component is reachable from and fits the corresponding content task at every supported breakpoint; require app-wide reachability only when the search scope is genuinely app-wide.",
|
|
6198
6432
|
"Do not run the test suite or any test command.",
|
|
6199
6433
|
"Do not modify files. Report each implementation issue as a concrete instruction for the next implementation pass.",
|
|
6200
|
-
`
|
|
6434
|
+
`Detected verification tools: ${JSON.stringify(input.findings.verification ?? [])}. Use only a type checker; ignore any linter or formatter in this list.`,
|
|
6201
6435
|
...!input.frontendHasPackageJson ? [
|
|
6202
6436
|
"The frontend has no package.json: never create one or run npm, pnpm, yarn, or bun. Validate the CDN implementation by inspecting its HTML or template and classic scripts for the search container, Algolia \u2192 InstantSearch \u2192 config \u2192 search load order, browser globals, autocomplete widget configuration, search.start(), and exact pinned CDN URLs and integrity attributes for every Algolia script or stylesheet. Source inspection is required even when the project has no automated checks. window.instantsearch.widgets.autocomplete is a real widget in the pinned InstantSearch build; do not report its use as an error, and do not fetch a remote bundle or URL to verify it."
|
|
6203
6437
|
] : [],
|
|
6204
6438
|
"Always call reportStatus with status=success after validation, even when sufficient=false.",
|
|
6205
6439
|
"Set sufficient=false if the input's computed accessible name is empty or generic, including when aria-labelledby references a missing or empty element; a placeholder does not count as the label.",
|
|
6440
|
+
'Exception: the InstantSearch Autocomplete widget (react-instantsearch or instantsearch.js) names its input "Submit" through its generated label (the input has aria-labelledby pointing at that label), and no widget option changes it. Accept that name when the widget is used unmodified, and do not ask for an aria-label, inputProps, or a label wrapper: do not set sufficient=false for it, and do not request a headless rebuild or DOM patch.',
|
|
6206
6441
|
"Set sufficient=true when the search bar is unique and structurally complete and no placement or accessibility violation was found.",
|
|
6207
6442
|
"Set sufficient=false when a required placement or accessibility property is observed to fail, the implementation is incomplete, or checks have implementation-caused failures; include concrete additionalInstructions for the next pass.",
|
|
6208
|
-
"If
|
|
6209
|
-
"If a check fails only in untouched code, set unrelatedFailure to a concise failure description."
|
|
6443
|
+
"You can only read source files. If a required property can be checked in the source, check it there. If it can be established only in a browser (rendered contrast, keyboard behavior, 320-pixel reflow, live-region status, computed accessible name), set sufficient=true when the source shows no violation. Never request devtools, a screenshot, or a contrast tool, and never repeat a finding that an earlier pass already fixed.",
|
|
6444
|
+
"If a check fails only in untouched code, set unrelatedFailure to a concise failure description.",
|
|
6445
|
+
"Keep the summary to one short sentence. Put the details in additionalInstructions."
|
|
6210
6446
|
];
|
|
6211
6447
|
}
|
|
6212
6448
|
var IMPLEMENT_CONFIG = {
|
|
@@ -6227,12 +6463,19 @@ var useCaseToolMap = {
|
|
|
6227
6463
|
ingestion: [
|
|
6228
6464
|
...FS_READ_TOOLS,
|
|
6229
6465
|
"writeFile",
|
|
6466
|
+
"editFile",
|
|
6230
6467
|
"writeCredentials",
|
|
6231
6468
|
"runShell",
|
|
6232
6469
|
"reviewScript",
|
|
6233
6470
|
"notifyUser"
|
|
6234
6471
|
],
|
|
6235
|
-
search: [
|
|
6472
|
+
search: [
|
|
6473
|
+
...FS_READ_TOOLS,
|
|
6474
|
+
"writeFile",
|
|
6475
|
+
"editFile",
|
|
6476
|
+
"runShell",
|
|
6477
|
+
"notifyUser"
|
|
6478
|
+
],
|
|
6236
6479
|
validation: [...FS_READ_TOOLS, "runShell", "notifyUser"]
|
|
6237
6480
|
};
|
|
6238
6481
|
function toolsForUseCase(useCase, ingestionSource) {
|
|
@@ -6306,7 +6549,7 @@ function jsTypeName(value) {
|
|
|
6306
6549
|
}
|
|
6307
6550
|
async function firstRecordIn(filePath) {
|
|
6308
6551
|
try {
|
|
6309
|
-
const parsed = JSON.parse(await
|
|
6552
|
+
const parsed = JSON.parse(await readFile10(filePath, "utf8"));
|
|
6310
6553
|
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
6311
6554
|
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
6312
6555
|
return first;
|
|
@@ -6363,6 +6606,12 @@ function makeToolContext(root, appId, env = async () => ({}), secretEnv = async
|
|
|
6363
6606
|
appId
|
|
6364
6607
|
};
|
|
6365
6608
|
}
|
|
6609
|
+
var ERROR_SUMMARY_MAX_CHARS = 200;
|
|
6610
|
+
function briefSummary(summary) {
|
|
6611
|
+
const firstLine = summary.trim().split("\n")[0] ?? "";
|
|
6612
|
+
if (!firstLine) return "no summary given";
|
|
6613
|
+
return firstLine.length > ERROR_SUMMARY_MAX_CHARS ? `${firstLine.slice(0, ERROR_SUMMARY_MAX_CHARS - 1)}\u2026` : firstLine;
|
|
6614
|
+
}
|
|
6366
6615
|
function validationRetryInstructions(validation) {
|
|
6367
6616
|
return [
|
|
6368
6617
|
`Implementation insufficient. Address these findings before reporting completion: ${validation.additionalInstructions ?? validation.summary}`
|
|
@@ -6420,7 +6669,7 @@ function installCheckInstructions(plan, attempt) {
|
|
|
6420
6669
|
}
|
|
6421
6670
|
return ["Dependencies were already installed. Do not install them again."];
|
|
6422
6671
|
}
|
|
6423
|
-
function validationRunInstructions(plan,
|
|
6672
|
+
function validationRunInstructions(plan, attempt) {
|
|
6424
6673
|
if (!plan.run) {
|
|
6425
6674
|
return [
|
|
6426
6675
|
"The developer chose to skip build checks. Do not install dependencies or run shell checks."
|
|
@@ -6431,7 +6680,6 @@ function validationRunInstructions(plan, changedFiles, attempt) {
|
|
|
6431
6680
|
...installCheckInstructions(plan, attempt),
|
|
6432
6681
|
"Do not use npx, dlx, or another package runner that can download an undeclared tool.",
|
|
6433
6682
|
"Run the type check once with the project's existing command, when one is available.",
|
|
6434
|
-
`Lint only these changed files: ${JSON.stringify(changedFiles)}. Do not run a full-project lint. If this list is empty, do not run lint.`,
|
|
6435
6683
|
"Do not use an autofix option during validation."
|
|
6436
6684
|
];
|
|
6437
6685
|
}
|
|
@@ -6526,11 +6774,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
6526
6774
|
const searchLocation = findings.searchImplementationAnalysis;
|
|
6527
6775
|
let appId;
|
|
6528
6776
|
let ingestAppId;
|
|
6777
|
+
let appPlan;
|
|
6529
6778
|
if (useCases.includes("search")) {
|
|
6530
|
-
|
|
6779
|
+
const app = await requireApplication(ctx.appId);
|
|
6780
|
+
appId = app.id;
|
|
6781
|
+
appPlan = app.plan;
|
|
6531
6782
|
}
|
|
6532
6783
|
if (useCases.includes("ingestion")) {
|
|
6533
|
-
|
|
6784
|
+
const app = appId ? void 0 : await requireApplication(ctx.appId);
|
|
6785
|
+
ingestAppId = appId ?? app?.id;
|
|
6786
|
+
appPlan ??= app?.plan;
|
|
6534
6787
|
}
|
|
6535
6788
|
let ingestWriteKey;
|
|
6536
6789
|
if (useCases.includes("ingestion") && ingestAppId) {
|
|
@@ -6564,6 +6817,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
6564
6817
|
targetIndex,
|
|
6565
6818
|
language,
|
|
6566
6819
|
appId,
|
|
6820
|
+
appPlan,
|
|
6567
6821
|
searchKey,
|
|
6568
6822
|
searchKeyError,
|
|
6569
6823
|
ingestDir: INGEST_DIR,
|
|
@@ -6577,6 +6831,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
6577
6831
|
};
|
|
6578
6832
|
let agentRuns = 0;
|
|
6579
6833
|
let ingestCommand;
|
|
6834
|
+
let ingestionScript;
|
|
6580
6835
|
let ingestScriptRan = false;
|
|
6581
6836
|
let ingestRecordCount;
|
|
6582
6837
|
let ingestDurationMs;
|
|
@@ -6596,18 +6851,31 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
6596
6851
|
async function runImplementationUseCase(currentUseCase, extraInstructions = [], isRetry = false) {
|
|
6597
6852
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
6598
6853
|
agentRuns += 1;
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
currentUseCase
|
|
6603
|
-
input,
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6854
|
+
const toolContext = currentUseCase === "ingestion" ? ingestionTools : makeToolContext(repoRoot);
|
|
6855
|
+
try {
|
|
6856
|
+
return await runAgent({
|
|
6857
|
+
operation: `${currentUseCase}-implementation`,
|
|
6858
|
+
instructions: buildAgentInstructions(currentUseCase, input, [
|
|
6859
|
+
...currentUseCase === "search" && ingestionScript ? [recordSourceInstruction(ingestionScript)] : [],
|
|
6860
|
+
...extraInstructions
|
|
6861
|
+
]),
|
|
6862
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
6863
|
+
outputSchema: implementationOutputSchema,
|
|
6864
|
+
modelProfile: isRetry ? "implementationRetry" /* implementationRetry */ : "implementation" /* implementation */,
|
|
6865
|
+
toolContext
|
|
6866
|
+
});
|
|
6867
|
+
} catch (err) {
|
|
6868
|
+
if (currentUseCase === "search" && err instanceof AgentReportedFailureError && readLimitReached(toolContext)) {
|
|
6869
|
+
logger.warn(
|
|
6870
|
+
{ reason: err.reason, counts: toolContext.counts },
|
|
6871
|
+
"implement: search agent reported fail at a read limit; continuing to validation"
|
|
6872
|
+
);
|
|
6873
|
+
return {
|
|
6874
|
+
summary: "Stopped at a read limit before the implementation was complete."
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
throw err;
|
|
6878
|
+
}
|
|
6611
6879
|
}
|
|
6612
6880
|
async function runValidationUseCase(extraInstructions) {
|
|
6613
6881
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
@@ -6619,6 +6887,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
6619
6887
|
operation: "search-validation",
|
|
6620
6888
|
instructions: buildAgentInstructions("validation", input, [
|
|
6621
6889
|
writtenFiles.length ? `Files written during search implementation: ${JSON.stringify(writtenFiles)}. Inspect these first, then trace the actual import and render site.` : "The search agent reported no written files. Inspect the current diff and set sufficient=false if no complete mounted search implementation exists.",
|
|
6890
|
+
...ingestionScript ? [recordFieldCheckInstruction(ingestionScript)] : [],
|
|
6622
6891
|
...extraInstructions
|
|
6623
6892
|
]),
|
|
6624
6893
|
tools: toolsForUseCase("validation"),
|
|
@@ -6714,7 +6983,7 @@ ${detail}` : ""}`
|
|
|
6714
6983
|
messages: [
|
|
6715
6984
|
ingestOutcomeMessage,
|
|
6716
6985
|
"View your ingested records in the Algolia dashboard:",
|
|
6717
|
-
|
|
6986
|
+
browseUrl(ingestAppId, targetIndex),
|
|
6718
6987
|
"When you're ready to continue, we'll move on to search implementation."
|
|
6719
6988
|
]
|
|
6720
6989
|
});
|
|
@@ -6739,6 +7008,11 @@ ${detail}` : ""}`
|
|
|
6739
7008
|
}
|
|
6740
7009
|
let searchConfigFile;
|
|
6741
7010
|
if (useCases.includes("search")) {
|
|
7011
|
+
ingestionScript = await findIngestionScript(
|
|
7012
|
+
repoRoot,
|
|
7013
|
+
INGEST_DIR,
|
|
7014
|
+
ingestCommand
|
|
7015
|
+
);
|
|
6742
7016
|
let extraInstructions = [];
|
|
6743
7017
|
let buildCheckPlan;
|
|
6744
7018
|
const confirmedFrameworks = language.frameworks ?? [];
|
|
@@ -6786,11 +7060,8 @@ ${detail}` : ""}`
|
|
|
6786
7060
|
searchConfigFile = searchResult.searchConfigFile;
|
|
6787
7061
|
}
|
|
6788
7062
|
buildCheckPlan ??= await resolveBuildCheckPlan(ctx, repoRoot, input);
|
|
6789
|
-
const searchFilesChanged = [
|
|
6790
|
-
...new Set(useWizard.getState().writtenFiles)
|
|
6791
|
-
].map((file) => relative7(repoRoot, file));
|
|
6792
7063
|
const validation = await runValidationUseCase(
|
|
6793
|
-
validationRunInstructions(buildCheckPlan,
|
|
7064
|
+
validationRunInstructions(buildCheckPlan, attempt)
|
|
6794
7065
|
);
|
|
6795
7066
|
summaries.push(formatSummary("validation", validation.summary));
|
|
6796
7067
|
const finalAttempt = attempt === MAX_IMPLEMENT_VALIDATION_ATTEMPTS;
|
|
@@ -6824,8 +7095,16 @@ ${detail}` : ""}`
|
|
|
6824
7095
|
if (finalAttempt) {
|
|
6825
7096
|
ctx.setUserInput("implementation", "fail");
|
|
6826
7097
|
trackComponentGenerated(false, attempt);
|
|
7098
|
+
logger.warn(
|
|
7099
|
+
{
|
|
7100
|
+
summary: validation.summary,
|
|
7101
|
+
additionalInstructions: validation.additionalInstructions,
|
|
7102
|
+
unrelatedFailure: validation.unrelatedFailure
|
|
7103
|
+
},
|
|
7104
|
+
"implement: search validation failed on the final attempt"
|
|
7105
|
+
);
|
|
6827
7106
|
throw new Error(
|
|
6828
|
-
`Unable to validate the implementation after ${MAX_IMPLEMENT_VALIDATION_ATTEMPTS} attempts
|
|
7107
|
+
`Unable to validate the implementation after ${MAX_IMPLEMENT_VALIDATION_ATTEMPTS} attempts (details in wizard.log): ${briefSummary(validation.summary)}`
|
|
6829
7108
|
);
|
|
6830
7109
|
}
|
|
6831
7110
|
extraInstructions = validation.sufficient ? [] : validationRetryInstructions(validation);
|
|
@@ -6911,8 +7190,8 @@ var defaultWorkflow = {
|
|
|
6911
7190
|
defineStep({
|
|
6912
7191
|
id: "select-index",
|
|
6913
7192
|
title: "Set up index",
|
|
6914
|
-
outputSchema:
|
|
6915
|
-
selection:
|
|
7193
|
+
outputSchema: z30.object({
|
|
7194
|
+
selection: z30.string()
|
|
6916
7195
|
}),
|
|
6917
7196
|
run: (ctx) => selectIndexStep(ctx)
|
|
6918
7197
|
}),
|
|
@@ -6973,7 +7252,7 @@ var defaultWorkflow = {
|
|
|
6973
7252
|
import "zod";
|
|
6974
7253
|
|
|
6975
7254
|
// src/actions/playbookSteps/selectPlaybook.ts
|
|
6976
|
-
import { z as
|
|
7255
|
+
import { z as z33 } from "zod";
|
|
6977
7256
|
|
|
6978
7257
|
// src/lib/dashboardApi/client.ts
|
|
6979
7258
|
import "zod";
|
|
@@ -7032,67 +7311,67 @@ async function dashboardError(res) {
|
|
|
7032
7311
|
}
|
|
7033
7312
|
|
|
7034
7313
|
// src/lib/dashboardApi/schemas.ts
|
|
7035
|
-
import { z as
|
|
7036
|
-
var messageResponseSchema =
|
|
7037
|
-
message:
|
|
7314
|
+
import { z as z32 } from "zod";
|
|
7315
|
+
var messageResponseSchema = z32.object({
|
|
7316
|
+
message: z32.string()
|
|
7038
7317
|
});
|
|
7039
|
-
var playbookSchema =
|
|
7040
|
-
slug:
|
|
7041
|
-
category:
|
|
7042
|
-
title:
|
|
7043
|
-
description:
|
|
7318
|
+
var playbookSchema = z32.object({
|
|
7319
|
+
slug: z32.string(),
|
|
7320
|
+
category: z32.string().optional(),
|
|
7321
|
+
title: z32.string(),
|
|
7322
|
+
description: z32.string().optional()
|
|
7044
7323
|
});
|
|
7045
|
-
var playbooksListSchema =
|
|
7046
|
-
var playbookExecutionStepSchema =
|
|
7324
|
+
var playbooksListSchema = z32.array(playbookSchema);
|
|
7325
|
+
var playbookExecutionStepSchema = z32.enum([
|
|
7047
7326
|
"assessment",
|
|
7048
7327
|
"optimization",
|
|
7049
7328
|
"implement"
|
|
7050
7329
|
]);
|
|
7051
|
-
var playbookExecutionStepStatusSchema =
|
|
7330
|
+
var playbookExecutionStepStatusSchema = z32.enum([
|
|
7052
7331
|
"not_started",
|
|
7053
7332
|
"in_progress",
|
|
7054
7333
|
"done",
|
|
7055
7334
|
"error"
|
|
7056
7335
|
]);
|
|
7057
|
-
var schemaComparisonEntrySchema =
|
|
7058
|
-
canonical_attr:
|
|
7059
|
-
canonical_type:
|
|
7060
|
-
canonical_roles:
|
|
7061
|
-
canonical_required:
|
|
7062
|
-
index_attr:
|
|
7063
|
-
index_type:
|
|
7064
|
-
index_required:
|
|
7065
|
-
status:
|
|
7066
|
-
mapping_confidence:
|
|
7336
|
+
var schemaComparisonEntrySchema = z32.object({
|
|
7337
|
+
canonical_attr: z32.string(),
|
|
7338
|
+
canonical_type: z32.array(z32.string()),
|
|
7339
|
+
canonical_roles: z32.array(z32.string()),
|
|
7340
|
+
canonical_required: z32.boolean(),
|
|
7341
|
+
index_attr: z32.string().nullable(),
|
|
7342
|
+
index_type: z32.array(z32.string()).nullable(),
|
|
7343
|
+
index_required: z32.boolean().nullable(),
|
|
7344
|
+
status: z32.string(),
|
|
7345
|
+
mapping_confidence: z32.number()
|
|
7067
7346
|
});
|
|
7068
|
-
var settingsRecommendationSchema =
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
current:
|
|
7072
|
-
recommended:
|
|
7347
|
+
var settingsRecommendationSchema = z32.record(
|
|
7348
|
+
z32.string(),
|
|
7349
|
+
z32.object({
|
|
7350
|
+
current: z32.any(),
|
|
7351
|
+
recommended: z32.any()
|
|
7073
7352
|
})
|
|
7074
7353
|
);
|
|
7075
|
-
var assessmentResultSchema =
|
|
7076
|
-
ready:
|
|
7077
|
-
schema_comparison:
|
|
7354
|
+
var assessmentResultSchema = z32.object({
|
|
7355
|
+
ready: z32.boolean(),
|
|
7356
|
+
schema_comparison: z32.array(schemaComparisonEntrySchema),
|
|
7078
7357
|
settings_recommendation: settingsRecommendationSchema
|
|
7079
7358
|
});
|
|
7080
|
-
var playbookExecutionSchema =
|
|
7081
|
-
uuid:
|
|
7082
|
-
playbook_slug:
|
|
7083
|
-
application_id:
|
|
7084
|
-
source_index_name:
|
|
7359
|
+
var playbookExecutionSchema = z32.object({
|
|
7360
|
+
uuid: z32.string(),
|
|
7361
|
+
playbook_slug: z32.string(),
|
|
7362
|
+
application_id: z32.string(),
|
|
7363
|
+
source_index_name: z32.string(),
|
|
7085
7364
|
current_step: playbookExecutionStepSchema,
|
|
7086
7365
|
step_status: playbookExecutionStepStatusSchema,
|
|
7087
|
-
status_reason:
|
|
7088
|
-
completed:
|
|
7366
|
+
status_reason: z32.string().nullable(),
|
|
7367
|
+
completed: z32.boolean(),
|
|
7089
7368
|
assessment_result: assessmentResultSchema.nullable(),
|
|
7090
|
-
settings_snapshot:
|
|
7091
|
-
created_at:
|
|
7369
|
+
settings_snapshot: z32.json().nullable(),
|
|
7370
|
+
created_at: z32.iso.datetime()
|
|
7092
7371
|
});
|
|
7093
7372
|
var playbookExecutionCreateResponseSchema = playbookExecutionSchema.extend({
|
|
7094
|
-
resumed:
|
|
7095
|
-
sample_data:
|
|
7373
|
+
resumed: z32.boolean(),
|
|
7374
|
+
sample_data: z32.boolean()
|
|
7096
7375
|
});
|
|
7097
7376
|
|
|
7098
7377
|
// src/lib/dashboardApi/api.ts
|
|
@@ -7142,8 +7421,8 @@ async function assessPlaybookExecution(uuid) {
|
|
|
7142
7421
|
}
|
|
7143
7422
|
|
|
7144
7423
|
// src/actions/playbookSteps/selectPlaybook.ts
|
|
7145
|
-
var selectPlaybookSchema =
|
|
7146
|
-
slug:
|
|
7424
|
+
var selectPlaybookSchema = z33.object({
|
|
7425
|
+
slug: z33.string()
|
|
7147
7426
|
});
|
|
7148
7427
|
function persistSlug(ctx, slug) {
|
|
7149
7428
|
ctx.setUserInput("playbookSlug", slug);
|
|
@@ -7175,10 +7454,10 @@ async function selectPlaybookStep(ctx, requestedSlug) {
|
|
|
7175
7454
|
}
|
|
7176
7455
|
|
|
7177
7456
|
// src/actions/playbookSteps/selectIndex.ts
|
|
7178
|
-
import { z as
|
|
7179
|
-
var selectIndexSchema =
|
|
7180
|
-
indexName:
|
|
7181
|
-
sampleIndex:
|
|
7457
|
+
import { z as z34 } from "zod";
|
|
7458
|
+
var selectIndexSchema = z34.object({
|
|
7459
|
+
indexName: z34.string(),
|
|
7460
|
+
sampleIndex: z34.boolean()
|
|
7182
7461
|
});
|
|
7183
7462
|
var CREATE_SAMPLE_INDEX = "Create a sample index";
|
|
7184
7463
|
var GO_BACK_OPTION = "Go back to playbook selection";
|
|
@@ -8736,7 +9015,7 @@ function App() {
|
|
|
8736
9015
|
onComplete: () => setTipsComplete(true)
|
|
8737
9016
|
}
|
|
8738
9017
|
),
|
|
8739
|
-
showNoticesInMain && showNotices && /* @__PURE__ */ jsx21(Notices, {
|
|
9018
|
+
showNoticesInMain && showNotices && /* @__PURE__ */ jsx21(Notices, { border: false }),
|
|
8740
9019
|
/* @__PURE__ */ jsx21(Review, {}),
|
|
8741
9020
|
!showTips && /* @__PURE__ */ jsx21(PromptInput, {}),
|
|
8742
9021
|
phase === "error" && error && /* @__PURE__ */ jsx21(Box22, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text23, { color: COLORS.status.error, children: [
|
|
@@ -8967,7 +9246,7 @@ async function seedWorkflowState(workflow, value) {
|
|
|
8967
9246
|
}
|
|
8968
9247
|
|
|
8969
9248
|
// src/lib/resetState.ts
|
|
8970
|
-
import { readdir as
|
|
9249
|
+
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
8971
9250
|
import { join as join12 } from "node:path";
|
|
8972
9251
|
var KEEP = ["wizard.log"];
|
|
8973
9252
|
async function resetProjectState() {
|
|
@@ -8975,7 +9254,7 @@ async function resetProjectState() {
|
|
|
8975
9254
|
await forgetResolvedKeys();
|
|
8976
9255
|
let entries;
|
|
8977
9256
|
try {
|
|
8978
|
-
entries = await
|
|
9257
|
+
entries = await readdir4(dir);
|
|
8979
9258
|
} catch {
|
|
8980
9259
|
return { dir, removed: [] };
|
|
8981
9260
|
}
|
|
@@ -8991,7 +9270,7 @@ async function resetProjectState() {
|
|
|
8991
9270
|
// package.json with { type: 'json' }
|
|
8992
9271
|
var package_default2 = {
|
|
8993
9272
|
name: "@algolia/wizard",
|
|
8994
|
-
version: "0.
|
|
9273
|
+
version: "0.84.0-rc.202.644",
|
|
8995
9274
|
description: "Magically implement Algolia functionality in your codebase",
|
|
8996
9275
|
type: "module",
|
|
8997
9276
|
engines: {
|