fluxflow-cli 1.18.20 → 1.18.21

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.
Files changed (2) hide show
  1. package/dist/fluxflow.js +76 -46
  2. package/package.json +2 -2
package/dist/fluxflow.js CHANGED
@@ -4290,6 +4290,7 @@ var init_write_docx = __esm({
4290
4290
 
4291
4291
  // src/tools/search_keyword.js
4292
4292
  import { exec } from "child_process";
4293
+ import path13 from "path";
4293
4294
  var search_keyword;
4294
4295
  var init_search_keyword = __esm({
4295
4296
  "src/tools/search_keyword.js"() {
@@ -4302,14 +4303,14 @@ var init_search_keyword = __esm({
4302
4303
  let command = "";
4303
4304
  if (file) {
4304
4305
  if (isWindows) {
4305
- command = `powershell -Command "if (Test-Path '${file}') { Select-String -Path '${file}' -Pattern '${keyword}' | Select-Object -First 150 | ForEach-Object { $rel = Resolve-Path $_.Path -Relative; '{0}:{1}:' -f $rel, $_.LineNumber } } else { Write-Error 'File not found: ${file}' }"`;
4306
+ command = `powershell -NoProfile -Command "if (Test-Path '${file}') { Select-String -Path '${file}' -Pattern '${keyword}' -ErrorAction SilentlyContinue | Select-Object -First 150 | ForEach-Object { '{0}|{1}' -f $_.Path, $_.LineNumber } } else { Write-Error 'File not found: ${file}' }"`;
4306
4307
  } else {
4307
4308
  command = `grep -HnI "${keyword}" "${file}" | head -n 150`;
4308
4309
  }
4309
4310
  } else {
4310
4311
  if (isWindows) {
4311
4312
  const excludePattern = excludes.join("|").replace(/\./g, "\\.");
4312
- command = `powershell -Command "Get-ChildItem -Path . -Recurse -File | Where-Object { $_.FullName -notmatch '${excludePattern}' } | Select-String -Pattern '${keyword}' | Select-Object -First 150 | ForEach-Object { $rel = Resolve-Path $_.Path -Relative; '{0}:{1}:' -f $rel, $_.LineNumber }"`;
4313
+ command = `powershell -NoProfile -Command "Get-ChildItem -Path . -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '${excludePattern}' } | Select-String -Pattern '${keyword}' -ErrorAction SilentlyContinue | Select-Object -First 150 | ForEach-Object { '{0}|{1}' -f $_.Path, $_.LineNumber }"`;
4313
4314
  } else {
4314
4315
  const excludeDirArgs = excludes.map((d) => `--exclude-dir="${d}"`).join(" ");
4315
4316
  command = `grep -rnI ${excludeDirArgs} "${keyword}" . | head -n 150`;
@@ -4334,12 +4335,41 @@ var init_search_keyword = __esm({
4334
4335
  });
4335
4336
  if (filteredLines.length === 0) return resolve(`Found 0 matches for keyword: "${keyword}"${file ? ` in file: ${file}` : ""}`);
4336
4337
  const matches = filteredLines.slice(0, 150).map((line) => {
4337
- const firstColon = line.indexOf(":");
4338
- const secondColon = line.indexOf(":", firstColon + 1);
4339
- if (firstColon === -1 || secondColon === -1) return null;
4340
- const filePath = line.substring(0, firstColon).replace(/^(\.\/|\.\\)/, "");
4341
- const lineNum = line.substring(firstColon + 1, secondColon);
4342
- return `${filePath} ${lineNum}`;
4338
+ if (line.includes("|")) {
4339
+ const parts = line.split("|");
4340
+ let rawPath = parts[0];
4341
+ if (path13.isAbsolute(rawPath)) {
4342
+ rawPath = path13.relative(process.cwd(), rawPath);
4343
+ }
4344
+ const filePath = rawPath.replace(/^(\.\/|\.\\)/, "").replace(/\\/g, "/");
4345
+ const lineNum = parts[1];
4346
+ return `${filePath} ${lineNum}`;
4347
+ } else {
4348
+ let rawPath, lineNum;
4349
+ const driveMatch = line.match(/^([a-zA-Z]:)/);
4350
+ if (driveMatch) {
4351
+ const startSearch = 2;
4352
+ const nextColon = line.indexOf(":", startSearch);
4353
+ const thirdColon = line.indexOf(":", nextColon + 1);
4354
+ if (nextColon !== -1 && thirdColon !== -1) {
4355
+ rawPath = line.substring(0, nextColon);
4356
+ lineNum = line.substring(nextColon + 1, thirdColon);
4357
+ }
4358
+ } else {
4359
+ const firstColon = line.indexOf(":");
4360
+ const secondColon = line.indexOf(":", firstColon + 1);
4361
+ if (firstColon !== -1 && secondColon !== -1) {
4362
+ rawPath = line.substring(0, firstColon);
4363
+ lineNum = line.substring(firstColon + 1, secondColon);
4364
+ }
4365
+ }
4366
+ if (!rawPath || !lineNum) return null;
4367
+ if (path13.isAbsolute(rawPath)) {
4368
+ rawPath = path13.relative(process.cwd(), rawPath);
4369
+ }
4370
+ const filePath = rawPath.replace(/^(\.\/|\.\\)/, "").replace(/\\/g, "/");
4371
+ return `${filePath} ${lineNum}`;
4372
+ }
4343
4373
  }).filter(Boolean);
4344
4374
  let output = `Found ${filteredLines.length} matches:
4345
4375
 
@@ -4357,7 +4387,7 @@ var init_search_keyword = __esm({
4357
4387
 
4358
4388
  // src/utils/settings.js
4359
4389
  import fs14 from "fs-extra";
4360
- import path13 from "path";
4390
+ import path14 from "path";
4361
4391
  var DEFAULT_SETTINGS, loadSettings, migrateToExternal, saveSettings;
4362
4392
  var init_settings = __esm({
4363
4393
  "src/utils/settings.js"() {
@@ -4444,8 +4474,8 @@ var init_settings = __esm({
4444
4474
  const { FLUXFLOW_DIR: FLUXFLOW_DIR2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
4445
4475
  const folders = ["logs", "secret"];
4446
4476
  for (const folder of folders) {
4447
- const src = path13.join(FLUXFLOW_DIR2, folder);
4448
- const dest = path13.join(newPath, folder);
4477
+ const src = path14.join(FLUXFLOW_DIR2, folder);
4478
+ const dest = path14.join(newPath, folder);
4449
4479
  try {
4450
4480
  if (await fs14.exists(src)) {
4451
4481
  await fs14.ensureDir(dest);
@@ -4475,7 +4505,7 @@ var init_settings = __esm({
4475
4505
  if (updated.imageSettings) {
4476
4506
  updated.imageSettings = { ...updated.imageSettings, apiKey: "" };
4477
4507
  }
4478
- await fs14.ensureDir(path13.dirname(SETTINGS_FILE));
4508
+ await fs14.ensureDir(path14.dirname(SETTINGS_FILE));
4479
4509
  writeAesEncryptedJson(SETTINGS_FILE, updated);
4480
4510
  return true;
4481
4511
  } catch (err) {
@@ -4496,7 +4526,7 @@ var init_fallback_key = __esm({
4496
4526
 
4497
4527
  // src/tools/generate_image.js
4498
4528
  import fs15 from "fs-extra";
4499
- import path14 from "path";
4529
+ import path15 from "path";
4500
4530
  var injectPngMetadata, generate_image;
4501
4531
  var init_generate_image = __esm({
4502
4532
  "src/tools/generate_image.js"() {
@@ -4676,12 +4706,12 @@ var init_generate_image = __esm({
4676
4706
  "Seed": String(seed)
4677
4707
  };
4678
4708
  finalBuffer = injectPngMetadata(finalBuffer, metadata);
4679
- const absolutePath = path14.resolve(process.cwd(), outputPath);
4680
- await fs15.ensureDir(path14.dirname(absolutePath));
4709
+ const absolutePath = path15.resolve(process.cwd(), outputPath);
4710
+ await fs15.ensureDir(path15.dirname(absolutePath));
4681
4711
  await RevertManager.recordFileChange(absolutePath);
4682
4712
  await fs15.writeFile(absolutePath, finalBuffer);
4683
4713
  await recordImageGeneration(settings);
4684
- const ext = path14.extname(outputPath).toLowerCase();
4714
+ const ext = path15.extname(outputPath).toLowerCase();
4685
4715
  const mimeMap = {
4686
4716
  ".jpg": "image/jpeg",
4687
4717
  ".jpeg": "image/jpeg",
@@ -4871,7 +4901,7 @@ var init_tools = __esm({
4871
4901
 
4872
4902
  // src/utils/ai.js
4873
4903
  import { GoogleGenAI, ThinkingLevel, HarmBlockThreshold, HarmCategory } from "@google/genai";
4874
- import path15 from "path";
4904
+ import path16 from "path";
4875
4905
  import fs16 from "fs";
4876
4906
  var client, TERMINATION_SIGNAL, stripAnsi2, signalTermination, TOOL_LABELS2, getToolDetail, runJanitorTask, getActiveToolContext, getContextSafeText, contextSafeReplace, getSanitizedText, detectToolCalls, initAI, consolidatePastMemories, getAIStream;
4877
4907
  var init_ai = __esm({
@@ -4913,7 +4943,7 @@ var init_ai = __esm({
4913
4943
  try {
4914
4944
  const pArgs = parseArgs(argsStr);
4915
4945
  const filePath = pArgs.path || pArgs.targetFile || pArgs.TargetFile || pArgs.directory;
4916
- return filePath ? path15.basename(filePath.replace(/["']/g, "").replace(/\\/g, "/")) : null;
4946
+ return filePath ? path16.basename(filePath.replace(/["']/g, "").replace(/\\/g, "/")) : null;
4917
4947
  } catch (e) {
4918
4948
  return null;
4919
4949
  }
@@ -5080,9 +5110,9 @@ ${originalTextProcessed.length > USER_CONTEXT_LENGTH ? "... (truncated) ...\n\n"
5080
5110
  process.stdout.write(`\x1B]0;Finalizing Error\x07`);
5081
5111
  }
5082
5112
  await new Promise((resolve) => setTimeout(resolve, 1e3));
5083
- const janitorErrDir = path15.join(LOGS_DIR, "janitor");
5113
+ const janitorErrDir = path16.join(LOGS_DIR, "janitor");
5084
5114
  if (!fs16.existsSync(janitorErrDir)) fs16.mkdirSync(janitorErrDir, { recursive: true });
5085
- fs16.appendFileSync(path15.join(janitorErrDir, "error.log"), `ERROR [Attempt ${attempts}/${MAX_JANITOR_RETRIES + 1}] [${date}]: ${String(janitorErr)}
5115
+ fs16.appendFileSync(path16.join(janitorErrDir, "error.log"), `ERROR [Attempt ${attempts}/${MAX_JANITOR_RETRIES + 1}] [${date}]: ${String(janitorErr)}
5086
5116
 
5087
5117
  `);
5088
5118
  if (attempts > MAX_JANITOR_RETRIES) break;
@@ -5091,8 +5121,8 @@ ${originalTextProcessed.length > USER_CONTEXT_LENGTH ? "... (truncated) ...\n\n"
5091
5121
  }
5092
5122
  }
5093
5123
  if (attempts) {
5094
- const janitorErrDir = path15.join(LOGS_DIR, "janitor");
5095
- fs16.appendFileSync(path15.join(janitorErrDir, "error.log"), `-----------------------------------------------------------------------------
5124
+ const janitorErrDir = path16.join(LOGS_DIR, "janitor");
5125
+ fs16.appendFileSync(path16.join(janitorErrDir, "error.log"), `-----------------------------------------------------------------------------
5096
5126
 
5097
5127
 
5098
5128
  `);
@@ -5408,10 +5438,10 @@ ${newMemoryListStr}
5408
5438
  }
5409
5439
  }
5410
5440
  } catch (err) {
5411
- const janitorLogDir = path15.join(LOGS_DIR, "janitor");
5441
+ const janitorLogDir = path16.join(LOGS_DIR, "janitor");
5412
5442
  if (!fs16.existsSync(janitorLogDir)) fs16.mkdirSync(janitorLogDir, { recursive: true });
5413
5443
  fs16.appendFileSync(
5414
- path15.join(janitorLogDir, "error.log"),
5444
+ path16.join(janitorLogDir, "error.log"),
5415
5445
  `[${(/* @__PURE__ */ new Date()).toLocaleString()}] Past memory batch consolidation error: ${err.message}
5416
5446
  `
5417
5447
  );
@@ -5615,16 +5645,16 @@ ${newMemoryListStr}
5615
5645
  if (COLLAPSED_DIRS_GLOBAL.includes(entry.name)) continue;
5616
5646
  if (entry.isDirectory()) {
5617
5647
  currentCount.value++;
5618
- countFolders(path15.join(dir, entry.name), currentCount, depth + 1);
5648
+ countFolders(path16.join(dir, entry.name), currentCount, depth + 1);
5619
5649
  }
5620
5650
  }
5621
5651
  return currentCount.value;
5622
5652
  };
5623
5653
  const getDirTree = (dir, maxDepth, prefix = "", depth = 1) => {
5624
5654
  const entries = safeReaddirWithTypes(dir);
5625
- const sep = path15.sep;
5655
+ const sep = path16.sep;
5626
5656
  if (entries.length > 100) {
5627
- return `${prefix}\u2514\u2500\u2500 ${path15.basename(dir)}${sep} ...100+ files...
5657
+ return `${prefix}\u2514\u2500\u2500 ${path16.basename(dir)}${sep} ...100+ files...
5628
5658
  `;
5629
5659
  }
5630
5660
  let result = "";
@@ -5642,7 +5672,7 @@ ${newMemoryListStr}
5642
5672
  ];
5643
5673
  finalItems.forEach((item, index) => {
5644
5674
  const isLast = index === finalItems.length - 1;
5645
- const filePath = path15.join(dir, item.name);
5675
+ const filePath = path16.join(dir, item.name);
5646
5676
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
5647
5677
  const childPrefix = prefix + (isLast ? " " : "\u2502 ");
5648
5678
  if (item.isCollapsed) {
@@ -5907,12 +5937,12 @@ ${thinkingLevel != "Fast" ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITI
5907
5937
  if (keyword) {
5908
5938
  detail = keyword.replace(/["']/g, "");
5909
5939
  } else if (filePath) {
5910
- detail = path15.basename(filePath.replace(/["']/g, "").replace(/\\/g, "/"));
5940
+ detail = path16.basename(filePath.replace(/["']/g, "").replace(/\\/g, "/"));
5911
5941
  } else {
5912
5942
  const m = partialArgs.match(/(?:path|targetFile|TargetFile|directory|keyword)\s*=\s*\\?["']?([^\\"' \),]+)/);
5913
5943
  if (m) {
5914
5944
  const val = m[1].replace(/["']/g, "");
5915
- detail = potentialTool === "search_keyword" ? val : path15.basename(val.replace(/\\/g, "/"));
5945
+ detail = potentialTool === "search_keyword" ? val : path16.basename(val.replace(/\\/g, "/"));
5916
5946
  }
5917
5947
  }
5918
5948
  }
@@ -6072,7 +6102,7 @@ ${thinkingLevel != "Fast" ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITI
6072
6102
  let totalLines = "...";
6073
6103
  let actualEndLine = eLine;
6074
6104
  try {
6075
- const absPath = path15.resolve(process.cwd(), targetPath2);
6105
+ const absPath = path16.resolve(process.cwd(), targetPath2);
6076
6106
  if (fs16.existsSync(absPath)) {
6077
6107
  const content = fs16.readFileSync(absPath, "utf8");
6078
6108
  const lines = content.split("\n").length;
@@ -6094,8 +6124,8 @@ ${thinkingLevel != "Fast" ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITI
6094
6124
  }
6095
6125
  } else if (normToolName === "list_files" || normToolName === "read_folder") {
6096
6126
  const action = normToolName === "list_files" ? "List" : "Viewed";
6097
- const path17 = parseArgs(toolCall.args).path;
6098
- label = `\u{1F4C2} ${action}: ${path17 === "." ? "./" : path17}`;
6127
+ const path18 = parseArgs(toolCall.args).path;
6128
+ label = `\u{1F4C2} ${action}: ${path18 === "." ? "./" : path18}`;
6099
6129
  } else if (normToolName === "write_file" || normToolName === "update_file") {
6100
6130
  const action = normToolName === "write_file" ? "Created" : "Edited";
6101
6131
  label = `\u{1F4BE} ${action}: ${parseArgs(toolCall.args).path || "..."}`;
@@ -6117,7 +6147,7 @@ ${thinkingLevel != "Fast" ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITI
6117
6147
  const { command } = parseArgs(toolCall.args);
6118
6148
  if (command && settings.systemSettings && settings.systemSettings.allowExternalAccess === false) {
6119
6149
  const riskyPatterns = [/[a-zA-Z]:[\\\/]/i, /^\//, /\.\.[\\\/]/, /\/etc\//, /\/var\//, /\/root\//, /\/bin\//, /\/usr\//];
6120
- const currentDrive = path15.resolve(process.cwd()).substring(0, 3).toLowerCase();
6150
+ const currentDrive = path16.resolve(process.cwd()).substring(0, 3).toLowerCase();
6121
6151
  const isViolating = riskyPatterns.some((pattern) => {
6122
6152
  if (pattern.source === "[a-zA-Z]:[\\\\\\/]") {
6123
6153
  const driveMatch = command.match(/[a-zA-Z]:[\\\/]/i);
@@ -6146,8 +6176,8 @@ ${thinkingLevel != "Fast" ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITI
6146
6176
  const targetPath = parsedArgs.path || parsedArgs.targetPath || null;
6147
6177
  if (targetPath) {
6148
6178
  const isExternalOff = settings.systemSettings && settings.systemSettings.allowExternalAccess === false;
6149
- const absoluteTarget = path15.resolve(targetPath);
6150
- const absoluteCwd = path15.resolve(process.cwd());
6179
+ const absoluteTarget = path16.resolve(targetPath);
6180
+ const absoluteCwd = path16.resolve(process.cwd());
6151
6181
  if (isExternalOff && !absoluteTarget.startsWith(absoluteCwd)) {
6152
6182
  const denyMsg = `Access Denied. You are not allowed to access files outside the current workspace.`;
6153
6183
  if (normToolName === "write_file" || normToolName === "update_file") {
@@ -6418,9 +6448,9 @@ ${boxBottom}` };
6418
6448
  const errMsg = err.status || err.error && err.error.message || String(err);
6419
6449
  const errLog = String(err);
6420
6450
  const date = (/* @__PURE__ */ new Date()).toLocaleString();
6421
- const agentErrDir = path15.join(LOGS_DIR, "agent");
6451
+ const agentErrDir = path16.join(LOGS_DIR, "agent");
6422
6452
  if (!fs16.existsSync(agentErrDir)) fs16.mkdirSync(agentErrDir, { recursive: true });
6423
- fs16.appendFileSync(path15.join(agentErrDir, "error.log"), `ERROR [${date}]: ${errLog}
6453
+ fs16.appendFileSync(path16.join(agentErrDir, "error.log"), `ERROR [${date}]: ${errLog}
6424
6454
 
6425
6455
  ----------------------------------------------------------------------
6426
6456
 
@@ -6464,7 +6494,7 @@ ${recoveryText}`
6464
6494
  yield { type: "status", content: `Error Occured. Recovering Stream...` };
6465
6495
  } else {
6466
6496
  throw new Error(`Stream collapsed too many times. (Failed to resolve ${MAX_RETRIES} times)
6467
- Error Log can be found in ${path15.join(LOGS_DIR, "agent", "error.log")}`);
6497
+ Error Log can be found in ${path16.join(LOGS_DIR, "agent", "error.log")}`);
6468
6498
  }
6469
6499
  } else {
6470
6500
  if (retryCount <= MAX_RETRIES) {
@@ -6481,7 +6511,7 @@ Error Log can be found in ${path15.join(LOGS_DIR, "agent", "error.log")}`);
6481
6511
  yield { type: "status", content: `Trying to reach ${modelName}...` };
6482
6512
  } else {
6483
6513
  throw new Error(`Model ${modelName} cannot be reached. (Failed ${MAX_RETRIES} times)
6484
- Error Log can be found in ${path15.join(LOGS_DIR, "agent", "error.log")}`);
6514
+ Error Log can be found in ${path16.join(LOGS_DIR, "agent", "error.log")}`);
6485
6515
  }
6486
6516
  }
6487
6517
  }
@@ -7007,7 +7037,7 @@ import os4 from "os";
7007
7037
  import React13, { useState as useState10, useEffect as useEffect7, useRef as useRef3, useMemo as useMemo2 } from "react";
7008
7038
  import { Box as Box13, Text as Text13, useInput as useInput7, useStdout } from "ink";
7009
7039
  import fs18 from "fs-extra";
7010
- import path16 from "path";
7040
+ import path17 from "path";
7011
7041
  import { exec as exec3 } from "child_process";
7012
7042
  import { fileURLToPath } from "url";
7013
7043
  import TextInput4 from "ink-text-input";
@@ -8096,7 +8126,7 @@ ${hintText}`, color: "magenta" }];
8096
8126
  }
8097
8127
  case "/export": {
8098
8128
  const exportFile = `export-fluxflow-${chatId}.txt`;
8099
- const exportPath = path16.join(process.cwd(), exportFile);
8129
+ const exportPath = path17.join(process.cwd(), exportFile);
8100
8130
  const exportLines = [];
8101
8131
  let insideAgentBlock = false;
8102
8132
  for (let i = 0; i < messages.length; i++) {
@@ -8257,7 +8287,7 @@ ${list || "No saved chats found."}`, isMeta: true }];
8257
8287
  # SKILLS & WORKFLOWS
8258
8288
  - [Define custom step-by-step recipes for this project here]
8259
8289
  `;
8260
- const filePath = path16.join(process.cwd(), "FluxFlow.md");
8290
+ const filePath = path17.join(process.cwd(), "FluxFlow.md");
8261
8291
  if (fs18.pathExistsSync(filePath)) {
8262
8292
  setMessages((prev) => {
8263
8293
  setCompletedIndex(prev.length + 1);
@@ -9409,7 +9439,7 @@ var init_app = __esm({
9409
9439
  CHANGELOG_URL = "https://fluxflow-cli.onrender.com/changelog.html";
9410
9440
  linesAdded = 0;
9411
9441
  linesRemoved = 0;
9412
- packageJsonPath = path16.join(path16.dirname(fileURLToPath(import.meta.url)), "../package.json");
9442
+ packageJsonPath = path17.join(path17.dirname(fileURLToPath(import.meta.url)), "../package.json");
9413
9443
  packageJson = JSON.parse(fs18.readFileSync(packageJsonPath, "utf8"));
9414
9444
  versionFluxflow = packageJson.version;
9415
9445
  updatedOn = packageJson.date || "2026-05-20";
@@ -9530,14 +9560,14 @@ var init_app = __esm({
9530
9560
  if (["node_modules", ".git", ".gemini", "dist", "build", ".next", ".cache", "out"].includes(file)) {
9531
9561
  continue;
9532
9562
  }
9533
- const filePath = path16.join(currentDir, file);
9563
+ const filePath = path17.join(currentDir, file);
9534
9564
  const stat = fs18.statSync(filePath);
9535
9565
  if (stat.isDirectory()) {
9536
9566
  scan(filePath);
9537
9567
  } else {
9538
9568
  fileList.push({
9539
9569
  name: file,
9540
- relativePath: path16.relative(process.cwd(), filePath)
9570
+ relativePath: path17.relative(process.cwd(), filePath)
9541
9571
  });
9542
9572
  }
9543
9573
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fluxflow-cli",
3
- "version": "1.18.20",
4
- "date": "2026-06-01",
3
+ "version": "1.18.21",
4
+ "date": "2026-06-02",
5
5
  "description": "A high-fidelity agentic terminal assistant for the Flux Era.",
6
6
  "keywords": [
7
7
  "ai",