@amistio/cli 0.1.2 → 0.1.4

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/index.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spawn as spawn3 } from "node:child_process";
5
- import { createHash as createHash3, randomUUID } from "node:crypto";
4
+ import { createHash as createHash4, randomUUID } from "node:crypto";
6
5
  import { writeFile as writeFile8 } from "node:fs/promises";
7
6
  import os5 from "node:os";
8
- import path9 from "node:path";
7
+ import path11 from "node:path";
9
8
  import { Command } from "commander";
10
9
 
11
10
  // ../shared/src/schemas.ts
@@ -86,9 +85,12 @@ var runnerToolNameSchema = z.enum(runnerToolNames);
86
85
  var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
87
86
  var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
88
87
  var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
89
- var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "custom", "none"]);
88
+ var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "channelUnsupported", "custom", "none"]);
89
+ var runnerInvocationChannelSchema = z.enum(["auto", "sdk", "command"]);
90
+ var runnerEffectiveInvocationChannelSchema = z.enum(["sdk", "command"]);
90
91
  var runnerToolModelPreferenceSchema = z.object({
91
92
  tool: runnerToolSelectionSchema.optional(),
93
+ invocationChannel: runnerInvocationChannelSchema.optional(),
92
94
  model: z.string().trim().min(1).max(160).optional()
93
95
  });
94
96
  var runnerToolCapabilitySchema = z.object({
@@ -104,6 +106,7 @@ var runnerToolCapabilitySchema = z.object({
104
106
  });
105
107
  var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
106
108
  var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
109
+ var projectStatusSchema = z.enum(["active", "archived"]);
107
110
  var baseItemSchema = z.object({
108
111
  id: z.string().min(1),
109
112
  type: itemTypeSchema,
@@ -130,7 +133,11 @@ var projectItemSchema = baseItemSchema.extend({
130
133
  projectId: z.string().min(1),
131
134
  name: z.string().min(1),
132
135
  slug: z.string().min(1),
133
- description: z.string().optional()
136
+ description: z.string().optional(),
137
+ status: projectStatusSchema.default("active"),
138
+ archivedAt: isoDateTimeSchema.optional(),
139
+ archivedByUserId: z.string().min(1).optional(),
140
+ archiveReason: z.string().min(1).optional()
134
141
  });
135
142
  var repositoryLinkItemSchema = baseItemSchema.extend({
136
143
  type: z.literal("repositoryLink"),
@@ -229,14 +236,16 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
229
236
  projectId: z.string().min(1),
230
237
  runnerId: z.string().min(1),
231
238
  repositoryLinkId: z.string().min(1),
232
- status: z.enum(["online", "offline", "running", "blocked"]),
239
+ status: z.enum(["online", "offline", "running", "blocked", "removed"]),
233
240
  version: z.string().optional(),
234
241
  mode: z.enum(["foreground", "background"]).optional(),
235
242
  hostname: z.string().min(1).optional(),
236
243
  runnerName: z.string().min(1).optional(),
237
244
  capabilities: z.array(runnerToolCapabilitySchema).optional(),
238
245
  requestedTool: runnerToolSelectionSchema.optional(),
246
+ requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
239
247
  effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
248
+ effectiveInvocationChannel: runnerEffectiveInvocationChannelSchema.optional(),
240
249
  effectiveModel: z.string().min(1).optional(),
241
250
  preferenceSource: runnerPreferenceSourceSchema.optional(),
242
251
  preferenceStatus: runnerPreferenceStatusSchema.optional(),
@@ -717,7 +726,7 @@ function runnerWaitAction(readiness, fallbackTitle) {
717
726
  }
718
727
  function getSharedRunnerReadiness(repositoryLinks, runnerHeartbeats, nowMs) {
719
728
  const repositoryLinkIds = new Set(repositoryLinks.map((link) => link.repositoryLinkId));
720
- const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId)).sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
729
+ const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId) && heartbeat.status !== "removed").sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
721
730
  const repositoryLink = latestHeartbeat ? repositoryLinks.find((link) => link.repositoryLinkId === latestHeartbeat.repositoryLinkId) ?? repositoryLinks[0] : repositoryLinks[0];
722
731
  if (!latestHeartbeat) {
723
732
  return { ready: false, reason: "runnerMissing", ...repositoryLink ? { repositoryLink } : {} };
@@ -992,6 +1001,22 @@ var ApiClient = class {
992
1001
  }
993
1002
  );
994
1003
  }
1004
+ async importPairingSession(input) {
1005
+ return this.request(
1006
+ "pairing-sessions",
1007
+ z3.object({
1008
+ accountId: z3.string().min(1),
1009
+ projectId: z3.string().min(1),
1010
+ repositoryLink: repositoryLinkItemSchema,
1011
+ repositoryLinkAction: z3.enum(["created", "reused"]),
1012
+ token: z3.string().min(1)
1013
+ }),
1014
+ {
1015
+ method: "PATCH",
1016
+ body: JSON.stringify(input)
1017
+ }
1018
+ );
1019
+ }
995
1020
  async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300) {
996
1021
  return this.request(
997
1022
  `/projects/${projectId}/work-items/claim`,
@@ -1066,6 +1091,7 @@ var ApiClient = class {
1066
1091
  project: runnerSettingsItemSchema.optional(),
1067
1092
  effective: z3.object({
1068
1093
  tool: runnerToolSelectionSchema,
1094
+ invocationChannel: runnerInvocationChannelSchema,
1069
1095
  model: z3.string().optional(),
1070
1096
  source: runnerPreferenceSourceSchema
1071
1097
  })
@@ -1141,7 +1167,7 @@ var ApiClient = class {
1141
1167
  ...init,
1142
1168
  headers: {
1143
1169
  "content-type": "application/json",
1144
- "x-amistio-account-id": this.options.accountId,
1170
+ ...this.options.accountId ? { "x-amistio-account-id": this.options.accountId } : {},
1145
1171
  ...this.options.token ? { authorization: `Bearer ${this.options.token}` } : {}
1146
1172
  }
1147
1173
  });
@@ -1178,8 +1204,8 @@ var toolSessionMutationSchema = z3.object({
1178
1204
  });
1179
1205
  function resolveApiUrl(apiUrl, urlPath) {
1180
1206
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
1181
- const path10 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1182
- return new URL(`${base}${path10}`);
1207
+ const path12 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1208
+ return new URL(`${base}${path12}`);
1183
1209
  }
1184
1210
 
1185
1211
  // src/orchestrator.ts
@@ -1375,6 +1401,7 @@ async function runLocalTool(options) {
1375
1401
  prompt: options.prompt,
1376
1402
  promptFilePath,
1377
1403
  tool: options.tool ?? "auto",
1404
+ invocationChannel: options.invocationChannel ?? "auto",
1378
1405
  ...options.model ? { model: options.model } : {}
1379
1406
  };
1380
1407
  if (options.toolCommand) {
@@ -1407,6 +1434,7 @@ async function createToolRunPreview(options) {
1407
1434
  prompt: options.prompt,
1408
1435
  promptFilePath,
1409
1436
  tool: options.tool ?? "auto",
1437
+ invocationChannel: options.invocationChannel ?? "auto",
1410
1438
  ...options.model ? { model: options.model } : {}
1411
1439
  };
1412
1440
  if (options.toolCommand) {
@@ -1443,25 +1471,32 @@ async function createToolRunner(options) {
1443
1471
  if (tool === "none") {
1444
1472
  throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
1445
1473
  }
1446
- const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model)) : await selectRequestedAdapter(tool);
1474
+ const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model), options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
1447
1475
  if (options.model && !adapter.supportsModelSelection) {
1448
1476
  throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
1449
1477
  }
1450
- if (adapter.runWithSdk && await isSdkAvailable(adapter)) {
1478
+ if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
1451
1479
  return {
1452
1480
  toolName: adapter.name,
1453
1481
  kind: "sdk",
1454
1482
  displayCommand: adapter.sdkDisplayCommand ?? `${adapter.name} SDK`,
1455
- adapter
1483
+ adapter,
1484
+ allowCommandFallback: options.invocationChannel === "auto"
1456
1485
  };
1457
1486
  }
1458
- if (adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
1487
+ if (options.invocationChannel !== "sdk" && adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
1459
1488
  return {
1460
1489
  toolName: adapter.name,
1461
1490
  kind: "command",
1462
1491
  invocation: adapter.buildInvocation(options)
1463
1492
  };
1464
1493
  }
1494
+ if (options.invocationChannel === "sdk") {
1495
+ throw new Error(`The ${adapter.name} SDK was not found. Select Auto or Command invocation, install the SDK/runtime, or pass --tool-command locally.`);
1496
+ }
1497
+ if (options.invocationChannel === "command") {
1498
+ throw new Error(`The ${adapter.name} executable was not found. Select Auto or SDK invocation, install the command, or pass --tool-command locally.`);
1499
+ }
1465
1500
  throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
1466
1501
  }
1467
1502
  async function executeToolRunner(runner2, input) {
@@ -1471,7 +1506,7 @@ async function executeToolRunner(runner2, input) {
1471
1506
  try {
1472
1507
  return await runner2.adapter.runWithSdk(input);
1473
1508
  } catch (error) {
1474
- if (runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
1509
+ if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
1475
1510
  const fallback = runner2.adapter.buildInvocation(input);
1476
1511
  const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
1477
1512
  const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
@@ -1490,34 +1525,53 @@ function normalizeToolRequest(value) {
1490
1525
  }
1491
1526
  throw new Error(`Unsupported local tool: ${value}. Supported tools: auto, none, ${localToolNames.join(", ")}.`);
1492
1527
  }
1493
- async function selectFirstAvailableAdapter(requiresModelSelection = false) {
1528
+ async function selectFirstAvailableAdapter(requiresModelSelection = false, invocationChannel = "auto") {
1494
1529
  for (const adapter of localToolAdapters) {
1495
1530
  if (requiresModelSelection && !adapter.supportsModelSelection) {
1496
1531
  continue;
1497
1532
  }
1498
1533
  const sdkAvailable = await isSdkAvailable(adapter);
1499
1534
  const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
1500
- if (sdkAvailable || commandAvailable) {
1535
+ if (supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
1501
1536
  return adapter;
1502
1537
  }
1503
1538
  }
1539
+ if (invocationChannel !== "auto") {
1540
+ throw new Error(`No installed local AI tool supports ${invocationChannel} invocation${requiresModelSelection ? " with model selection" : ""}. Select Auto or install a compatible tool.`);
1541
+ }
1504
1542
  throw new Error(
1505
1543
  requiresModelSelection ? "No installed local AI tool supports model selection. Remove --model or choose a model-aware adapter." : `No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
1506
1544
  );
1507
1545
  }
1508
- async function selectRequestedAdapter(tool) {
1546
+ async function selectRequestedAdapter(tool, invocationChannel = "auto") {
1509
1547
  const adapter = localToolAdapters.find((candidate) => candidate.name === tool);
1510
1548
  if (!adapter) {
1511
1549
  throw new Error(`Unsupported local tool: ${tool}.`);
1512
1550
  }
1513
- if (await isSdkAvailable(adapter)) {
1551
+ const sdkAvailable = await isSdkAvailable(adapter);
1552
+ const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
1553
+ if (!supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
1554
+ if (invocationChannel === "sdk") {
1555
+ throw new Error(`The ${tool} SDK was not found. Select Auto or Command invocation, install it, or pass --tool-command locally.`);
1556
+ }
1557
+ if (invocationChannel === "command") {
1558
+ throw new Error(`The ${tool} executable was not found. Select Auto or SDK invocation, install it, or pass --tool-command locally.`);
1559
+ }
1560
+ throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
1561
+ }
1562
+ if (invocationChannel !== "command" && sdkAvailable) {
1514
1563
  return adapter;
1515
1564
  }
1516
- if (adapter.executable && await commandExists(adapter.executable)) {
1565
+ if (invocationChannel !== "sdk" && commandAvailable) {
1517
1566
  return adapter;
1518
1567
  }
1519
1568
  throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
1520
1569
  }
1570
+ function supportsRequestedInvocationChannel(capability, invocationChannel) {
1571
+ if (invocationChannel === "auto") return capability.sdkAvailable || capability.commandAvailable;
1572
+ if (invocationChannel === "sdk") return capability.sdkAvailable;
1573
+ return capability.commandAvailable;
1574
+ }
1521
1575
  async function isSdkAvailable(adapter) {
1522
1576
  if (!adapter.sdkPackageName || !adapter.runWithSdk) {
1523
1577
  return false;
@@ -2484,12 +2538,354 @@ function watchStateKey(action) {
2484
2538
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
2485
2539
  }
2486
2540
 
2541
+ // src/importer.ts
2542
+ import { execFile as execFile2 } from "node:child_process";
2543
+ import { createHash as createHash3 } from "node:crypto";
2544
+ import { readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
2545
+ import path9 from "node:path";
2546
+ import { promisify as promisify2 } from "node:util";
2547
+ var execFileAsync2 = promisify2(execFile2);
2548
+ var defaultMaxFileKb = 256;
2549
+ var controlPlaneRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
2550
+ var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
2551
+ var excludedFileNames = /* @__PURE__ */ new Set(["context/amistio-project.md"]);
2552
+ var generatedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
2553
+ var documentFolderByType = {
2554
+ architecture: "architecture",
2555
+ context: "context",
2556
+ decision: "decisions",
2557
+ feature: "features",
2558
+ memory: "memory",
2559
+ plan: "plans",
2560
+ prompt: "prompts/shared",
2561
+ workflow: "workflows"
2562
+ };
2563
+ async function inspectLocalRepository(rootDir, defaultBranch) {
2564
+ const requestedRoot = path9.resolve(rootDir);
2565
+ const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
2566
+ const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
2567
+ const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
2568
+ const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
2569
+ const repoName = (parsedCloneUrl?.repoName ?? path9.basename(root)) || "repository";
2570
+ const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
2571
+ return {
2572
+ rootDir: root,
2573
+ repoName,
2574
+ defaultBranch: detectedBranch || defaultBranch,
2575
+ repoFingerprint: `import_${hashText(fingerprintSeed, 24)}`,
2576
+ ...parsedCloneUrl ? { parsedCloneUrl } : {},
2577
+ ...!parsedCloneUrl && originUrl ? { originRemoteWarning: "Origin remote is not a supported hosted HTTPS or SSH clone URL, so no clone URL will be stored." } : {}
2578
+ };
2579
+ }
2580
+ async function scanLegacyDocuments(options) {
2581
+ const rootDir = path9.resolve(options.rootDir);
2582
+ const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
2583
+ const skipped = [];
2584
+ const candidates = [];
2585
+ const usedDestinationPaths = /* @__PURE__ */ new Set();
2586
+ const repoPaths = (await listRepositoryPaths(rootDir)).sort((first, second) => first.localeCompare(second));
2587
+ for (const repoPath of repoPaths) {
2588
+ if (!matchesIncludeExclude(repoPath, options.include, options.exclude)) {
2589
+ skipped.push({ repoPath, reason: "excluded" });
2590
+ continue;
2591
+ }
2592
+ if (!isMarkdownDocument(repoPath)) {
2593
+ skipped.push({ repoPath, reason: "notMarkdown" });
2594
+ continue;
2595
+ }
2596
+ if (isExcludedRepoPath(repoPath)) {
2597
+ skipped.push({ repoPath, reason: "excluded" });
2598
+ continue;
2599
+ }
2600
+ const fullPath = path9.join(rootDir, ...repoPath.split("/"));
2601
+ const fileStat = await stat4(fullPath).catch(() => void 0);
2602
+ if (!fileStat?.isFile()) {
2603
+ skipped.push({ repoPath, reason: "unreadable" });
2604
+ continue;
2605
+ }
2606
+ if (fileStat.size > maxBytes) {
2607
+ skipped.push({ repoPath, reason: "tooLarge" });
2608
+ continue;
2609
+ }
2610
+ const content = await readFile6(fullPath, "utf8").catch(() => void 0);
2611
+ if (content === void 0) {
2612
+ skipped.push({ repoPath, reason: "unreadable" });
2613
+ continue;
2614
+ }
2615
+ if (isAmistioManagedMarkdown(content)) {
2616
+ skipped.push({ repoPath, reason: "alreadyManaged" });
2617
+ continue;
2618
+ }
2619
+ const documentType = classifyLegacyDocument(repoPath, content);
2620
+ const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType), repoPath, usedDestinationPaths);
2621
+ candidates.push({
2622
+ sourcePath: repoPath,
2623
+ repoPath: destinationPath,
2624
+ documentType,
2625
+ title: inferTitle2(content, repoPath),
2626
+ content,
2627
+ contentHash: sha256ContentHash(content)
2628
+ });
2629
+ }
2630
+ return { candidates, skipped };
2631
+ }
2632
+ function buildImportedBrainDocuments(options) {
2633
+ const importedAt = options.importedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2634
+ const existingById = new Map((options.existingDocuments ?? []).map((document) => [document.documentId, document]));
2635
+ return options.candidates.map((candidate) => {
2636
+ const documentId = stableImportDocumentId(options.accountId, options.projectId, options.repositoryLinkId, candidate.sourcePath);
2637
+ const existing = existingById.get(documentId);
2638
+ const revision = existing ? existing.contentHash === candidate.contentHash ? existing.revision : existing.revision + 1 : 0;
2639
+ return {
2640
+ id: documentId,
2641
+ type: "brainDocument",
2642
+ schemaVersion: 1,
2643
+ accountId: options.accountId,
2644
+ projectId: options.projectId,
2645
+ documentId,
2646
+ documentType: candidate.documentType,
2647
+ title: candidate.title,
2648
+ status: "reviewing",
2649
+ repoPath: candidate.repoPath,
2650
+ content: candidate.content,
2651
+ contentHash: candidate.contentHash,
2652
+ frontmatter: {
2653
+ ...existing?.frontmatter ?? {},
2654
+ legacySourcePath: candidate.sourcePath,
2655
+ importedByCommand: "amistio import",
2656
+ importedAt,
2657
+ importedSourceHash: candidate.contentHash
2658
+ },
2659
+ revision,
2660
+ source: "repo",
2661
+ syncState: "dirtyInRepo",
2662
+ createdAt: existing?.createdAt ?? importedAt,
2663
+ updatedAt: importedAt,
2664
+ ...existing?.approvedRevision !== void 0 ? { approvedRevision: existing.approvedRevision } : {}
2665
+ };
2666
+ });
2667
+ }
2668
+ function importSkipCounts(skipped) {
2669
+ return {
2670
+ notMarkdown: skipped.filter((item) => item.reason === "notMarkdown").length,
2671
+ excluded: skipped.filter((item) => item.reason === "excluded").length,
2672
+ tooLarge: skipped.filter((item) => item.reason === "tooLarge").length,
2673
+ alreadyManaged: skipped.filter((item) => item.reason === "alreadyManaged").length,
2674
+ unreadable: skipped.filter((item) => item.reason === "unreadable").length
2675
+ };
2676
+ }
2677
+ function parseOptionalOriginCloneUrl(originUrl) {
2678
+ try {
2679
+ return parseRepositoryCloneUrl(originUrl);
2680
+ } catch (error) {
2681
+ const message = error instanceof Error ? error.message : String(error);
2682
+ if (message.toLowerCase().includes("credential") || message.toLowerCase().includes("password")) {
2683
+ throw new Error("Repository origin remote contains embedded credentials. Remove credentials from the remote URL before importing.");
2684
+ }
2685
+ return void 0;
2686
+ }
2687
+ }
2688
+ async function listRepositoryPaths(rootDir) {
2689
+ const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
2690
+ if (gitFiles !== void 0) {
2691
+ return gitFiles.split("\n").map((line) => normalizeRepoPath2(line)).filter((line) => line.length > 0);
2692
+ }
2693
+ const files = [];
2694
+ await walkRepository(rootDir, rootDir, files);
2695
+ return files;
2696
+ }
2697
+ async function walkRepository(rootDir, directory, files) {
2698
+ const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
2699
+ for (const entry of entries) {
2700
+ const fullPath = path9.join(directory, entry.name);
2701
+ const repoPath = normalizeRepoPath2(path9.relative(rootDir, fullPath));
2702
+ if (entry.isDirectory()) {
2703
+ if (!excludedDirectoryNames.has(entry.name)) {
2704
+ await walkRepository(rootDir, fullPath, files);
2705
+ }
2706
+ } else if (entry.isFile() && !isExcludedRepoPath(repoPath)) {
2707
+ files.push(repoPath);
2708
+ }
2709
+ }
2710
+ }
2711
+ function matchesIncludeExclude(repoPath, include, exclude) {
2712
+ if (include?.length && !include.some((pattern) => wildcardMatch(pattern, repoPath))) {
2713
+ return false;
2714
+ }
2715
+ if (exclude?.some((pattern) => wildcardMatch(pattern, repoPath))) {
2716
+ return false;
2717
+ }
2718
+ return true;
2719
+ }
2720
+ function wildcardMatch(pattern, repoPath) {
2721
+ const normalizedPattern = normalizeRepoPath2(pattern);
2722
+ const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "::DOUBLE_STAR_SLASH::").replace(/\*\*/g, "::DOUBLE_STAR::").replace(/\?/g, "[^/]").replace(/\*/g, "[^/]*").replace(/::DOUBLE_STAR_SLASH::/g, "(?:.*/)?").replace(/::DOUBLE_STAR::/g, ".*");
2723
+ return new RegExp(`^${escaped}$`).test(repoPath);
2724
+ }
2725
+ function isMarkdownDocument(repoPath) {
2726
+ return /\.(md|mdx)$/i.test(repoPath);
2727
+ }
2728
+ function isExcludedRepoPath(repoPath) {
2729
+ if (excludedFileNames.has(repoPath)) return true;
2730
+ const segments = repoPath.split("/");
2731
+ if (segments.some((segment) => excludedDirectoryNames.has(segment) || generatedPathSegments.has(segment))) return true;
2732
+ const basename = segments[segments.length - 1]?.toLowerCase() ?? "";
2733
+ return basename.startsWith(".env") || basename.includes("secret") || basename.includes("token") || basename.includes("credential") || basename.endsWith(".lock");
2734
+ }
2735
+ function isAmistioManagedMarkdown(content) {
2736
+ const frontmatter = parseFrontmatter(content);
2737
+ return typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0;
2738
+ }
2739
+ function classifyLegacyDocument(repoPath, content) {
2740
+ const haystack = `${repoPath} ${inferTitle2(content, repoPath)}`.toLowerCase();
2741
+ if (/\b(prompt|prompts|instruction|instructions|copilot|agent|skill)\b/.test(haystack)) return "prompt";
2742
+ if (/\b(adr|decision|decisions|rfc)\b/.test(haystack)) return "decision";
2743
+ if (/\b(architecture|architectural|design|system|technical|tech-spec)\b/.test(haystack)) return "architecture";
2744
+ if (/\b(feature|features|spec|requirements|prd|story|stories)\b/.test(haystack)) return "feature";
2745
+ if (/\b(memory|memories|lesson|lessons|mistake|mistakes|learning|retro|retrospective)\b/.test(haystack)) return "memory";
2746
+ if (/\b(plan|plans|roadmap|milestone|todo|task|tasks)\b/.test(haystack)) return "plan";
2747
+ if (/\b(workflow|workflows|runbook|playbook|process|procedure|ops)\b/.test(haystack)) return "workflow";
2748
+ return "context";
2749
+ }
2750
+ function canonicalImportPath(sourcePath, documentType) {
2751
+ if (isControlPlanePath2(sourcePath)) {
2752
+ return sourcePath;
2753
+ }
2754
+ const baseSlug = slugFromPath(sourcePath);
2755
+ return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
2756
+ }
2757
+ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
2758
+ if (!usedPaths.has(basePath)) {
2759
+ usedPaths.add(basePath);
2760
+ return basePath;
2761
+ }
2762
+ const extension = path9.posix.extname(basePath) || ".md";
2763
+ const directory = path9.posix.dirname(basePath);
2764
+ const basename = path9.posix.basename(basePath, extension);
2765
+ const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
2766
+ usedPaths.add(uniquePath);
2767
+ return uniquePath;
2768
+ }
2769
+ function isControlPlanePath2(repoPath) {
2770
+ const [firstSegment] = repoPath.split("/");
2771
+ return Boolean(firstSegment && controlPlaneRoots.includes(firstSegment));
2772
+ }
2773
+ function inferTitle2(content, repoPath) {
2774
+ const body = stripFrontmatter(content);
2775
+ const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
2776
+ if (heading) return heading;
2777
+ const basename = path9.posix.basename(repoPath, path9.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
2778
+ return titleCase(basename || "Imported Document");
2779
+ }
2780
+ function stripFrontmatter(content) {
2781
+ if (!content.startsWith("---\n")) return content;
2782
+ const end = content.indexOf("\n---", 4);
2783
+ if (end === -1) return content;
2784
+ const closingLineEnd = content.indexOf("\n", end + 4);
2785
+ return closingLineEnd === -1 ? "" : content.slice(closingLineEnd + 1);
2786
+ }
2787
+ function slugFromPath(repoPath) {
2788
+ const withoutExtension = repoPath.replace(/\.(md|mdx)$/i, "");
2789
+ const slug = withoutExtension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 90);
2790
+ return slug || "imported-document";
2791
+ }
2792
+ function titleCase(value) {
2793
+ return value.replace(/\b\w/g, (match) => match.toUpperCase());
2794
+ }
2795
+ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePath) {
2796
+ return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
2797
+ }
2798
+ function hashText(value, length) {
2799
+ return createHash3("sha256").update(value).digest("hex").slice(0, length);
2800
+ }
2801
+ function normalizeRepoPath2(value) {
2802
+ return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
2803
+ }
2804
+ async function runGit2(args) {
2805
+ const { stdout } = await execFileAsync2("git", args, { maxBuffer: 10 * 1024 * 1024 });
2806
+ return stdout.trim();
2807
+ }
2808
+
2809
+ // src/runner-actions.ts
2810
+ import { spawn as spawn3 } from "node:child_process";
2811
+ import path10 from "node:path";
2812
+ function buildBackgroundRunnerArgs(options) {
2813
+ const args = [
2814
+ "run",
2815
+ "--watch",
2816
+ "--api-url",
2817
+ options.apiUrl,
2818
+ "--runner-id",
2819
+ options.runnerId,
2820
+ "--root",
2821
+ path10.resolve(options.root),
2822
+ "--session",
2823
+ options.session,
2824
+ "--interval-seconds",
2825
+ String(options.intervalSeconds)
2826
+ ];
2827
+ if (options.tool) {
2828
+ args.push("--tool", options.tool);
2829
+ }
2830
+ if (options.invocationChannel) {
2831
+ args.push("--invocation-channel", options.invocationChannel);
2832
+ }
2833
+ if (options.toolCommand) {
2834
+ args.push("--tool-command", options.toolCommand);
2835
+ }
2836
+ if (options.model) {
2837
+ args.push("--model", options.model);
2838
+ }
2839
+ if (options.maxIterations !== void 0) {
2840
+ args.push("--max-iterations", String(options.maxIterations));
2841
+ }
2842
+ if (!options.stream) {
2843
+ args.push("--no-stream");
2844
+ }
2845
+ return args;
2846
+ }
2847
+ async function runOfficialCliUpdate() {
2848
+ const result = await runOfficialUpdateProcess("npm", ["install", "-g", "@amistio/cli"], 12e4);
2849
+ if (result.exitCode === 0) {
2850
+ return { succeeded: true, message: "Official Amistio CLI update command completed." };
2851
+ }
2852
+ return { succeeded: false, message: "Official Amistio CLI update command failed.", error: result.output || `npm exited with code ${result.exitCode}.` };
2853
+ }
2854
+ function runOfficialUpdateProcess(command, args, timeoutMs) {
2855
+ return new Promise((resolve) => {
2856
+ const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
2857
+ let output = "";
2858
+ const updateTimeout = setTimeout(() => {
2859
+ output += "Timed out while running official CLI update.\n";
2860
+ child.kill("SIGTERM");
2861
+ }, timeoutMs);
2862
+ child.stdout?.on("data", (chunk) => {
2863
+ output += chunk.toString("utf8");
2864
+ });
2865
+ child.stderr?.on("data", (chunk) => {
2866
+ output += chunk.toString("utf8");
2867
+ });
2868
+ child.on("error", (error) => {
2869
+ clearTimeout(updateTimeout);
2870
+ resolve({ exitCode: 1, output: error.message });
2871
+ });
2872
+ child.on("close", (code) => {
2873
+ clearTimeout(updateTimeout);
2874
+ resolve({ exitCode: code ?? 1, output: truncateProcessOutput(output) });
2875
+ });
2876
+ });
2877
+ }
2878
+ function truncateProcessOutput(value) {
2879
+ const trimmed = value.trim();
2880
+ return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
2881
+ }
2882
+
2487
2883
  // src/index.ts
2488
2884
  var program = new Command();
2489
2885
  var defaultRoot = process.env.INIT_CWD ?? process.cwd();
2490
2886
  var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
2491
- program.name("amistio").description("Amistio project brain CLI").version("0.1.2");
2492
- var CLI_VERSION = "0.1.2";
2887
+ program.name("amistio").description("Amistio project brain CLI").version("0.1.4");
2888
+ var CLI_VERSION = "0.1.4";
2493
2889
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
2494
2890
  const created = await initControlPlane(options.root);
2495
2891
  console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
@@ -2530,6 +2926,74 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
2530
2926
  console.log(`Wrote non-secret project metadata to ${filePath}.`);
2531
2927
  console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
2532
2928
  });
2929
+ program.command("import").description("Pair an existing checkout and import legacy Markdown docs for review").argument("[code]", "Short-lived pairing code from the Amistio app").option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--root <path>", "Repository root", defaultRoot).option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--default-branch <branch>", "Default branch fallback", "main").option("--include <glob>", "Only import files matching a repo-relative glob", collectRepeatedOption, []).option("--exclude <glob>", "Exclude files matching a repo-relative glob", collectRepeatedOption, []).option("--max-file-kb <kb>", "Maximum Markdown file size to import", parsePositiveInteger, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
2930
+ const pairingCode = (options.pairingCode ?? code)?.trim();
2931
+ if (!pairingCode) {
2932
+ throw new Error("Provide a pairing code as `amistio import <code>` or with `--pairing-code <code>`.");
2933
+ }
2934
+ const repository = await inspectLocalRepository(options.root, options.defaultBranch);
2935
+ const scan = await scanLegacyDocuments({
2936
+ rootDir: repository.rootDir,
2937
+ include: options.include,
2938
+ exclude: options.exclude,
2939
+ maxFileKb: options.maxFileKb
2940
+ });
2941
+ const skipCounts = importSkipCounts(scan.skipped);
2942
+ console.log(`Repository: ${repository.repoName}`);
2943
+ console.log(`Root: ${repository.rootDir}`);
2944
+ console.log(`Default branch: ${repository.defaultBranch}`);
2945
+ if (repository.originRemoteWarning) {
2946
+ console.log(repository.originRemoteWarning);
2947
+ }
2948
+ console.log(`Import candidates: ${scan.candidates.length}`);
2949
+ console.log(formatImportSkipSummary(skipCounts));
2950
+ for (const candidate of scan.candidates.slice(0, 12)) {
2951
+ console.log(`- ${candidate.sourcePath} -> ${candidate.repoPath} (${candidate.documentType})`);
2952
+ }
2953
+ if (scan.candidates.length > 12) {
2954
+ console.log(`...and ${scan.candidates.length - 12} more.`);
2955
+ }
2956
+ if (options.dryRun) {
2957
+ console.log("Dry run complete. No pairing code was consumed and no files or Amistio records were written.");
2958
+ return;
2959
+ }
2960
+ const parsedCloneUrl = repository.parsedCloneUrl;
2961
+ const pairing = await new ApiClient({ apiUrl: options.apiUrl }).importPairingSession({
2962
+ pairingCode,
2963
+ repoName: repository.repoName,
2964
+ repoFingerprint: repository.repoFingerprint,
2965
+ defaultBranch: repository.defaultBranch,
2966
+ ...parsedCloneUrl ? { cloneUrl: parsedCloneUrl.cloneUrl } : {},
2967
+ ...parsedCloneUrl?.provider ? { provider: parsedCloneUrl.provider } : {},
2968
+ ...parsedCloneUrl?.repoOwner ? { repoOwner: parsedCloneUrl.repoOwner } : {},
2969
+ ...parsedCloneUrl?.repoFullName ? { repoFullName: parsedCloneUrl.repoFullName } : {}
2970
+ });
2971
+ await initControlPlane(repository.rootDir);
2972
+ const metadataFilePath = await writeProjectLink(repository.rootDir, {
2973
+ amistioAccountId: pairing.accountId,
2974
+ amistioProjectId: pairing.projectId,
2975
+ repositoryLinkId: pairing.repositoryLink.repositoryLinkId,
2976
+ defaultBranch: repository.defaultBranch,
2977
+ lastSyncedRevision: 0
2978
+ });
2979
+ await new LocalCredentialStore().set(credentialKey(pairing.accountId, pairing.projectId, pairing.repositoryLink.repositoryLinkId), pairing.token);
2980
+ const authenticatedClient = new ApiClient({ apiUrl: options.apiUrl, accountId: pairing.accountId, token: pairing.token });
2981
+ const { documents: existingDocuments } = await authenticatedClient.listBrainDocuments(pairing.projectId);
2982
+ const documents = buildImportedBrainDocuments({
2983
+ accountId: pairing.accountId,
2984
+ projectId: pairing.projectId,
2985
+ repositoryLinkId: pairing.repositoryLink.repositoryLinkId,
2986
+ candidates: scan.candidates,
2987
+ existingDocuments
2988
+ });
2989
+ if (documents.length) {
2990
+ await authenticatedClient.pushBrainDocuments(pairing.projectId, documents);
2991
+ }
2992
+ console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}; repository link ${pairing.repositoryLinkAction}.`);
2993
+ console.log(`Wrote non-secret project metadata to ${metadataFilePath}.`);
2994
+ console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"} for review.`);
2995
+ console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
2996
+ });
2533
2997
  program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
2534
2998
  let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
2535
2999
  let credential = options.token;
@@ -2691,7 +3155,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
2691
3155
  }
2692
3156
  console.log("custom - pass --tool-command to use any other local runner command.");
2693
3157
  });
2694
- program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
3158
+ program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
2695
3159
  const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
2696
3160
  const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
2697
3161
  if (options.promptOut) {
@@ -2703,12 +3167,13 @@ program.command("orchestrate").description("Update the Amistio control plane thr
2703
3167
  return;
2704
3168
  }
2705
3169
  const sessionPolicy = normalizeSessionPolicy(options.session);
2706
- const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
3170
+ const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
2707
3171
  console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
2708
3172
  const result = await runLocalTool({
2709
3173
  rootDir: options.root,
2710
3174
  prompt,
2711
3175
  tool: options.tool,
3176
+ invocationChannel: options.invocationChannel,
2712
3177
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
2713
3178
  ...options.model ? { model: options.model } : {},
2714
3179
  streamOutput: options.stream,
@@ -2724,7 +3189,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
2724
3189
  process.exitCode = result.exitCode;
2725
3190
  }
2726
3191
  });
2727
- program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
3192
+ program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
2728
3193
  const context = await loadPairedApiContext(options.root, options.apiUrl);
2729
3194
  if (!context) {
2730
3195
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -2746,7 +3211,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
2746
3211
  projectId: context.metadata.amistioProjectId,
2747
3212
  repositoryLinkId: context.metadata.repositoryLinkId,
2748
3213
  runnerId: options.runnerId,
2749
- rootDir: path9.resolve(options.root),
3214
+ rootDir: path11.resolve(options.root),
2750
3215
  apiUrl: options.apiUrl,
2751
3216
  args: buildBackgroundRunnerArgs(options)
2752
3217
  });
@@ -2773,6 +3238,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
2773
3238
  root: options.root,
2774
3239
  sessionPolicy: normalizeSessionPolicy(options.session),
2775
3240
  ...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
3241
+ ...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
2776
3242
  ...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
2777
3243
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
2778
3244
  dryRun: Boolean(options.dryRun),
@@ -2892,6 +3358,7 @@ async function runNextWorkItem({
2892
3358
  sessionPolicy,
2893
3359
  stream,
2894
3360
  explicitModel,
3361
+ explicitInvocationChannel,
2895
3362
  explicitTool,
2896
3363
  toolCommand,
2897
3364
  commandContext,
@@ -2900,6 +3367,7 @@ async function runNextWorkItem({
2900
3367
  const toolConfig = await resolveRunnerToolConfig({
2901
3368
  apiClient,
2902
3369
  projectId,
3370
+ ...explicitInvocationChannel ? { explicitInvocationChannel } : {},
2903
3371
  ...explicitModel ? { explicitModel } : {},
2904
3372
  ...explicitTool ? { explicitTool } : {},
2905
3373
  ...toolCommand ? { toolCommand } : {}
@@ -2932,7 +3400,7 @@ async function runNextWorkItem({
2932
3400
  await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
2933
3401
  return { status: "preview", exitCode: 0 };
2934
3402
  }
2935
- const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
3403
+ const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
2936
3404
  const sessionContext = await prepareToolSession({
2937
3405
  apiClient,
2938
3406
  projectId,
@@ -2953,6 +3421,7 @@ async function runNextWorkItem({
2953
3421
  rootDir: root,
2954
3422
  prompt,
2955
3423
  tool: toolConfig.tool,
3424
+ invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
2956
3425
  ...toolCommand ? { toolCommand } : {},
2957
3426
  ...toolConfig.model ? { model: toolConfig.model } : {},
2958
3427
  streamOutput: stream,
@@ -3088,37 +3557,6 @@ async function restartCurrentRunner(context) {
3088
3557
  return { succeeded: false, message: "Background restart failed.", error: errorMessage2(error) };
3089
3558
  }
3090
3559
  }
3091
- async function runOfficialCliUpdate() {
3092
- const result = await runOfficialUpdateProcess("npm", ["install", "-g", "@amistio/cli"], 12e4);
3093
- if (result.exitCode === 0) {
3094
- return { succeeded: true, message: "Official Amistio CLI update command completed." };
3095
- }
3096
- return { succeeded: false, message: "Official Amistio CLI update command failed.", error: result.output || `npm exited with code ${result.exitCode}.` };
3097
- }
3098
- function runOfficialUpdateProcess(command, args, timeoutMs) {
3099
- return new Promise((resolve) => {
3100
- const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
3101
- let output = "";
3102
- const timer = setTimeout(() => {
3103
- output += "Timed out while running official CLI update.\n";
3104
- child.kill("SIGTERM");
3105
- }, timeoutMs);
3106
- child.stdout?.on("data", (chunk) => {
3107
- output += chunk.toString("utf8");
3108
- });
3109
- child.stderr?.on("data", (chunk) => {
3110
- output += chunk.toString("utf8");
3111
- });
3112
- child.on("error", (error) => {
3113
- clearTimeout(timer);
3114
- resolve({ exitCode: 1, output: error.message });
3115
- });
3116
- child.on("close", (code) => {
3117
- clearTimeout(timer);
3118
- resolve({ exitCode: code ?? 1, output: truncateLogExcerpt(output) });
3119
- });
3120
- });
3121
- }
3122
3560
  function runnerCommandLabel(commandKind) {
3123
3561
  if (commandKind === "update") return "update";
3124
3562
  if (commandKind === "restart") return "restart";
@@ -3386,6 +3824,12 @@ function truncateLogExcerpt(value) {
3386
3824
  const trimmed = value.trim();
3387
3825
  return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
3388
3826
  }
3827
+ function collectRepeatedOption(value, previous) {
3828
+ return [...previous, value];
3829
+ }
3830
+ function formatImportSkipSummary(counts) {
3831
+ return `Skipped: ${counts.excluded} excluded, ${counts.tooLarge} too large, ${counts.alreadyManaged} already managed, ${counts.unreadable} unreadable, ${counts.notMarkdown} non-Markdown.`;
3832
+ }
3389
3833
  function parsePositiveInteger(value) {
3390
3834
  const parsed = Number(value);
3391
3835
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -3393,11 +3837,17 @@ function parsePositiveInteger(value) {
3393
3837
  }
3394
3838
  return parsed;
3395
3839
  }
3840
+ function parseInvocationChannel(value) {
3841
+ if (value === "auto" || value === "sdk" || value === "command") {
3842
+ return value;
3843
+ }
3844
+ throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
3845
+ }
3396
3846
  function inferRepoName(root) {
3397
- return path9.basename(path9.resolve(root)) || "repository";
3847
+ return path11.basename(path11.resolve(root)) || "repository";
3398
3848
  }
3399
3849
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
3400
- return createHash3("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
3850
+ return createHash4("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
3401
3851
  }
3402
3852
  function defaultApiUrl() {
3403
3853
  const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
@@ -3412,7 +3862,7 @@ function formatApiUrlFlag(apiUrl) {
3412
3862
  function formatShellArg(value) {
3413
3863
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
3414
3864
  }
3415
- async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool, projectId, toolCommand }) {
3865
+ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
3416
3866
  const capabilities = toRunnerToolCapabilities(await detectLocalTools());
3417
3867
  if (toolCommand) {
3418
3868
  return {
@@ -3422,6 +3872,8 @@ async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool,
3422
3872
  source: "cli",
3423
3873
  status: "custom",
3424
3874
  effectiveTool: "custom",
3875
+ requestedInvocationChannel: explicitInvocationChannel ?? "command",
3876
+ effectiveInvocationChannel: "command",
3425
3877
  ...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
3426
3878
  ...explicitModel ? { model: explicitModel } : {},
3427
3879
  message: "Using local custom tool command."
@@ -3429,43 +3881,51 @@ async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool,
3429
3881
  }
3430
3882
  if (explicitTool === "none") {
3431
3883
  if (explicitModel) {
3432
- return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", model: explicitModel });
3884
+ return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", model: explicitModel });
3433
3885
  }
3434
- return { ready: true, tool: "none", capabilities, source: "cli", status: "none", message: "No local tool selected." };
3886
+ return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
3435
3887
  }
3436
3888
  if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
3437
- return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, ...explicitModel ? { model: explicitModel } : {} });
3889
+ return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...explicitModel ? { model: explicitModel } : {} });
3438
3890
  }
3439
- const remotePreference = explicitTool || explicitModel ? void 0 : await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
3891
+ const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
3440
3892
  const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
3893
+ const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
3441
3894
  const model = explicitModel ?? remotePreference?.model;
3442
- const source = explicitTool || explicitModel ? "cli" : remotePreference?.source ?? "default";
3443
- return resolveRequestedTool({ capabilities, requestedTool, source, ...model ? { model } : {} });
3895
+ const source = explicitTool || explicitInvocationChannel || explicitModel ? "cli" : remotePreference?.source ?? "default";
3896
+ return resolveRequestedTool({ capabilities, requestedInvocationChannel, requestedTool, source, ...model ? { model } : {} });
3444
3897
  }
3445
- function resolveRequestedTool({ capabilities, model, requestedTool, source }) {
3898
+ function resolveRequestedTool({ capabilities, model, requestedInvocationChannel, requestedTool, source }) {
3446
3899
  if (requestedTool === "auto") {
3447
- const candidate = capabilities.find((capability2) => capability2.available && (!model || capability2.supportsModelSelection));
3900
+ const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!model || capability2.supportsModelSelection));
3448
3901
  if (!candidate) {
3902
+ const anyAvailable = capabilities.some((capability2) => capability2.available);
3903
+ const anyChannelAvailable = capabilities.some((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel));
3904
+ const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : model ? "modelUnsupported" : "unavailable";
3449
3905
  return unavailableToolConfig({
3450
3906
  capabilities,
3451
3907
  source,
3452
- status: model ? "modelUnsupported" : "unavailable",
3908
+ status,
3453
3909
  requestedTool,
3910
+ requestedInvocationChannel,
3454
3911
  tool: "auto",
3455
3912
  ...model ? { model } : {},
3456
- message: model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
3913
+ message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` : model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
3457
3914
  });
3458
3915
  }
3459
- return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, effectiveTool: candidate.name, ...model ? { model } : {} };
3916
+ return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...model ? { model } : {} };
3460
3917
  }
3461
3918
  const capability = capabilities.find((candidate) => candidate.name === requestedTool);
3462
3919
  if (!capability?.available) {
3463
- return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
3920
+ return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
3921
+ }
3922
+ if (!capabilitySupportsInvocationChannel(capability, requestedInvocationChannel)) {
3923
+ return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is available but does not support ${requestedInvocationChannel} invocation on this runner.` });
3464
3924
  }
3465
3925
  if (model && !capability.supportsModelSelection) {
3466
- return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, effectiveTool: requestedTool, tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
3926
+ return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
3467
3927
  }
3468
- return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, effectiveTool: requestedTool, ...model ? { model } : {} };
3928
+ return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...model ? { model } : {} };
3469
3929
  }
3470
3930
  function unavailableToolConfig(input) {
3471
3931
  return {
@@ -3476,10 +3936,21 @@ function unavailableToolConfig(input) {
3476
3936
  status: input.status,
3477
3937
  message: input.message,
3478
3938
  ...input.requestedTool ? { requestedTool: input.requestedTool } : {},
3939
+ ...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
3479
3940
  ...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
3941
+ ...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
3480
3942
  ...input.model ? { model: input.model } : {}
3481
3943
  };
3482
3944
  }
3945
+ function capabilitySupportsInvocationChannel(capability, channel) {
3946
+ if (channel === "auto") return capability.available;
3947
+ if (channel === "sdk") return capability.sdkAvailable;
3948
+ return capability.commandAvailable;
3949
+ }
3950
+ function effectiveInvocationChannel(capability, channel) {
3951
+ if (channel === "sdk" || channel === "command") return channel;
3952
+ return capability.sdkAvailable ? "sdk" : "command";
3953
+ }
3483
3954
  function toRunnerToolCapabilities(tools) {
3484
3955
  return tools.map((tool) => ({
3485
3956
  name: tool.name,
@@ -3493,38 +3964,6 @@ function toRunnerToolCapabilities(tools) {
3493
3964
  supportsModelSelection: tool.supportsModelSelection
3494
3965
  }));
3495
3966
  }
3496
- function buildBackgroundRunnerArgs(options) {
3497
- const args = [
3498
- "run",
3499
- "--watch",
3500
- "--api-url",
3501
- options.apiUrl,
3502
- "--runner-id",
3503
- options.runnerId,
3504
- "--root",
3505
- path9.resolve(options.root),
3506
- "--session",
3507
- options.session,
3508
- "--interval-seconds",
3509
- String(options.intervalSeconds)
3510
- ];
3511
- if (options.tool) {
3512
- args.push("--tool", options.tool);
3513
- }
3514
- if (options.toolCommand) {
3515
- args.push("--tool-command", options.toolCommand);
3516
- }
3517
- if (options.model) {
3518
- args.push("--model", options.model);
3519
- }
3520
- if (options.maxIterations !== void 0) {
3521
- args.push("--max-iterations", String(options.maxIterations));
3522
- }
3523
- if (!options.stream) {
3524
- args.push("--no-stream");
3525
- }
3526
- return args;
3527
- }
3528
3967
  function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
3529
3968
  return {
3530
3969
  version: CLI_VERSION,
@@ -3532,7 +3971,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
3532
3971
  hostname: os5.hostname(),
3533
3972
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
3534
3973
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
3974
+ ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
3535
3975
  ...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
3976
+ ...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
3536
3977
  ...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
3537
3978
  ...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
3538
3979
  ...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},