@locusai/cli 0.17.3 → 0.17.5

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/bin/locus.js +378 -18
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -1598,7 +1598,8 @@ ${bold("Initializing Locus...")}
1598
1598
  const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
1599
1599
  if (entriesToAdd.length > 0) {
1600
1600
  const newContent = `${gitignoreContent.trimEnd()}
1601
- ${GITIGNORE_ENTRIES.join(`
1601
+
1602
+ ${entriesToAdd.join(`
1602
1603
  `)}
1603
1604
  `;
1604
1605
  writeFileSync4(gitignorePath, newContent, "utf-8");
@@ -1672,8 +1673,8 @@ var init_init = __esm(() => {
1672
1673
  ".locus/sessions/",
1673
1674
  ".locus/logs/",
1674
1675
  ".locus/worktrees/",
1675
- ".locus/artifacts",
1676
- ".locus/discussions"
1676
+ ".locus/artifacts/",
1677
+ ".locus/discussions/"
1677
1678
  ];
1678
1679
  });
1679
1680
 
@@ -5117,7 +5118,7 @@ ${gitLog}
5117
5118
  function buildExecutionRules(config) {
5118
5119
  return `# Execution Rules
5119
5120
 
5120
- 1. **Commit format:** Use conventional commits: \`feat: <title> (#<issue>)\`, \`fix: ...\`, \`chore: ...\`
5121
+ 1. **Commit format:** Use conventional commits: \`feat: <title> (#<issue>)\`, \`fix: ...\`, \`chore: ...\`. Always include a blank line followed by \`Co-Authored-By: LocusAgent <agent@locusai.team>\` as a trailer in every commit message.
5121
5122
  2. **Code quality:** Follow existing code style. Run linters/formatters if available.
5122
5123
  3. **Testing:** If test files exist for modified code, update them accordingly.
5123
5124
  4. **Do NOT:**
@@ -5261,6 +5262,100 @@ class JsonStream {
5261
5262
  }
5262
5263
  }
5263
5264
 
5265
+ // src/display/diff-renderer.ts
5266
+ function renderDiff(diff, options = {}) {
5267
+ const { maxLines, lineNumbers = true } = options;
5268
+ const lines = diff.split(`
5269
+ `);
5270
+ const output = [];
5271
+ let lineCount = 0;
5272
+ let oldLine = 0;
5273
+ let newLine = 0;
5274
+ for (const line of lines) {
5275
+ if (maxLines && lineCount >= maxLines) {
5276
+ const remaining = lines.length - lineCount;
5277
+ if (remaining > 0) {
5278
+ output.push(dim(` ... +${remaining} more lines`));
5279
+ }
5280
+ break;
5281
+ }
5282
+ if (line.startsWith("diff --git")) {
5283
+ const filePath = extractFilePath(line);
5284
+ output.push("");
5285
+ output.push(bold(cyan(`── ${filePath} ──`)));
5286
+ lineCount += 2;
5287
+ continue;
5288
+ }
5289
+ if (line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++")) {
5290
+ continue;
5291
+ }
5292
+ if (line.startsWith("@@")) {
5293
+ const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/);
5294
+ if (match) {
5295
+ oldLine = Number.parseInt(match[1], 10);
5296
+ newLine = Number.parseInt(match[2], 10);
5297
+ const context = match[3] ?? "";
5298
+ output.push(dim(` ${line.slice(0, 60)}${context ? ` ${context.trim()}` : ""}`));
5299
+ lineCount++;
5300
+ }
5301
+ continue;
5302
+ }
5303
+ if (line.startsWith("+")) {
5304
+ const gutter = lineNumbers ? `${dim(padNum(newLine))} ` : "";
5305
+ output.push(`${gutter}${green("+")} ${green(line.slice(1))}`);
5306
+ newLine++;
5307
+ lineCount++;
5308
+ continue;
5309
+ }
5310
+ if (line.startsWith("-")) {
5311
+ const gutter = lineNumbers ? `${dim(padNum(oldLine))} ` : "";
5312
+ output.push(`${gutter}${red("-")} ${red(line.slice(1))}`);
5313
+ oldLine++;
5314
+ lineCount++;
5315
+ continue;
5316
+ }
5317
+ if (line.startsWith(" ")) {
5318
+ const gutter = lineNumbers ? `${dim(padNum(newLine))} ` : "";
5319
+ output.push(`${gutter}${dim("|")} ${dim(line.slice(1))}`);
5320
+ oldLine++;
5321
+ newLine++;
5322
+ lineCount++;
5323
+ continue;
5324
+ }
5325
+ if (line.trim()) {
5326
+ output.push(dim(` ${line}`));
5327
+ lineCount++;
5328
+ }
5329
+ }
5330
+ return output;
5331
+ }
5332
+ function countDiffChanges(diff) {
5333
+ const lines = diff.split(`
5334
+ `);
5335
+ let additions = 0;
5336
+ let deletions = 0;
5337
+ let files = 0;
5338
+ for (const line of lines) {
5339
+ if (line.startsWith("diff --git"))
5340
+ files++;
5341
+ else if (line.startsWith("+") && !line.startsWith("+++"))
5342
+ additions++;
5343
+ else if (line.startsWith("-") && !line.startsWith("---"))
5344
+ deletions++;
5345
+ }
5346
+ return { additions, deletions, files };
5347
+ }
5348
+ function extractFilePath(diffLine) {
5349
+ const match = diffLine.match(/diff --git a\/(.+) b\//);
5350
+ return match?.[1] ?? diffLine;
5351
+ }
5352
+ function padNum(n) {
5353
+ return String(n).padStart(4);
5354
+ }
5355
+ var init_diff_renderer = __esm(() => {
5356
+ init_terminal();
5357
+ });
5358
+
5264
5359
  // src/repl/commands.ts
5265
5360
  import { execSync as execSync7 } from "node:child_process";
5266
5361
  function getSlashCommands() {
@@ -5431,12 +5526,30 @@ function cmdDiff(_args, ctx) {
5431
5526
  encoding: "utf-8",
5432
5527
  stdio: ["pipe", "pipe", "pipe"]
5433
5528
  });
5434
- if (diff.trim()) {
5435
- process.stdout.write(diff);
5436
- } else {
5529
+ if (!diff.trim()) {
5437
5530
  process.stderr.write(`${dim("No changes.")}
5531
+ `);
5532
+ return;
5533
+ }
5534
+ const { additions, deletions, files } = countDiffChanges(diff);
5535
+ const filesLabel = files === 1 ? "file" : "files";
5536
+ process.stderr.write(`
5537
+ `);
5538
+ process.stderr.write(`${bold("Changes:")} ${cyan(`${files} ${filesLabel}`)} ${green(`+${additions}`)} ${red(`-${deletions}`)}
5539
+ `);
5540
+ process.stderr.write(`${dim("─".repeat(60))}
5541
+ `);
5542
+ const lines = renderDiff(diff);
5543
+ for (const line of lines) {
5544
+ process.stderr.write(`${line}
5438
5545
  `);
5439
5546
  }
5547
+ process.stderr.write(`${dim("─".repeat(60))}
5548
+ `);
5549
+ process.stderr.write(dim(`${yellow("tip:")} use ${cyan("/undo")} to revert all unstaged changes
5550
+ `));
5551
+ process.stderr.write(`
5552
+ `);
5440
5553
  } catch {
5441
5554
  process.stderr.write(`${red("✗")} Could not get diff.
5442
5555
  `);
@@ -5476,6 +5589,7 @@ function cmdExit(_args, ctx) {
5476
5589
  }
5477
5590
  var init_commands = __esm(() => {
5478
5591
  init_ai_models();
5592
+ init_diff_renderer();
5479
5593
  init_terminal();
5480
5594
  });
5481
5595
 
@@ -7384,7 +7498,9 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
7384
7498
  encoding: "utf-8",
7385
7499
  stdio: ["pipe", "pipe", "pipe"]
7386
7500
  });
7387
- const message = `chore: complete #${issueNumber} - ${issueTitle}`;
7501
+ const message = `chore: complete #${issueNumber} - ${issueTitle}
7502
+
7503
+ Co-Authored-By: LocusAgent <agent@locusai.team>`;
7388
7504
  execSync12(`git commit -m ${JSON.stringify(message)}`, {
7389
7505
  cwd: projectRoot,
7390
7506
  encoding: "utf-8",
@@ -8416,6 +8532,7 @@ import {
8416
8532
  writeFileSync as writeFileSync8
8417
8533
  } from "node:fs";
8418
8534
  import { join as join16 } from "node:path";
8535
+ import { createInterface as createInterface2 } from "node:readline";
8419
8536
  function printHelp4() {
8420
8537
  process.stderr.write(`
8421
8538
  ${bold("locus discuss")} — AI-powered architectural discussions
@@ -8448,7 +8565,7 @@ function generateId() {
8448
8565
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8449
8566
  }
8450
8567
  async function discussCommand(projectRoot, args, flags = {}) {
8451
- if (args[0] === "help" || args.length === 0) {
8568
+ if (args[0] === "help") {
8452
8569
  printHelp4();
8453
8570
  return;
8454
8571
  }
@@ -8462,6 +8579,14 @@ async function discussCommand(projectRoot, args, flags = {}) {
8462
8579
  if (subcommand === "delete") {
8463
8580
  return deleteDiscussion(projectRoot, args[1]);
8464
8581
  }
8582
+ if (args.length === 0) {
8583
+ const topic2 = await promptForTopic();
8584
+ if (!topic2) {
8585
+ printHelp4();
8586
+ return;
8587
+ }
8588
+ return startDiscussion(projectRoot, topic2, flags);
8589
+ }
8465
8590
  const topic = args.join(" ").trim();
8466
8591
  return startDiscussion(projectRoot, topic, flags);
8467
8592
  }
@@ -8541,6 +8666,21 @@ function deleteDiscussion(projectRoot, id) {
8541
8666
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
8542
8667
  `);
8543
8668
  }
8669
+ async function promptForTopic() {
8670
+ return new Promise((resolve2) => {
8671
+ const rl = createInterface2({
8672
+ input: process.stdin,
8673
+ output: process.stderr,
8674
+ terminal: true
8675
+ });
8676
+ process.stderr.write(`${bold("Discussion topic:")} `);
8677
+ rl.once("line", (line) => {
8678
+ rl.close();
8679
+ resolve2(line.trim());
8680
+ });
8681
+ rl.once("close", () => resolve2(""));
8682
+ });
8683
+ }
8544
8684
  async function startDiscussion(projectRoot, topic, flags) {
8545
8685
  const config = loadConfig(projectRoot);
8546
8686
  const timer = createTimer();
@@ -8630,23 +8770,236 @@ var init_discuss = __esm(() => {
8630
8770
  init_terminal();
8631
8771
  });
8632
8772
 
8773
+ // src/commands/artifacts.ts
8774
+ var exports_artifacts = {};
8775
+ __export(exports_artifacts, {
8776
+ readArtifact: () => readArtifact,
8777
+ listArtifacts: () => listArtifacts,
8778
+ formatSize: () => formatSize,
8779
+ formatDate: () => formatDate2,
8780
+ artifactsCommand: () => artifactsCommand
8781
+ });
8782
+ import { existsSync as existsSync16, readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
8783
+ import { join as join17 } from "node:path";
8784
+ function printHelp5() {
8785
+ process.stderr.write(`
8786
+ ${bold("locus artifacts")} — View and manage AI-generated artifacts
8787
+
8788
+ ${bold("Usage:")}
8789
+ locus artifacts ${dim("# List all artifacts")}
8790
+ locus artifacts show <name> ${dim("# Show artifact content")}
8791
+ locus artifacts plan <name> ${dim("# Convert artifact to a plan")}
8792
+
8793
+ ${bold("Examples:")}
8794
+ locus artifacts
8795
+ locus artifacts show reduce-cli-terminal-output
8796
+ locus artifacts plan aws-instance-orchestration-prd
8797
+
8798
+ ${dim("Artifact names support partial matching.")}
8799
+
8800
+ `);
8801
+ }
8802
+ function getArtifactsDir(projectRoot) {
8803
+ return join17(projectRoot, ".locus", "artifacts");
8804
+ }
8805
+ function listArtifacts(projectRoot) {
8806
+ const dir = getArtifactsDir(projectRoot);
8807
+ if (!existsSync16(dir))
8808
+ return [];
8809
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
8810
+ const filePath = join17(dir, fileName);
8811
+ const stat = statSync4(filePath);
8812
+ return {
8813
+ name: fileName.replace(/\.md$/, ""),
8814
+ fileName,
8815
+ createdAt: stat.birthtime,
8816
+ size: stat.size
8817
+ };
8818
+ }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
8819
+ }
8820
+ function readArtifact(projectRoot, name) {
8821
+ const dir = getArtifactsDir(projectRoot);
8822
+ const fileName = name.endsWith(".md") ? name : `${name}.md`;
8823
+ const filePath = join17(dir, fileName);
8824
+ if (!existsSync16(filePath))
8825
+ return null;
8826
+ const stat = statSync4(filePath);
8827
+ return {
8828
+ content: readFileSync13(filePath, "utf-8"),
8829
+ info: {
8830
+ name: fileName.replace(/\.md$/, ""),
8831
+ fileName,
8832
+ createdAt: stat.birthtime,
8833
+ size: stat.size
8834
+ }
8835
+ };
8836
+ }
8837
+ function formatSize(bytes) {
8838
+ if (bytes < 1024)
8839
+ return `${bytes}B`;
8840
+ const kb = bytes / 1024;
8841
+ if (kb < 1024)
8842
+ return `${kb.toFixed(1)}KB`;
8843
+ return `${(kb / 1024).toFixed(1)}MB`;
8844
+ }
8845
+ function formatDate2(date) {
8846
+ return date.toLocaleDateString("en-US", {
8847
+ year: "numeric",
8848
+ month: "short",
8849
+ day: "numeric",
8850
+ hour: "2-digit",
8851
+ minute: "2-digit"
8852
+ });
8853
+ }
8854
+ async function artifactsCommand(projectRoot, args) {
8855
+ if (args[0] === "help") {
8856
+ printHelp5();
8857
+ return;
8858
+ }
8859
+ const subcommand = args[0];
8860
+ switch (subcommand) {
8861
+ case "show":
8862
+ case "view": {
8863
+ const name = args.slice(1).join(" ").trim();
8864
+ if (!name) {
8865
+ process.stderr.write(`${red("✗")} Please provide an artifact name.
8866
+ `);
8867
+ process.stderr.write(` Usage: ${bold("locus artifacts show <name>")}
8868
+ `);
8869
+ return;
8870
+ }
8871
+ showArtifact(projectRoot, name);
8872
+ break;
8873
+ }
8874
+ case "plan": {
8875
+ const name = args.slice(1).join(" ").trim();
8876
+ if (!name) {
8877
+ process.stderr.write(`${red("✗")} Please provide an artifact name.
8878
+ `);
8879
+ process.stderr.write(` Usage: ${bold("locus artifacts plan <name>")}
8880
+ `);
8881
+ return;
8882
+ }
8883
+ await convertToPlan(projectRoot, name);
8884
+ break;
8885
+ }
8886
+ default:
8887
+ listArtifactsCommand(projectRoot);
8888
+ break;
8889
+ }
8890
+ }
8891
+ function listArtifactsCommand(projectRoot) {
8892
+ const artifacts = listArtifacts(projectRoot);
8893
+ if (artifacts.length === 0) {
8894
+ process.stderr.write(`${dim("No artifacts found.")}
8895
+ `);
8896
+ return;
8897
+ }
8898
+ process.stderr.write(`
8899
+ ${bold("Artifacts")} ${dim(`(${artifacts.length} total)`)}
8900
+
8901
+ `);
8902
+ for (let i = 0;i < artifacts.length; i++) {
8903
+ const a = artifacts[i];
8904
+ const idx = dim(`${String(i + 1).padStart(2)}.`);
8905
+ process.stderr.write(` ${idx} ${cyan(a.name)}
8906
+ `);
8907
+ process.stderr.write(` ${dim(`${formatDate2(a.createdAt)} • ${formatSize(a.size)}`)}
8908
+ `);
8909
+ }
8910
+ process.stderr.write(`
8911
+ ${dim("Use")} ${bold("locus artifacts show <name>")} ${dim("to view content")}
8912
+ `);
8913
+ process.stderr.write(` ${dim("Use")} ${bold("locus artifacts plan <name>")} ${dim("to convert to a plan")}
8914
+
8915
+ `);
8916
+ }
8917
+ function showArtifact(projectRoot, name) {
8918
+ const result = readArtifact(projectRoot, name);
8919
+ if (!result) {
8920
+ const artifacts = listArtifacts(projectRoot);
8921
+ const matches = artifacts.filter((a) => a.name.toLowerCase().includes(name.toLowerCase()));
8922
+ if (matches.length === 1) {
8923
+ const match = readArtifact(projectRoot, matches[0].name);
8924
+ if (match) {
8925
+ printArtifact(match.info, match.content);
8926
+ return;
8927
+ }
8928
+ }
8929
+ if (matches.length > 1) {
8930
+ process.stderr.write(`${red("✗")} Multiple artifacts match "${name}":
8931
+ `);
8932
+ for (const m of matches) {
8933
+ process.stderr.write(` ${cyan(m.name)}
8934
+ `);
8935
+ }
8936
+ return;
8937
+ }
8938
+ process.stderr.write(`${red("✗")} Artifact "${name}" not found.
8939
+ `);
8940
+ process.stderr.write(` Run ${bold("locus artifacts")} to see available artifacts.
8941
+ `);
8942
+ return;
8943
+ }
8944
+ printArtifact(result.info, result.content);
8945
+ }
8946
+ function printArtifact(info, content) {
8947
+ const line = dim("─".repeat(50));
8948
+ process.stderr.write(`
8949
+ ${bold(info.name)}
8950
+ ${dim(`${formatDate2(info.createdAt)} • ${formatSize(info.size)}`)}
8951
+ ${line}
8952
+
8953
+ `);
8954
+ process.stdout.write(`${content}
8955
+ `);
8956
+ }
8957
+ async function convertToPlan(projectRoot, name) {
8958
+ const result = readArtifact(projectRoot, name);
8959
+ if (!result) {
8960
+ const artifacts = listArtifacts(projectRoot);
8961
+ const matches = artifacts.filter((a) => a.name.toLowerCase().includes(name.toLowerCase()));
8962
+ if (matches.length === 1) {
8963
+ await runPlanConversion(projectRoot, matches[0].name);
8964
+ return;
8965
+ }
8966
+ process.stderr.write(`${red("✗")} Artifact "${name}" not found.
8967
+ `);
8968
+ process.stderr.write(` Run ${bold("locus artifacts")} to see available artifacts.
8969
+ `);
8970
+ return;
8971
+ }
8972
+ await runPlanConversion(projectRoot, result.info.name);
8973
+ }
8974
+ async function runPlanConversion(projectRoot, artifactName) {
8975
+ const { execCommand: execCommand2 } = await Promise.resolve().then(() => (init_exec(), exports_exec));
8976
+ process.stderr.write(`
8977
+ ${bold("Converting artifact to plan:")} ${cyan(artifactName)}
8978
+
8979
+ `);
8980
+ await execCommand2(projectRoot, [`Create a plan according to ${artifactName}`], {});
8981
+ }
8982
+ var init_artifacts = __esm(() => {
8983
+ init_terminal();
8984
+ });
8985
+
8633
8986
  // src/cli.ts
8634
8987
  init_config();
8635
8988
  init_context();
8636
8989
  init_logger();
8637
8990
  init_rate_limiter();
8638
8991
  init_terminal();
8639
- import { join as join17 } from "node:path";
8640
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
8992
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
8993
+ import { join as join18 } from "node:path";
8641
8994
  import { fileURLToPath } from "node:url";
8642
8995
  function getCliVersion() {
8643
8996
  const fallbackVersion = "0.0.0";
8644
- const packageJsonPath = join17(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
8645
- if (!existsSync16(packageJsonPath)) {
8997
+ const packageJsonPath = join18(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
8998
+ if (!existsSync17(packageJsonPath)) {
8646
8999
  return fallbackVersion;
8647
9000
  }
8648
9001
  try {
8649
- const parsed = JSON.parse(readFileSync13(packageJsonPath, "utf-8"));
9002
+ const parsed = JSON.parse(readFileSync14(packageJsonPath, "utf-8"));
8650
9003
  return parsed.version ?? fallbackVersion;
8651
9004
  } catch {
8652
9005
  return fallbackVersion;
@@ -8733,7 +9086,7 @@ function parseArgs(argv) {
8733
9086
  const args = positional.slice(1);
8734
9087
  return { command, args, flags };
8735
9088
  }
8736
- function printHelp5() {
9089
+ function printHelp6() {
8737
9090
  process.stderr.write(`
8738
9091
  ${bold("Locus")} ${dim(`v${VERSION}`)} — GitHub-native AI engineering assistant
8739
9092
 
@@ -8750,6 +9103,7 @@ ${bold("Commands:")}
8750
9103
  ${cyan("review")} AI-powered code review on PRs
8751
9104
  ${cyan("iterate")} Re-execute tasks with PR feedback
8752
9105
  ${cyan("discuss")} AI-powered architectural discussions
9106
+ ${cyan("artifacts")} View and manage AI-generated artifacts
8753
9107
  ${cyan("status")} Dashboard view of current state
8754
9108
  ${cyan("config")} View and manage settings
8755
9109
  ${cyan("logs")} View, tail, and manage execution logs
@@ -8786,7 +9140,7 @@ async function main() {
8786
9140
  process.exit(0);
8787
9141
  }
8788
9142
  if (parsed.flags.help && !parsed.command) {
8789
- printHelp5();
9143
+ printHelp6();
8790
9144
  process.exit(0);
8791
9145
  }
8792
9146
  const command = resolveAlias(parsed.command);
@@ -8797,7 +9151,7 @@ async function main() {
8797
9151
  try {
8798
9152
  const root = getGitRoot(cwd);
8799
9153
  if (isInitialized(root)) {
8800
- logDir = join17(root, ".locus", "logs");
9154
+ logDir = join18(root, ".locus", "logs");
8801
9155
  getRateLimiter(root);
8802
9156
  }
8803
9157
  } catch {}
@@ -8818,7 +9172,7 @@ async function main() {
8818
9172
  printVersionNotice = startVersionCheck2(VERSION);
8819
9173
  }
8820
9174
  if (!command) {
8821
- printHelp5();
9175
+ printHelp6();
8822
9176
  process.exit(0);
8823
9177
  }
8824
9178
  if (command === "init") {
@@ -8931,6 +9285,12 @@ async function main() {
8931
9285
  });
8932
9286
  break;
8933
9287
  }
9288
+ case "artifacts": {
9289
+ const { artifactsCommand: artifactsCommand2 } = await Promise.resolve().then(() => (init_artifacts(), exports_artifacts));
9290
+ const artifactsArgs = parsed.flags.help ? ["help"] : parsed.args;
9291
+ await artifactsCommand2(projectRoot, artifactsArgs);
9292
+ break;
9293
+ }
8934
9294
  case "upgrade": {
8935
9295
  const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
8936
9296
  await upgradeCommand2(projectRoot, parsed.args, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {