@algolia/wizard 0.16.0 → 0.18.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 CHANGED
@@ -2246,7 +2246,7 @@ async function ensureApplication() {
2246
2246
  }
2247
2247
 
2248
2248
  // src/workflows/default.ts
2249
- import { z as z28 } from "zod";
2249
+ import { z as z29 } from "zod";
2250
2250
 
2251
2251
  // src/actions/listIndices.ts
2252
2252
  import { z as z5 } from "zod";
@@ -2980,6 +2980,15 @@ import { tool as tool8 } from "ai";
2980
2980
  import z15 from "zod";
2981
2981
  import { relative as relative4 } from "node:path";
2982
2982
 
2983
+ // src/lib/tools/utils/prompt.ts
2984
+ var chain = Promise.resolve();
2985
+ function serializePrompt(work) {
2986
+ const result = chain.then(work);
2987
+ chain = result.catch(() => {
2988
+ });
2989
+ return result;
2990
+ }
2991
+
2983
2992
  // src/lib/tools/utils/runShell.ts
2984
2993
  import { spawn as spawn2 } from "node:child_process";
2985
2994
 
@@ -3010,7 +3019,8 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), sh
3010
3019
  cwd,
3011
3020
  limits: { ...limits },
3012
3021
  counts: { list: 0, search: 0, read: 0, shell: 0 },
3013
- shell: shell2
3022
+ shell: shell2,
3023
+ reviewed: []
3014
3024
  };
3015
3025
  }
3016
3026
 
@@ -3088,13 +3098,6 @@ function storeApproval(root) {
3088
3098
  return answer === "approve" ? "approve" : "reject";
3089
3099
  };
3090
3100
  }
3091
- var shellChain = Promise.resolve();
3092
- function serialize(work) {
3093
- const result = shellChain.then(work);
3094
- shellChain = result.catch(() => {
3095
- });
3096
- return result;
3097
- }
3098
3101
  async function approveAndRun(ctx, command, cwd, explanation) {
3099
3102
  const decision = await ctx.shell.approve({ command, cwd, explanation });
3100
3103
  if (decision === "reject") {
@@ -3158,20 +3161,81 @@ function runShellTool(ctx) {
3158
3161
  const resolved2 = resolveInRoot(ctx, cwd ?? ".");
3159
3162
  if (!resolved2.ok) return resolved2.error;
3160
3163
  logger.info({ command, cwd: resolved2.target }, "called runShell tool");
3161
- return serialize(
3164
+ return serializePrompt(
3162
3165
  () => approveAndRun(ctx, command, resolved2.target, explanation)
3163
3166
  );
3164
3167
  }
3165
3168
  });
3166
3169
  }
3167
3170
 
3171
+ // src/lib/tools/reviewScript.ts
3172
+ import { tool as tool9 } from "ai";
3173
+ import z16 from "zod";
3174
+ import { stat as stat2 } from "node:fs/promises";
3175
+ import { relative as relative5 } from "node:path";
3176
+
3177
+ // src/lib/editor.ts
3178
+ import { spawn as spawn3 } from "node:child_process";
3179
+ function openInEditor(filePath) {
3180
+ const { command, args } = process.platform === "darwin" ? (
3181
+ // -t forces the default *text* editor; plain `open` on an extension
3182
+ // with no handler (.mjs, .py) pops a "choose an application" dialog.
3183
+ { command: "open", args: ["-t", filePath] }
3184
+ ) : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", filePath] } : { command: "xdg-open", args: [filePath] };
3185
+ try {
3186
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
3187
+ child.on("error", () => {
3188
+ });
3189
+ child.unref();
3190
+ } catch {
3191
+ }
3192
+ }
3193
+
3194
+ // src/lib/tools/reviewScript.ts
3195
+ function reviewScriptTool(ctx) {
3196
+ return tool9({
3197
+ description: "Show a script you have written to the user and wait while they read it. Call this as soon as the script is finished and before you run it for the first time, so nothing with a side effect happens behind their back. It opens the file in their editor and returns once they have confirmed.",
3198
+ inputSchema: z16.object({
3199
+ filePath: z16.string().describe(
3200
+ "Path to the script to review, relative to the project root."
3201
+ )
3202
+ }),
3203
+ execute: async ({ filePath }) => {
3204
+ logger.info({ filePath }, "called reviewScript tool");
3205
+ const resolved2 = resolveInRoot(ctx, filePath);
3206
+ if (resolved2.ok === false) return resolved2.error;
3207
+ const isFile = await stat2(resolved2.target).then(
3208
+ (s) => s.isFile(),
3209
+ () => false
3210
+ );
3211
+ if (!isFile) {
3212
+ return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
3213
+ }
3214
+ return serializePrompt(async () => {
3215
+ openInEditor(resolved2.target);
3216
+ await useWizard.getState().requestUserInput({
3217
+ prompt: "",
3218
+ promptType: "enterToContinue",
3219
+ options: [],
3220
+ messages: [
3221
+ `Please review ${relative5(ctx.root, resolved2.target)} before it runs:`,
3222
+ resolved2.target
3223
+ ]
3224
+ });
3225
+ ctx.reviewed.push(resolved2.target);
3226
+ return "The user has reviewed the script. You may run it now.";
3227
+ });
3228
+ }
3229
+ });
3230
+ }
3231
+
3168
3232
  // src/lib/tools/generateRecord.ts
3169
- import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
3233
+ import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
3170
3234
  import { createAnthropic } from "@ai-sdk/anthropic";
3171
3235
  import { nanoid as nanoid2 } from "nanoid";
3172
3236
  import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
3173
3237
  import { dirname as dirname6 } from "node:path";
3174
- import z16 from "zod";
3238
+ import z17 from "zod";
3175
3239
  var DATA_DIR = ".algolia-wizard/data";
3176
3240
  var RECORD_MODEL = "claude-haiku-4-5";
3177
3241
  var MAX_RECORDS = 100;
@@ -3181,19 +3245,19 @@ var anthropic = createAnthropic({
3181
3245
  apiKey: process.env.PROVIDER_API_KEY ?? ""
3182
3246
  });
3183
3247
  function generateRecordTool(ctx) {
3184
- return tool9({
3248
+ return tool10({
3185
3249
  description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. 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.",
3186
- inputSchema: z16.object({
3187
- entityName: z16.string().describe("Name of the entity to generate records for."),
3188
- attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
3189
- count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
3190
- hint: z16.string().optional().describe("Optional context to steer realistic values.")
3250
+ inputSchema: z17.object({
3251
+ entityName: z17.string().describe("Name of the entity to generate records for."),
3252
+ attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
3253
+ count: z17.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
3254
+ hint: z17.string().optional().describe("Optional context to steer realistic values.")
3191
3255
  }),
3192
3256
  execute: async ({ entityName, attributes, count, hint }) => {
3193
3257
  logger.info({ entityName, count }, "called generateRecord tool");
3194
3258
  try {
3195
- const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
3196
- const recordSchema = z16.object(
3259
+ const value = z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]);
3260
+ const recordSchema = z17.object(
3197
3261
  Object.fromEntries(attributes.map((attr) => [attr, value]))
3198
3262
  );
3199
3263
  const generateBatch = async (batchCount) => {
@@ -3203,8 +3267,8 @@ function generateRecordTool(ctx) {
3203
3267
  const { output } = await generateText({
3204
3268
  model: anthropic(RECORD_MODEL),
3205
3269
  output: Output.object({
3206
- schema: z16.object({
3207
- records: z16.array(recordSchema).length(batchCount)
3270
+ schema: z17.object({
3271
+ records: z17.array(recordSchema).length(batchCount)
3208
3272
  })
3209
3273
  }),
3210
3274
  prompt: [
@@ -3261,13 +3325,13 @@ function generateRecordTool(ctx) {
3261
3325
  }
3262
3326
 
3263
3327
  // src/lib/tools/notifyUser.ts
3264
- import { tool as tool10 } from "ai";
3265
- import z17 from "zod";
3328
+ import { tool as tool11 } from "ai";
3329
+ import z18 from "zod";
3266
3330
  function notifyUserTool() {
3267
- return tool10({
3331
+ return tool11({
3268
3332
  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.`,
3269
- inputSchema: z17.object({
3270
- message: z17.string().describe(
3333
+ inputSchema: z18.object({
3334
+ message: z18.string().describe(
3271
3335
  "Short, plain-language description of what you are doing now."
3272
3336
  )
3273
3337
  }),
@@ -3311,6 +3375,7 @@ function createTools(ctx, { output, tools }) {
3311
3375
  ),
3312
3376
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
3313
3377
  runShell: withLogging("runShell", runShellTool(ctx)),
3378
+ reviewScript: withLogging("reviewScript", reviewScriptTool(ctx)),
3314
3379
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
3315
3380
  notifyUser: withLogging("notifyUser", notifyUserTool())
3316
3381
  };
@@ -3432,10 +3497,10 @@ async function runAgent(req) {
3432
3497
  }
3433
3498
 
3434
3499
  // src/actions/detectLanguage.ts
3435
- import z20 from "zod";
3436
- var detectLanguageSchema = z20.object({
3437
- languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
3438
- frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
3500
+ import z21 from "zod";
3501
+ var detectLanguageSchema = z21.object({
3502
+ languages: z21.array(z21.object({ name: z21.string(), version: z21.string() })),
3503
+ frameworks: z21.array(z21.object({ name: z21.string(), version: z21.string() }))
3439
3504
  });
3440
3505
  var detectLanguage = () => runAgent({
3441
3506
  instructions: [
@@ -3453,31 +3518,31 @@ var detectLanguage = () => runAgent({
3453
3518
  });
3454
3519
 
3455
3520
  // src/actions/analyzeCodebase.ts
3456
- import z21 from "zod";
3521
+ import z22 from "zod";
3457
3522
  var READONLY_TOOLS = [
3458
3523
  "listFiles",
3459
3524
  "changeDirectory",
3460
3525
  "readFile",
3461
3526
  "searchFiles"
3462
3527
  ];
3463
- var ingestionAnalysisSchema = z21.object({
3464
- ingestionAnalysis: z21.array(
3465
- z21.object({
3466
- name: z21.string(),
3467
- paths: z21.array(z21.string()),
3528
+ var ingestionAnalysisSchema = z22.object({
3529
+ ingestionAnalysis: z22.array(
3530
+ z22.object({
3531
+ name: z22.string(),
3532
+ paths: z22.array(z22.string()),
3468
3533
  // indexable fields the agent found for this entity
3469
- attributes: z21.array(z21.string())
3534
+ attributes: z22.array(z22.string())
3470
3535
  })
3471
3536
  )
3472
3537
  });
3473
- var searchImplementationAnalysisSchema = z21.object({
3474
- searchImplementationAnalysis: z21.string()
3538
+ var searchImplementationAnalysisSchema = z22.object({
3539
+ searchImplementationAnalysis: z22.string()
3475
3540
  });
3476
- var verificationSchema = z21.object({
3477
- verification: z21.array(z21.string())
3541
+ var verificationSchema = z22.object({
3542
+ verification: z22.array(z22.string())
3478
3543
  });
3479
3544
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3480
- var analyzeCodebaseSchema = z21.object({
3545
+ var analyzeCodebaseSchema = z22.object({
3481
3546
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3482
3547
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3483
3548
  verification: verificationSchema.shape.verification.optional(),
@@ -3539,7 +3604,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3539
3604
  // package.json
3540
3605
  var package_default = {
3541
3606
  name: "@algolia/wizard",
3542
- version: "0.16.0",
3607
+ version: "0.18.0",
3543
3608
  description: "Magically implement Algolia functionality in your codebase",
3544
3609
  type: "module",
3545
3610
  engines: {
@@ -3658,8 +3723,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
3658
3723
  }
3659
3724
 
3660
3725
  // src/actions/confirmLanguage.ts
3661
- import z23 from "zod";
3662
- var confirmLanguageSchema = z23.object({
3726
+ import z24 from "zod";
3727
+ var confirmLanguageSchema = z24.object({
3663
3728
  languages: detectLanguageSchema.shape.languages
3664
3729
  });
3665
3730
  async function confirmLanguage(ctx) {
@@ -3680,8 +3745,8 @@ async function confirmLanguage(ctx) {
3680
3745
  }
3681
3746
 
3682
3747
  // src/actions/confirmFramework.ts
3683
- import z24 from "zod";
3684
- var confirmFrameworkSchema = z24.object({
3748
+ import z25 from "zod";
3749
+ var confirmFrameworkSchema = z25.object({
3685
3750
  frameworks: detectLanguageSchema.shape.frameworks
3686
3751
  });
3687
3752
  var CURATED_FRAMEWORKS = [
@@ -3814,8 +3879,8 @@ async function promptUser(ctx, params) {
3814
3879
  }
3815
3880
 
3816
3881
  // src/actions/confirmEntities.ts
3817
- import z25 from "zod";
3818
- var confirmEntitiesSchema = z25.object({
3882
+ import z26 from "zod";
3883
+ var confirmEntitiesSchema = z26.object({
3819
3884
  // Final detection — the focused re-run may supersede project-scan's.
3820
3885
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3821
3886
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3885,15 +3950,15 @@ async function confirmEntities(ctx) {
3885
3950
  }
3886
3951
 
3887
3952
  // src/actions/review.ts
3888
- import { z as z26 } from "zod";
3889
- var reviewSchema = z26.object({
3953
+ import { z as z27 } from "zod";
3954
+ var reviewSchema = z27.object({
3890
3955
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3891
3956
  // not one entry per workflow step — a step's raw output can be a long,
3892
3957
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3893
3958
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3894
- summaryPoints: z26.array(z26.string()),
3895
- reviewPrompt: z26.string(),
3896
- nextSteps: z26.array(z26.string())
3959
+ summaryPoints: z27.array(z27.string()),
3960
+ reviewPrompt: z27.string(),
3961
+ nextSteps: z27.array(z27.string())
3897
3962
  });
3898
3963
  function formatCompletedSteps(steps) {
3899
3964
  if (!steps.length) return "(no prior steps completed)";
@@ -3944,12 +4009,12 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3944
4009
  };
3945
4010
 
3946
4011
  // src/actions/implement.ts
3947
- import z27 from "zod";
4012
+ import z28 from "zod";
3948
4013
  import { join as join12 } from "node:path";
3949
4014
 
3950
4015
  // src/lib/worktree.ts
3951
4016
  import { execFile as execFile2 } from "node:child_process";
3952
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
4017
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
3953
4018
  import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
3954
4019
  var MAX_BUFFER = 32 * 1024 * 1024;
3955
4020
  var MAX_WIZARD_WORKTREES = 3;
@@ -4020,7 +4085,7 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4020
4085
  }
4021
4086
  const source = isAbsolute2(trimmed) ? trimmed : resolve3(repoRoot, trimmed);
4022
4087
  try {
4023
- if (!(await stat2(source)).isFile()) {
4088
+ if (!(await stat3(source)).isFile()) {
4024
4089
  return { ok: false, reason: `"${sourcePath}" is not a file` };
4025
4090
  }
4026
4091
  } catch {
@@ -4195,30 +4260,30 @@ function shellQuote(value) {
4195
4260
  }
4196
4261
 
4197
4262
  // src/actions/implement.ts
4198
- var implementSchema = z27.object({
4199
- filesChanged: z27.array(z27.string()),
4200
- summary: z27.string(),
4201
- worktreePath: z27.string().optional(),
4202
- ingestCommand: z27.string().optional(),
4203
- ingestScriptRan: z27.boolean().optional(),
4204
- ingestRecordCount: z27.number().optional(),
4205
- ingestDurationMs: z27.number().optional(),
4206
- ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
4207
- searchEnvVars: z27.array(
4208
- z27.object({
4209
- name: z27.string(),
4210
- value: z27.string()
4263
+ var implementSchema = z28.object({
4264
+ filesChanged: z28.array(z28.string()),
4265
+ summary: z28.string(),
4266
+ worktreePath: z28.string().optional(),
4267
+ ingestCommand: z28.string().optional(),
4268
+ ingestScriptRan: z28.boolean().optional(),
4269
+ ingestRecordCount: z28.number().optional(),
4270
+ ingestDurationMs: z28.number().optional(),
4271
+ ingestionSource: z28.enum(["local", "fileUpload", "generated"]),
4272
+ searchEnvVars: z28.array(
4273
+ z28.object({
4274
+ name: z28.string(),
4275
+ value: z28.string()
4211
4276
  })
4212
4277
  ).optional()
4213
4278
  });
4214
- var implementationOutputSchema = z27.object({
4215
- summary: z27.string(),
4216
- ingestCommand: z27.string().optional()
4279
+ var implementationOutputSchema = z28.object({
4280
+ summary: z28.string(),
4281
+ ingestCommand: z28.string().optional()
4217
4282
  });
4218
- var verificationOutputSchema = z27.object({
4219
- summary: z27.string(),
4220
- sufficient: z27.boolean(),
4221
- additionalInstructions: z27.string().optional()
4283
+ var verificationOutputSchema = z28.object({
4284
+ summary: z28.string(),
4285
+ sufficient: z28.boolean(),
4286
+ additionalInstructions: z28.string().optional()
4222
4287
  });
4223
4288
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4224
4289
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -4351,6 +4416,7 @@ function ingestionInstructions(input) {
4351
4416
  "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.",
4352
4417
  ...algoliaClientDoc(input),
4353
4418
  "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.",
4419
+ "When the script is finished, call reviewScript with its path and wait: running it writes records to a live index, so the developer reads it first. Do not run it before that call returns.",
4354
4420
  'Then run the script yourself via runShell, and 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.',
4355
4421
  "The summary should be extremely concise.",
4356
4422
  ...sourceSpecificInstructions(input)
@@ -4363,7 +4429,7 @@ function searchInstructions(input) {
4363
4429
  "Implement an in-app Algolia search experience.",
4364
4430
  `Build the search UI for ${input.searchUiTarget}.`,
4365
4431
  ...doc ? [
4366
- "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4432
+ "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:",
4367
4433
  doc
4368
4434
  ] : [
4369
4435
  "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
@@ -4421,6 +4487,7 @@ var useCaseToolMap = {
4421
4487
  "writeFile",
4422
4488
  "writeCredentials",
4423
4489
  "runShell",
4490
+ "reviewScript",
4424
4491
  "notifyUser"
4425
4492
  ],
4426
4493
  search: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"],
@@ -4675,7 +4742,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4675
4742
  const result = await runImplementationUseCase("ingestion");
4676
4743
  summaries.push(formatSummary("ingestion", result.summary));
4677
4744
  ingestCommand = result.ingestCommand;
4678
- const executions = (ingestionTools ?? searchTools).shell.executions;
4745
+ const ingestionContext = ingestionTools ?? searchTools;
4746
+ const executions = ingestionContext.shell.executions;
4679
4747
  const {
4680
4748
  run: ingestRun,
4681
4749
  attempt: ingestAttempt,
@@ -4684,6 +4752,15 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4684
4752
  ingestScriptRan = ingestRun != null;
4685
4753
  ingestRecordCount = recordCount;
4686
4754
  ingestDurationMs = ingestRun?.durationMs;
4755
+ if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4756
+ summaries.push(
4757
+ "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
4758
+ );
4759
+ logger.warn(
4760
+ { ingestCommand },
4761
+ "implement: ingestion ran without a reviewScript call"
4762
+ );
4763
+ }
4687
4764
  if (ingestScriptRan) {
4688
4765
  ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4689
4766
  if (ingestRecordCount != null) {
@@ -4913,8 +4990,8 @@ var defaultWorkflow = {
4913
4990
  defineStep({
4914
4991
  id: "select-index",
4915
4992
  title: "Set up index",
4916
- outputSchema: z28.object({
4917
- selection: z28.string()
4993
+ outputSchema: z29.object({
4994
+ selection: z29.string()
4918
4995
  }),
4919
4996
  run: (ctx) => selectIndexStep(ctx)
4920
4997
  }),
@@ -27,6 +27,7 @@ Always install the **latest stable** within the major (use a caret range like `^
27
27
  client cache and causes re-renders.
28
28
  - For InstantSearch, import the client from `algoliasearch/lite` (smaller bundle and
29
29
  correct types — see `instantsearch-setup.md`).
30
+ - Always enable **Insights** on the InstantSearch root with `insights: true`. Without it there are no click/conversion events, so many Algolia features will not work as expected.
30
31
 
31
32
  ## Files
32
33
 
@@ -22,7 +22,11 @@ import { searchBox, hits } from 'instantsearch.js/es/widgets'
22
22
 
23
23
  const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
24
24
 
25
- const search = instantsearch({ indexName: 'INDEX_NAME', searchClient })
25
+ const search = instantsearch({
26
+ indexName: 'INDEX_NAME',
27
+ searchClient,
28
+ insights: true,
29
+ })
26
30
 
27
31
  search.addWidgets([
28
32
  searchBox({ container: '#searchbox' }),
@@ -32,6 +36,122 @@ search.addWidgets([
32
36
  search.start()
33
37
  ```
34
38
 
39
+ ## Insights (click analytics) — always enable it
40
+
41
+ The `insights` option turns on the Insights middleware (`instantsearch.js` v4.55+),
42
+ which adds `clickAnalytics: true` to every query and keeps the `userToken` in sync
43
+ between search and event calls. Never set `clickAnalytics` or `userToken` on the `configure` widget by hand.
44
+
45
+ Use `insights: true`. The middleware loads `search-insights` from the jsDelivr CDN
46
+ itself; do not install the package or pass `insightsClient`.
47
+
48
+ If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
49
+ case, install `search-insights` from npm and pass its default export as
50
+ `insightsClient` in the `insights` option:
51
+
52
+ ```js
53
+ import aa from 'search-insights'
54
+
55
+ const search = instantsearch({
56
+ indexName: 'INDEX_NAME',
57
+ searchClient,
58
+ insights: { insightsClient: aa },
59
+ })
60
+ ```
61
+
62
+ The middleware calls `aa('init', ...)` itself with the app ID and search key, so no
63
+ manual `init` call is needed. Keep using the `insightsInitParams` option (for
64
+ example `useCookie`) the same way as with the CDN path.
65
+
66
+ The middleware sets an anonymous `userToken` itself, so events fire with no extra
67
+ code. That token lives in memory only — a page reload starts a new one, which is
68
+ enough for search→click attribution within a session but not across sessions, and not
69
+ enough for Personalization to build a profile. To persist it in the first-party
70
+ `_ALGOLIA` cookie, switch to the object form — only where the app's cookie-consent
71
+ policy allows it:
72
+
73
+ ```js
74
+ insights: {
75
+ insightsInitParams: {
76
+ useCookie: true
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Click and conversion events
82
+
83
+ The `hits` and `infiniteHits` widgets send `view` and `click` events on their own. A
84
+ custom `item` template whose click target is your own element gets `sendEvent` as the
85
+ second argument:
86
+
87
+ ```js
88
+ hits({
89
+ container: '#hits',
90
+ templates: {
91
+ item(hit, { html, sendEvent }) {
92
+ return html`<a
93
+ href="${hit.url}"
94
+ onClick="${() => sendEvent('click', hit, 'Hit Clicked')}"
95
+ >${hit.name}</a
96
+ >`
97
+ },
98
+ },
99
+ })
100
+ ```
101
+
102
+ Conversions are business actions (add to cart, signup, booking), so wire them at the
103
+ real action, not at render:
104
+
105
+ ```js
106
+ sendEvent('conversion', hit, 'Product Added To Cart', {
107
+ eventSubtype: 'addToCart',
108
+ objectData: [{ price: hit.price, quantity: 1 }],
109
+ value: hit.price,
110
+ currency: 'USD',
111
+ })
112
+ ```
113
+
114
+ Where the app has no such action on a search hit, leave a TODO at the best
115
+ candidate instead of inventing one.
116
+
117
+ Connector-based custom widgets get `sendEvent` in their render options, and unlike
118
+ the widgets above they send **no** click events automatically — wire them yourself.
119
+
120
+ ### Authenticated users
121
+
122
+ Anonymous tokens need no code. When the app already has a stable user id, link it
123
+ after login and clear it on logout — the anonymous `userToken` keeps flowing
124
+ alongside it (Personalization uses that one, not the authenticated token):
125
+
126
+ ```js
127
+ if (typeof window !== 'undefined') {
128
+ window.aa('setAuthenticatedUserToken', userId)
129
+ }
130
+ ```
131
+
132
+ ```js
133
+ if (typeof window !== 'undefined') {
134
+ window.aa('setAuthenticatedUserToken', undefined) // on logout
135
+ }
136
+ ```
137
+
138
+ The middleware installs `window.aa` as a queue before the CDN script arrives, so these
139
+ calls need no load-order guard. Under a server-side rendering framework (Angular
140
+ Universal, SvelteKit), `window` does not exist on the server, so `window.aa` throws
141
+ unless you add this guard or call it only from client-side code. In TypeScript
142
+ nothing augments `Window`, so declare it once:
143
+ `declare global { interface Window { aa: (method: string, ...args: unknown[]) => void } }`.
144
+
145
+ With the npm `insightsClient` fallback, call the same method on the imported `aa`
146
+ function instead of `window.aa`:
147
+
148
+ ```js
149
+ import aa from 'search-insights'
150
+
151
+ aa('setAuthenticatedUserToken', userId)
152
+ aa('setAuthenticatedUserToken', undefined) // on logout
153
+ ```
154
+
35
155
  ## Frameworks without their own flavor
36
156
 
37
157
  Only React and Vue have a maintained InstantSearch wrapper. For any other
@@ -28,7 +28,11 @@ const searchClient = algoliasearch(
28
28
 
29
29
  export function Search() {
30
30
  return (
31
- <InstantSearch searchClient={searchClient} indexName="INDEX_NAME">
31
+ <InstantSearch
32
+ searchClient={searchClient}
33
+ indexName="INDEX_NAME"
34
+ insights={true}
35
+ >
32
36
  <SearchBox />
33
37
  <Hits />
34
38
  </InstantSearch>
@@ -37,3 +41,111 @@ export function Search() {
37
41
  ```
38
42
 
39
43
  Do not inline `searchClient={algoliasearch(...)}` — keep the stable reference above.
44
+
45
+ ## Insights (click analytics) — always enable it
46
+
47
+ The `insights` prop turns on the Insights middleware, which adds
48
+ `clickAnalytics: true` to every query and keeps the `userToken` in sync between search
49
+ and event calls. Never set `clickAnalytics` or `userToken` on `<Configure>` by hand.
50
+
51
+ Use `insights={true}`. The middleware loads `search-insights` from the jsDelivr CDN
52
+ itself; do not install the package or pass `insightsClient`.
53
+
54
+ If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
55
+ case, install `search-insights` from npm and pass its default export as
56
+ `insightsClient`:
57
+
58
+ ```tsx
59
+ import aa from 'search-insights'
60
+
61
+ <InstantSearch insights={{ insightsClient: aa }} />
62
+ ```
63
+
64
+ The middleware calls `aa('init', ...)` itself with the app ID and search key, so no
65
+ manual `init` call is needed. Keep using the `insightsInitParams` option (for
66
+ example `useCookie`) the same way as with the CDN path.
67
+
68
+ The middleware sets an anonymous `userToken` itself, so events fire with no extra
69
+ code. That token lives in memory only — a page reload starts a new one, which is
70
+ enough for search→click attribution within a session but not across sessions, and not
71
+ enough for Personalization to build a profile. To persist it in the first-party
72
+ `_ALGOLIA` cookie, switch to the object form — only where the app's cookie-consent
73
+ policy allows it:
74
+
75
+ ```tsx
76
+ <InstantSearch insights={{ insightsInitParams: { useCookie: true } }} />
77
+ ```
78
+
79
+ ### Click and conversion events
80
+
81
+ `<Hits>` and `<InfiniteHits>` send `view` and `click` events on their own. Two cases
82
+ need explicit wiring:
83
+
84
+ - A custom `hitComponent` whose click target is your own element — it receives a
85
+ `sendEvent` prop.
86
+ - `useHits` / `useInfiniteHits`: `view` is automatic, `click` is **not**.
87
+
88
+ ```tsx
89
+ function Hit({ hit, sendEvent }) {
90
+ return (
91
+ <a href={hit.url} onClick={() => sendEvent('click', hit, 'Hit Clicked')}>
92
+ {hit.name}
93
+ </a>
94
+ )
95
+ }
96
+ ```
97
+
98
+ Conversions are business actions (add to cart, signup, booking), so wire them at the
99
+ real action, not at render:
100
+
101
+ ```tsx
102
+ sendEvent('conversion', hit, 'Product Added To Cart', {
103
+ eventSubtype: 'addToCart',
104
+ objectData: [{ price: hit.price, quantity: 1 }],
105
+ value: hit.price,
106
+ currency: 'USD',
107
+ })
108
+ ```
109
+
110
+ Where the app has no such action on a search hit, leave a TODO at the best
111
+ candidate instead of inventing one.
112
+
113
+ ### Authenticated users
114
+
115
+ Anonymous tokens need no code. When the app already has a stable user id, link it
116
+ after login and clear it on logout — the anonymous `userToken` keeps flowing
117
+ alongside it (Personalization uses that one, not the authenticated token):
118
+
119
+ ```ts
120
+ // `search-insights` is CDN-loaded, so nothing augments Window — declare it once.
121
+ declare global {
122
+ interface Window {
123
+ aa: (method: string, ...args: unknown[]) => void
124
+ }
125
+ }
126
+
127
+ if (typeof window !== 'undefined') {
128
+ window.aa('setAuthenticatedUserToken', userId)
129
+ }
130
+ ```
131
+
132
+ ```ts
133
+ if (typeof window !== 'undefined') {
134
+ window.aa('setAuthenticatedUserToken', undefined) // on logout
135
+ }
136
+ ```
137
+
138
+ The middleware installs `window.aa` as a queue before the CDN script arrives, so these
139
+ calls need no load-order guard. Under Next.js SSR (and React Server Components),
140
+ `window` does not exist on the server, so `window.aa` throws unless you add this
141
+ guard or call it only from a client-side hook such as `useEffect`.
142
+
143
+ With the npm `insightsClient` fallback, call the same method on the imported `aa`
144
+ function instead of `window.aa`:
145
+
146
+ ```ts
147
+ import aa from 'search-insights'
148
+
149
+ aa('setAuthenticatedUserToken', userId)
150
+ aa('setAuthenticatedUserToken', undefined) // on logout
151
+ ```
@@ -17,7 +17,11 @@ const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
17
17
 
18
18
  ```vue
19
19
  <template>
20
- <ais-instant-search :search-client="searchClient" index-name="INDEX_NAME">
20
+ <ais-instant-search
21
+ :search-client="searchClient"
22
+ index-name="INDEX_NAME"
23
+ :insights="true"
24
+ >
21
25
  <ais-search-box />
22
26
  <ais-hits />
23
27
  </ais-instant-search>
@@ -39,3 +43,99 @@ Register the widgets via the InstantSearch Vue plugin in your app entry:
39
43
  import InstantSearch from 'vue-instantsearch/vue3/es'
40
44
  app.use(InstantSearch)
41
45
  ```
46
+
47
+ ## Insights (click analytics) — always enable it
48
+
49
+ The `insights` prop turns on the Insights middleware, which adds
50
+ `clickAnalytics: true` to every query and keeps the `userToken` in sync between search
51
+ and event calls. Never set `clickAnalytics` or `userToken` on `ais-configure` by hand.
52
+
53
+ Use `:insights="true"`. The middleware loads `search-insights` from the jsDelivr CDN itself; do not install
54
+ the package or pass `insightsClient`.
55
+
56
+ If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
57
+ case, install `search-insights` from npm and pass its default export as
58
+ `insightsClient`:
59
+
60
+ ```js
61
+ import aa from 'search-insights'
62
+ const insights = { insightsClient: aa } // then :insights="insights"
63
+ ```
64
+
65
+ The middleware calls `aa('init', ...)` itself with the app ID and search key, so no
66
+ manual `init` call is needed.
67
+
68
+ The middleware sets an anonymous `userToken` itself, so events fire with no extra
69
+ code. That token lives in memory only — a page reload starts a new one, which is
70
+ enough for search→click attribution within a session but not across sessions, and not
71
+ enough for Personalization to build a profile. To persist it in the first-party
72
+ `_ALGOLIA` cookie, bind the object form — only where the app's cookie-consent policy
73
+ allows it:
74
+
75
+ ```js
76
+ const insights = { insightsInitParams: { useCookie: true } } // then :insights="insights"
77
+ ```
78
+
79
+ ### Click and conversion events
80
+
81
+ `ais-hits` sends `view` and `click` events on its own. A custom `item` slot whose
82
+ click target is your own element needs the slot's `sendEvent`:
83
+
84
+ ```vue
85
+ <ais-hits>
86
+ <template v-slot:item="{ item, sendEvent }">
87
+ <a :href="item.url" @click="sendEvent('click', item, 'Hit Clicked')">
88
+ {{ item.name }}
89
+ </a>
90
+ </template>
91
+ </ais-hits>
92
+ ```
93
+
94
+ Conversions are business actions (add to cart, signup, booking), so wire them at the
95
+ real action, not at render:
96
+
97
+ ```js
98
+ sendEvent('conversion', item, 'Product Added To Cart', {
99
+ eventSubtype: 'addToCart',
100
+ objectData: [{ price: item.price, quantity: 1 }],
101
+ value: item.price,
102
+ currency: 'USD',
103
+ })
104
+ ```
105
+
106
+ Where the app has no such action on a search hit, leave a TODO at the best
107
+ candidate instead of inventing one.
108
+
109
+ ### Authenticated users
110
+
111
+ Anonymous tokens need no code. When the app already has a stable user id, link it
112
+ after login and clear it on logout — the anonymous `userToken` keeps flowing
113
+ alongside it (Personalization uses that one, not the authenticated token):
114
+
115
+ ```js
116
+ if (typeof window !== 'undefined') {
117
+ window.aa('setAuthenticatedUserToken', userId)
118
+ }
119
+ ```
120
+
121
+ ```js
122
+ if (typeof window !== 'undefined') {
123
+ window.aa('setAuthenticatedUserToken', undefined) // on logout
124
+ }
125
+ ```
126
+
127
+ The middleware installs `window.aa` as a queue before the CDN script arrives, so these
128
+ calls need no load-order guard. Under Nuxt SSR, `window` does not exist on the server,
129
+ so `window.aa` throws unless you add this guard or call it only from a client-side
130
+ hook such as `onMounted`. In TypeScript nothing augments `Window`, so declare it
131
+ once: `declare global { interface Window { aa: (method: string, ...args: unknown[]) => void } }`.
132
+
133
+ With the npm `insightsClient` fallback, call the same method on the imported `aa`
134
+ function instead of `window.aa`:
135
+
136
+ ```js
137
+ import aa from 'search-insights'
138
+
139
+ aa('setAuthenticatedUserToken', userId)
140
+ aa('setAuthenticatedUserToken', undefined) // on logout
141
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {