@algolia/wizard 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +278 -104
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -239,6 +239,9 @@ function describeInputValue(value) {
|
|
|
239
239
|
}
|
|
240
240
|
var NOTICE_INTERVAL_MS = 2e3;
|
|
241
241
|
var CLI_OUTPUT_LIMIT = 200;
|
|
242
|
+
function commandApprovalKey(command, cwd) {
|
|
243
|
+
return `${cwd}\0${command}`;
|
|
244
|
+
}
|
|
242
245
|
var useWizard = create((set, get) => ({
|
|
243
246
|
phase: "idle",
|
|
244
247
|
homeScreen: "home",
|
|
@@ -356,10 +359,17 @@ var useWizard = create((set, get) => ({
|
|
|
356
359
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
357
360
|
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
358
361
|
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
359
|
-
isCommandApproved: (command, cwd) => get().approvedCommands.has(
|
|
362
|
+
isCommandApproved: (command, cwd) => get().approvedCommands.has(commandApprovalKey(command, cwd)),
|
|
360
363
|
approveCommand: (command, cwd) => set((s) => ({
|
|
361
|
-
approvedCommands: new Set(s.approvedCommands).add(
|
|
364
|
+
approvedCommands: new Set(s.approvedCommands).add(
|
|
365
|
+
commandApprovalKey(command, cwd)
|
|
366
|
+
)
|
|
362
367
|
})),
|
|
368
|
+
revokeCommand: (command, cwd) => set((s) => {
|
|
369
|
+
const approvedCommands = new Set(s.approvedCommands);
|
|
370
|
+
approvedCommands.delete(commandApprovalKey(command, cwd));
|
|
371
|
+
return { approvedCommands };
|
|
372
|
+
}),
|
|
363
373
|
logStart: (kind, name, input) => {
|
|
364
374
|
const id = nanoid();
|
|
365
375
|
set((s) => ({
|
|
@@ -738,9 +748,11 @@ function DecisionSelect({
|
|
|
738
748
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
739
749
|
function CommandApproval({
|
|
740
750
|
command,
|
|
751
|
+
messages,
|
|
741
752
|
onDecide
|
|
742
753
|
}) {
|
|
743
754
|
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, children: [
|
|
755
|
+
messages?.map((message, index) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: message }, `msg-${index}`)),
|
|
744
756
|
/* @__PURE__ */ jsx5(Text6, { color: COLORS.primary, bold: true, children: "Run this command?" }),
|
|
745
757
|
/* @__PURE__ */ jsxs5(
|
|
746
758
|
Box6,
|
|
@@ -1157,7 +1169,6 @@ function PromptInput() {
|
|
|
1157
1169
|
return /* @__PURE__ */ jsx8(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx8(
|
|
1158
1170
|
SelectPrompt,
|
|
1159
1171
|
{
|
|
1160
|
-
question: inputReq.prompt,
|
|
1161
1172
|
messages: inputReq.messages,
|
|
1162
1173
|
options: ["Continue"],
|
|
1163
1174
|
onSelect: handleSubmit
|
|
@@ -1176,7 +1187,14 @@ function PromptInput() {
|
|
|
1176
1187
|
);
|
|
1177
1188
|
}
|
|
1178
1189
|
if (inputReq.promptType === "commandApproval" && inputReq.command) {
|
|
1179
|
-
return /* @__PURE__ */ jsx8(
|
|
1190
|
+
return /* @__PURE__ */ jsx8(
|
|
1191
|
+
CommandApproval,
|
|
1192
|
+
{
|
|
1193
|
+
command: inputReq.command,
|
|
1194
|
+
messages: inputReq.messages,
|
|
1195
|
+
onDecide: submitInput
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1180
1198
|
}
|
|
1181
1199
|
if (inputReq.promptType === "acceptReject") {
|
|
1182
1200
|
const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
|
|
@@ -2135,7 +2153,25 @@ function isSecretEnvFile(filePath) {
|
|
|
2135
2153
|
return !/\.(example|sample|template)$/.test(name);
|
|
2136
2154
|
}
|
|
2137
2155
|
|
|
2156
|
+
// src/lib/tools/utils/truncateText.ts
|
|
2157
|
+
var HEAD_CHARS = 4e3;
|
|
2158
|
+
var TAIL_CHARS = 8e3;
|
|
2159
|
+
function truncateText(text) {
|
|
2160
|
+
if (text.length <= HEAD_CHARS + TAIL_CHARS) return text;
|
|
2161
|
+
const omitted = text.length - HEAD_CHARS - TAIL_CHARS;
|
|
2162
|
+
return [
|
|
2163
|
+
text.slice(0, HEAD_CHARS),
|
|
2164
|
+
`
|
|
2165
|
+
\u2026 [${omitted} characters omitted] \u2026
|
|
2166
|
+
`,
|
|
2167
|
+
text.slice(-TAIL_CHARS)
|
|
2168
|
+
].join("");
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2138
2171
|
// src/lib/tools/readFile.ts
|
|
2172
|
+
var lineRangeSchema = z9.tuple([z9.number().int().nonnegative(), z9.number().int().nonnegative()]).refine(([start, end]) => start < end, {
|
|
2173
|
+
message: "The end line must be greater than the start line"
|
|
2174
|
+
});
|
|
2139
2175
|
function redactEnvValues(content) {
|
|
2140
2176
|
return content.split("\n").map((line) => {
|
|
2141
2177
|
const match = line.match(/^(\s*(?:export\s+)?[\w.-]+\s*=)(.*)$/);
|
|
@@ -2145,22 +2181,55 @@ function redactEnvValues(content) {
|
|
|
2145
2181
|
return `${match[1]}[REDACTED]`;
|
|
2146
2182
|
}).join("\n");
|
|
2147
2183
|
}
|
|
2184
|
+
function splitRange([start, end]) {
|
|
2185
|
+
if (end - start < 2) return null;
|
|
2186
|
+
const middle = start + Math.ceil((end - start) / 2);
|
|
2187
|
+
return [
|
|
2188
|
+
[start, middle],
|
|
2189
|
+
[middle, end]
|
|
2190
|
+
];
|
|
2191
|
+
}
|
|
2192
|
+
function sampleFile(content) {
|
|
2193
|
+
const sampled = truncateText(content);
|
|
2194
|
+
if (sampled === content) return content;
|
|
2195
|
+
const totalLines = content.split("\n").length;
|
|
2196
|
+
const ranges = splitRange([0, totalLines]);
|
|
2197
|
+
const next = ranges ? ` If needed, use readLines: [${ranges[0].join(", ")}] and readLines: [${ranges[1].join(", ")}].` : "";
|
|
2198
|
+
return `[Sampled ${totalLines}-line file.${next}]
|
|
2199
|
+
${sampled}`;
|
|
2200
|
+
}
|
|
2201
|
+
function readLineRange(content, requested) {
|
|
2202
|
+
const lines = content.split("\n");
|
|
2203
|
+
const range = [
|
|
2204
|
+
Math.min(requested[0], lines.length),
|
|
2205
|
+
Math.min(requested[1], lines.length)
|
|
2206
|
+
];
|
|
2207
|
+
const selected = lines.slice(...range).join("\n");
|
|
2208
|
+
const sampled = truncateText(selected);
|
|
2209
|
+
if (sampled === selected) return selected;
|
|
2210
|
+
const ranges = splitRange(range);
|
|
2211
|
+
const next = ranges ? ` If needed, split into readLines: [${ranges[0].join(", ")}] and readLines: [${ranges[1].join(", ")}].` : " This line is too large to return completely with readLines.";
|
|
2212
|
+
return `[readLines: [${range.join(", ")}] sampled; ${lines.length} total lines.${next}]
|
|
2213
|
+
${sampled}`;
|
|
2214
|
+
}
|
|
2148
2215
|
function readFileTool(ctx) {
|
|
2149
2216
|
return tool4({
|
|
2150
|
-
description: "Read the
|
|
2217
|
+
description: "Read a file at the given path. Large results return a beginning/end sample. Use readLines for omitted sections needed by the task, and read every range when full-file understanding is required.",
|
|
2151
2218
|
inputSchema: z9.object({
|
|
2152
|
-
filePath: z9.string().describe("Path to the file to read")
|
|
2219
|
+
filePath: z9.string().describe("Path to the file to read"),
|
|
2220
|
+
readLines: lineRangeSchema.optional().describe("Optional zero-based [start, end) line range to read")
|
|
2153
2221
|
}),
|
|
2154
|
-
execute: async ({ filePath }) => {
|
|
2222
|
+
execute: async ({ filePath, readLines }) => {
|
|
2155
2223
|
if (++ctx.counts.read > ctx.limits.read) {
|
|
2156
2224
|
return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
|
|
2157
2225
|
}
|
|
2158
|
-
logger.info({ filePath }, "called readFile tool");
|
|
2226
|
+
logger.info({ filePath, readLines }, "called readFile tool");
|
|
2159
2227
|
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2160
|
-
if (
|
|
2228
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2161
2229
|
try {
|
|
2162
2230
|
const content = await readFile3(resolved2.target, "utf8");
|
|
2163
|
-
|
|
2231
|
+
const safeContent = isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
|
|
2232
|
+
return readLines ? readLineRange(safeContent, [readLines[0], readLines[1]]) : sampleFile(safeContent);
|
|
2164
2233
|
} catch (err) {
|
|
2165
2234
|
return `Error reading ${filePath}: ${err.message}`;
|
|
2166
2235
|
}
|
|
@@ -2698,9 +2767,16 @@ function searchFilesTool(ctx) {
|
|
|
2698
2767
|
}
|
|
2699
2768
|
|
|
2700
2769
|
// src/lib/tools/runShell.ts
|
|
2701
|
-
import { tool as
|
|
2770
|
+
import { tool as tool9, generateText, Output } from "ai";
|
|
2702
2771
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2772
|
+
import z16 from "zod";
|
|
2773
|
+
import { relative as relative5 } from "node:path";
|
|
2774
|
+
|
|
2775
|
+
// src/lib/tools/reviewScript.ts
|
|
2776
|
+
import { tool as tool8 } from "ai";
|
|
2703
2777
|
import z15 from "zod";
|
|
2778
|
+
import { createHash } from "node:crypto";
|
|
2779
|
+
import { readFile as readFile7, stat as stat2 } from "node:fs/promises";
|
|
2704
2780
|
import { relative as relative4 } from "node:path";
|
|
2705
2781
|
|
|
2706
2782
|
// src/lib/tools/utils/prompt.ts
|
|
@@ -2712,6 +2788,116 @@ function serializePrompt(work) {
|
|
|
2712
2788
|
return result;
|
|
2713
2789
|
}
|
|
2714
2790
|
|
|
2791
|
+
// src/lib/tools/reviewScript.ts
|
|
2792
|
+
var reviewedCommandApprovals = /* @__PURE__ */ new WeakMap();
|
|
2793
|
+
async function fileDigest(filePath) {
|
|
2794
|
+
const content = await readFile7(filePath);
|
|
2795
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2796
|
+
}
|
|
2797
|
+
function grantReviewedCommandApproval(ctx, approval) {
|
|
2798
|
+
reviewedCommandApprovals.set(ctx, approval);
|
|
2799
|
+
}
|
|
2800
|
+
function getReviewedCommandApproval(ctx, command, cwd) {
|
|
2801
|
+
const approval = reviewedCommandApprovals.get(ctx);
|
|
2802
|
+
if (approval?.command !== command || approval.cwd !== cwd) return void 0;
|
|
2803
|
+
return approval;
|
|
2804
|
+
}
|
|
2805
|
+
function clearReviewedCommandApproval(ctx) {
|
|
2806
|
+
reviewedCommandApprovals.delete(ctx);
|
|
2807
|
+
}
|
|
2808
|
+
function revokeCommandApprovals(ctx, command, cwd) {
|
|
2809
|
+
clearReviewedCommandApproval(ctx);
|
|
2810
|
+
useWizard.getState().revokeCommand(command, cwd);
|
|
2811
|
+
}
|
|
2812
|
+
async function reviewedCommandApprovalIsCurrent(approval) {
|
|
2813
|
+
const currentDigest = await fileDigest(approval.filePath).catch(() => null);
|
|
2814
|
+
return currentDigest === approval.digest;
|
|
2815
|
+
}
|
|
2816
|
+
function reviewScriptTool(ctx) {
|
|
2817
|
+
return tool8({
|
|
2818
|
+
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.",
|
|
2819
|
+
inputSchema: z15.object({
|
|
2820
|
+
filePath: z15.string().describe(
|
|
2821
|
+
"Path to the script to review, relative to the project root."
|
|
2822
|
+
),
|
|
2823
|
+
command: z15.string().min(1).describe("The exact command that runShell will use to run the script."),
|
|
2824
|
+
cwd: z15.string().describe(
|
|
2825
|
+
"The exact working directory that runShell will use, relative to the current tool working directory."
|
|
2826
|
+
),
|
|
2827
|
+
entityName: z15.string().trim().min(1).max(100).regex(/^[^\r\n]+$/).describe("The exact entity name selected by the user."),
|
|
2828
|
+
usesGeneratedRecords: z15.boolean().describe(
|
|
2829
|
+
"Whether the script reads generated sample records instead of the selected real data source."
|
|
2830
|
+
)
|
|
2831
|
+
}),
|
|
2832
|
+
execute: async ({
|
|
2833
|
+
filePath,
|
|
2834
|
+
command,
|
|
2835
|
+
cwd,
|
|
2836
|
+
entityName,
|
|
2837
|
+
usesGeneratedRecords
|
|
2838
|
+
}) => {
|
|
2839
|
+
logger.info(
|
|
2840
|
+
{ filePath, command, cwd, entityName, usesGeneratedRecords },
|
|
2841
|
+
"called reviewScript tool"
|
|
2842
|
+
);
|
|
2843
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2844
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2845
|
+
const resolvedCwd = resolveInRoot(ctx, cwd);
|
|
2846
|
+
if (resolvedCwd.ok === false) return resolvedCwd.error;
|
|
2847
|
+
const displayCwd = relative4(ctx.root, resolvedCwd.target) || ".";
|
|
2848
|
+
const displayFilePath = relative4(ctx.root, resolved2.target);
|
|
2849
|
+
const ingestionSummary = usesGeneratedRecords ? `This will push ${entityName} records from generated sample data to Algolia. You can replace the sample dataset with your real data source in ${displayFilePath}.` : `This will push ${entityName} records from your selected source to Algolia.`;
|
|
2850
|
+
return serializePrompt(async () => {
|
|
2851
|
+
revokeCommandApprovals(ctx, command, resolvedCwd.target);
|
|
2852
|
+
const isFile = await stat2(resolved2.target).then(
|
|
2853
|
+
(s) => s.isFile(),
|
|
2854
|
+
() => false
|
|
2855
|
+
);
|
|
2856
|
+
if (!isFile) {
|
|
2857
|
+
return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
|
|
2858
|
+
}
|
|
2859
|
+
const reviewedDigest = await fileDigest(resolved2.target).catch(
|
|
2860
|
+
() => null
|
|
2861
|
+
);
|
|
2862
|
+
if (reviewedDigest === null) {
|
|
2863
|
+
return `Refused: ${filePath} could not be read. Write the script first, then review it again.`;
|
|
2864
|
+
}
|
|
2865
|
+
const answer = await useWizard.getState().requestUserInput({
|
|
2866
|
+
prompt: "Run this command?",
|
|
2867
|
+
promptType: "commandApproval",
|
|
2868
|
+
options: [],
|
|
2869
|
+
messages: [
|
|
2870
|
+
`We have created your ingestion script. Please review it at ${displayFilePath} before approving.`
|
|
2871
|
+
],
|
|
2872
|
+
command: {
|
|
2873
|
+
command,
|
|
2874
|
+
cwd: displayCwd,
|
|
2875
|
+
explanation: ingestionSummary
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
if (answer !== "approve") {
|
|
2879
|
+
revokeCommandApprovals(ctx, command, resolvedCwd.target);
|
|
2880
|
+
return "The user declined to run this ingestion script. Do not run it.";
|
|
2881
|
+
}
|
|
2882
|
+
const approvedDigest = await fileDigest(resolved2.target).catch(
|
|
2883
|
+
() => null
|
|
2884
|
+
);
|
|
2885
|
+
if (approvedDigest !== reviewedDigest) {
|
|
2886
|
+
return "The ingestion script changed while it was being reviewed. Call reviewScript again before running it.";
|
|
2887
|
+
}
|
|
2888
|
+
ctx.reviewed.push(resolved2.target);
|
|
2889
|
+
grantReviewedCommandApproval(ctx, {
|
|
2890
|
+
command,
|
|
2891
|
+
cwd: resolvedCwd.target,
|
|
2892
|
+
filePath: resolved2.target,
|
|
2893
|
+
digest: reviewedDigest
|
|
2894
|
+
});
|
|
2895
|
+
return "The user reviewed the script and approved this exact command. Run it now with the same cwd.";
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2715
2901
|
// src/lib/tools/utils/runShell.ts
|
|
2716
2902
|
import { spawn as spawn2 } from "node:child_process";
|
|
2717
2903
|
|
|
@@ -2749,19 +2935,6 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), sh
|
|
|
2749
2935
|
|
|
2750
2936
|
// src/lib/tools/utils/runShell.ts
|
|
2751
2937
|
var SIGKILL_DELAY_MS = 5e3;
|
|
2752
|
-
var HEAD_CHARS = 4e3;
|
|
2753
|
-
var TAIL_CHARS = 8e3;
|
|
2754
|
-
function truncateOutput(output) {
|
|
2755
|
-
if (output.length <= HEAD_CHARS + TAIL_CHARS) return output;
|
|
2756
|
-
const omitted = output.length - HEAD_CHARS - TAIL_CHARS;
|
|
2757
|
-
return [
|
|
2758
|
-
output.slice(0, HEAD_CHARS),
|
|
2759
|
-
`
|
|
2760
|
-
\u2026 [${omitted} characters omitted] \u2026
|
|
2761
|
-
`,
|
|
2762
|
-
output.slice(-TAIL_CHARS)
|
|
2763
|
-
].join("");
|
|
2764
|
-
}
|
|
2765
2938
|
function runShell(command, opts) {
|
|
2766
2939
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2767
2940
|
const startedAt = Date.now();
|
|
@@ -2782,7 +2955,7 @@ function runShell(command, opts) {
|
|
|
2782
2955
|
clearTimeout(killTimer);
|
|
2783
2956
|
resolve4({
|
|
2784
2957
|
exitCode,
|
|
2785
|
-
output:
|
|
2958
|
+
output: truncateText(output.trim()),
|
|
2786
2959
|
timedOut,
|
|
2787
2960
|
durationMs: Date.now() - startedAt
|
|
2788
2961
|
});
|
|
@@ -2810,7 +2983,7 @@ function storeApproval(root) {
|
|
|
2810
2983
|
return async (req) => {
|
|
2811
2984
|
const store = useWizard.getState();
|
|
2812
2985
|
if (store.isCommandApproved(req.command, req.cwd)) return "approve";
|
|
2813
|
-
const rel =
|
|
2986
|
+
const rel = relative5(root, req.cwd);
|
|
2814
2987
|
const answer = await store.requestUserInput({
|
|
2815
2988
|
prompt: "Run this command?",
|
|
2816
2989
|
promptType: "commandApproval",
|
|
@@ -2825,6 +2998,7 @@ function storeApproval(root) {
|
|
|
2825
2998
|
return "approve";
|
|
2826
2999
|
};
|
|
2827
3000
|
}
|
|
3001
|
+
var SUCCESS_EXIT_CODE = 0;
|
|
2828
3002
|
var FILE_IO_COMMANDS = /* @__PURE__ */ new Set([
|
|
2829
3003
|
"ls",
|
|
2830
3004
|
"find",
|
|
@@ -2963,9 +3137,9 @@ function fastPathSafety(command) {
|
|
|
2963
3137
|
return void 0;
|
|
2964
3138
|
}
|
|
2965
3139
|
var CLASSIFIER_MODEL = "claude-haiku-4-5";
|
|
2966
|
-
var commandSafetySchema =
|
|
2967
|
-
safe:
|
|
2968
|
-
reason:
|
|
3140
|
+
var commandSafetySchema = z16.object({
|
|
3141
|
+
safe: z16.boolean(),
|
|
3142
|
+
reason: z16.string().describe("One short sentence explaining the verdict.")
|
|
2969
3143
|
});
|
|
2970
3144
|
function defaultCreateModel() {
|
|
2971
3145
|
const token = getAuthToken();
|
|
@@ -3025,12 +3199,15 @@ async function classifyCommandSafety(createModel, command, cwd, explanation, app
|
|
|
3025
3199
|
return false;
|
|
3026
3200
|
}
|
|
3027
3201
|
}
|
|
3028
|
-
async function
|
|
3029
|
-
|
|
3030
|
-
const env = await ctx.shell.env().catch((err) => {
|
|
3202
|
+
async function resolveShellEnv(ctx, command) {
|
|
3203
|
+
return ctx.shell.env().catch((err) => {
|
|
3031
3204
|
logger.warn({ err, command }, "runShell: could not resolve command env");
|
|
3032
3205
|
return {};
|
|
3033
3206
|
});
|
|
3207
|
+
}
|
|
3208
|
+
async function runAndRecord(ctx, command, cwd, resolvedEnv) {
|
|
3209
|
+
useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
|
|
3210
|
+
const env = resolvedEnv ?? await resolveShellEnv(ctx, command);
|
|
3034
3211
|
const run2 = await (ctx.shell.run ?? runShell)(command, {
|
|
3035
3212
|
cwd,
|
|
3036
3213
|
env,
|
|
@@ -3061,6 +3238,19 @@ async function runAndRecord(ctx, command, cwd) {
|
|
|
3061
3238
|
output: run2.output
|
|
3062
3239
|
};
|
|
3063
3240
|
}
|
|
3241
|
+
async function runReviewedCommand(ctx, command, cwd) {
|
|
3242
|
+
const approval = getReviewedCommandApproval(ctx, command, cwd);
|
|
3243
|
+
if (!approval) return void 0;
|
|
3244
|
+
const env = await resolveShellEnv(ctx, command);
|
|
3245
|
+
if (!await reviewedCommandApprovalIsCurrent(approval)) {
|
|
3246
|
+
return `Refused: ${relative5(ctx.root, approval.filePath)} changed after review. Call reviewScript again before running it.`;
|
|
3247
|
+
}
|
|
3248
|
+
const result = await runAndRecord(ctx, command, cwd, env);
|
|
3249
|
+
if (result.exitCode === SUCCESS_EXIT_CODE && !result.timedOut) {
|
|
3250
|
+
clearReviewedCommandApproval(ctx);
|
|
3251
|
+
}
|
|
3252
|
+
return result;
|
|
3253
|
+
}
|
|
3064
3254
|
async function approveAndRun(ctx, command, cwd, explanation) {
|
|
3065
3255
|
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
3066
3256
|
if (decision === "reject") {
|
|
@@ -3071,16 +3261,16 @@ async function approveAndRun(ctx, command, cwd, explanation) {
|
|
|
3071
3261
|
return runAndRecord(ctx, command, cwd);
|
|
3072
3262
|
}
|
|
3073
3263
|
function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
3074
|
-
return
|
|
3264
|
+
return tool9({
|
|
3075
3265
|
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.",
|
|
3076
|
-
inputSchema:
|
|
3077
|
-
command:
|
|
3266
|
+
inputSchema: z16.object({
|
|
3267
|
+
command: z16.string().describe(
|
|
3078
3268
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
3079
3269
|
),
|
|
3080
|
-
cwd:
|
|
3270
|
+
cwd: z16.string().optional().describe(
|
|
3081
3271
|
"Directory to run in, relative to the project root. Defaults to the project root."
|
|
3082
3272
|
),
|
|
3083
|
-
explanation:
|
|
3273
|
+
explanation: z16.string().describe(
|
|
3084
3274
|
"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."
|
|
3085
3275
|
)
|
|
3086
3276
|
}),
|
|
@@ -3089,16 +3279,22 @@ function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
|
3089
3279
|
return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
|
|
3090
3280
|
}
|
|
3091
3281
|
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
3092
|
-
if (
|
|
3282
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
3093
3283
|
if (isFileIOCommand(command)) {
|
|
3094
3284
|
return "Refused: use listFiles or searchFiles to find files, and readFile to read one, instead of a shell file-listing, reading, or search command (ls/find/fd/tree/dir/locate/cat/head/tail/less/more/tac/nl/grep/egrep/fgrep/rg/ack/ag/xxd/hexdump/od/strings).";
|
|
3095
3285
|
}
|
|
3096
3286
|
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
3287
|
+
const reviewed = await serializePrompt(
|
|
3288
|
+
() => runReviewedCommand(ctx, command, resolved2.target)
|
|
3289
|
+
);
|
|
3290
|
+
if (reviewed !== void 0) return reviewed;
|
|
3097
3291
|
const fast = fastPathSafety(command);
|
|
3098
3292
|
let isSafe = fast;
|
|
3293
|
+
let usedStoredApproval = false;
|
|
3099
3294
|
if (isSafe === void 0) {
|
|
3100
3295
|
const store = useWizard.getState();
|
|
3101
|
-
|
|
3296
|
+
usedStoredApproval = store.isCommandApproved(command, resolved2.target);
|
|
3297
|
+
isSafe = usedStoredApproval;
|
|
3102
3298
|
if (!isSafe) {
|
|
3103
3299
|
isSafe = await classifyCommandSafety(
|
|
3104
3300
|
createModel,
|
|
@@ -3109,54 +3305,18 @@ function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
|
3109
3305
|
);
|
|
3110
3306
|
}
|
|
3111
3307
|
}
|
|
3112
|
-
if (isSafe) {
|
|
3113
|
-
return serializePrompt(
|
|
3114
|
-
() => runAndRecord(ctx, command, resolved2.target)
|
|
3115
|
-
);
|
|
3116
|
-
}
|
|
3117
|
-
return serializePrompt(
|
|
3118
|
-
() => approveAndRun(ctx, command, resolved2.target, explanation)
|
|
3119
|
-
);
|
|
3120
|
-
}
|
|
3121
|
-
});
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
// src/lib/tools/reviewScript.ts
|
|
3125
|
-
import { tool as tool9 } from "ai";
|
|
3126
|
-
import z16 from "zod";
|
|
3127
|
-
import { stat as stat2 } from "node:fs/promises";
|
|
3128
|
-
import { relative as relative5 } from "node:path";
|
|
3129
|
-
function reviewScriptTool(ctx) {
|
|
3130
|
-
return tool9({
|
|
3131
|
-
description: "Call this as soon as the ingestion script is finished and before you run it, so the user can review it.It shows the file path and returns once they have confirmed reviewing it.",
|
|
3132
|
-
inputSchema: z16.object({
|
|
3133
|
-
filePath: z16.string().describe(
|
|
3134
|
-
"Path to the script to review, relative to the project root."
|
|
3135
|
-
)
|
|
3136
|
-
}),
|
|
3137
|
-
execute: async ({ filePath }) => {
|
|
3138
|
-
logger.info({ filePath }, "called reviewScript tool");
|
|
3139
|
-
const resolved2 = resolveInRoot(ctx, filePath);
|
|
3140
|
-
if (resolved2.ok === false) return resolved2.error;
|
|
3141
|
-
const isFile = await stat2(resolved2.target).then(
|
|
3142
|
-
(s) => s.isFile(),
|
|
3143
|
-
() => false
|
|
3144
|
-
);
|
|
3145
|
-
if (!isFile) {
|
|
3146
|
-
return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
|
|
3147
|
-
}
|
|
3148
3308
|
return serializePrompt(async () => {
|
|
3149
|
-
await
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
return
|
|
3309
|
+
const lateReview = await runReviewedCommand(
|
|
3310
|
+
ctx,
|
|
3311
|
+
command,
|
|
3312
|
+
resolved2.target
|
|
3313
|
+
);
|
|
3314
|
+
if (lateReview !== void 0) return lateReview;
|
|
3315
|
+
const storedApprovalIsCurrent = !usedStoredApproval || useWizard.getState().isCommandApproved(command, resolved2.target);
|
|
3316
|
+
if (isSafe && storedApprovalIsCurrent) {
|
|
3317
|
+
return runAndRecord(ctx, command, resolved2.target);
|
|
3318
|
+
}
|
|
3319
|
+
return approveAndRun(ctx, command, resolved2.target, explanation);
|
|
3160
3320
|
});
|
|
3161
3321
|
}
|
|
3162
3322
|
});
|
|
@@ -3651,7 +3811,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3651
3811
|
// package.json
|
|
3652
3812
|
var package_default = {
|
|
3653
3813
|
name: "@algolia/wizard",
|
|
3654
|
-
version: "0.
|
|
3814
|
+
version: "0.54.0",
|
|
3655
3815
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3656
3816
|
type: "module",
|
|
3657
3817
|
engines: {
|
|
@@ -4353,8 +4513,8 @@ function ingestionInstructions(input) {
|
|
|
4353
4513
|
...algoliaClientDoc(input),
|
|
4354
4514
|
"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.",
|
|
4355
4515
|
`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.`,
|
|
4356
|
-
|
|
4357
|
-
'Then run the script yourself via runShell
|
|
4516
|
+
`When the script is finished, decide the exact command and cwd you will use to run it, then call reviewScript with that filePath, command, cwd, the exact selected entityName, and usesGeneratedRecords set to ${input.ingestionSource === "generated"}. Wait for the developer to review it because running it writes records to a live index. Do not run it before that call returns.`,
|
|
4517
|
+
'Then, without changing directories, run the script yourself via runShell using the exact same command and cwd. Report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
|
|
4358
4518
|
"The summary should be extremely concise.",
|
|
4359
4519
|
...sourceSpecificInstructions(input)
|
|
4360
4520
|
] : []
|
|
@@ -4734,19 +4894,33 @@ ${detail}` : ""}`
|
|
|
4734
4894
|
product_area: "AI Wizard"
|
|
4735
4895
|
});
|
|
4736
4896
|
}
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
}
|
|
4897
|
+
if (ingestScriptRan) {
|
|
4898
|
+
await ctx.requestUserInput({
|
|
4899
|
+
prompt: "Continue",
|
|
4900
|
+
promptType: "notice",
|
|
4901
|
+
options: [],
|
|
4902
|
+
messages: [
|
|
4903
|
+
ingestOutcomeMessage,
|
|
4904
|
+
"View your ingested records in the Algolia dashboard:",
|
|
4905
|
+
"https://dashboard.algolia.com/explorer/browse",
|
|
4906
|
+
"When you're ready to continue, we'll move on to search implementation."
|
|
4907
|
+
]
|
|
4908
|
+
});
|
|
4909
|
+
} else {
|
|
4910
|
+
const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
|
|
4911
|
+
await ctx.requestUserInput({
|
|
4912
|
+
prompt: "",
|
|
4913
|
+
promptType: "enterToContinue",
|
|
4914
|
+
options: [],
|
|
4915
|
+
// One message per line: Notices.tsx counts a message as one wrapped
|
|
4916
|
+
// line, so an embedded newline overflows the panel's height accounting.
|
|
4917
|
+
messages: [
|
|
4918
|
+
ingestOutcomeMessage,
|
|
4919
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4920
|
+
...commandMessages
|
|
4921
|
+
]
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4750
4924
|
}
|
|
4751
4925
|
let searchConfigFile;
|
|
4752
4926
|
if (useCases.includes("search")) {
|
|
@@ -6104,7 +6278,7 @@ function App() {
|
|
|
6104
6278
|
}
|
|
6105
6279
|
|
|
6106
6280
|
// src/lib/envAppId.ts
|
|
6107
|
-
import { readFile as
|
|
6281
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
6108
6282
|
import { join as join12 } from "node:path";
|
|
6109
6283
|
var ENV_FILES = [".env", ".env.local"];
|
|
6110
6284
|
var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
|
|
@@ -6112,7 +6286,7 @@ async function findEnvApplicationId(root = process.cwd()) {
|
|
|
6112
6286
|
for (const file of ENV_FILES) {
|
|
6113
6287
|
let content;
|
|
6114
6288
|
try {
|
|
6115
|
-
content = await
|
|
6289
|
+
content = await readFile8(join12(root, file), "utf8");
|
|
6116
6290
|
} catch (err) {
|
|
6117
6291
|
if (err.code !== "ENOENT") {
|
|
6118
6292
|
logger.warn(
|