@amistio/cli 0.1.8 → 0.1.10

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createHash as createHash6, randomUUID } from "node:crypto";
4
+ import { createHash as createHash7, randomUUID } from "node:crypto";
5
5
  import { writeFile as writeFile9 } from "node:fs/promises";
6
6
  import os7 from "node:os";
7
7
  import path13 from "node:path";
@@ -120,13 +120,55 @@ var runnerToolNameSchema = z.enum(runnerToolNames);
120
120
  var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
121
121
  var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
122
122
  var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
123
- var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "channelUnsupported", "custom", "none"]);
123
+ var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "variantUnsupported", "reasoningUnsupported", "channelUnsupported", "custom", "none"]);
124
124
  var runnerInvocationChannelSchema = z.enum(["auto", "sdk", "command"]);
125
125
  var runnerEffectiveInvocationChannelSchema = z.enum(["sdk", "command"]);
126
+ var runnerReasoningEffortSchema = z.enum(["auto", "low", "medium", "high", "xhigh"]);
127
+ var runnerProviderModelStatusSchema = z.enum(["alpha", "beta", "deprecated", "active"]);
128
+ var runnerProviderIdSchema = z.string().trim().min(1).max(120);
129
+ var runnerProviderModelIdSchema = z.string().trim().min(1).max(200);
130
+ var runnerProviderModelLimitSchema = z.object({
131
+ context: z.number().nonnegative().optional(),
132
+ input: z.number().nonnegative().optional(),
133
+ output: z.number().nonnegative().optional()
134
+ }).strict();
135
+ var runnerProviderModelModalitiesSchema = z.object({
136
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])).max(12).optional(),
137
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])).max(12).optional()
138
+ }).strict();
139
+ var runnerProviderModelVariantSchema = z.object({
140
+ disabled: z.boolean().optional()
141
+ }).strict();
142
+ var runnerProviderModelSchema = z.object({
143
+ id: runnerProviderModelIdSchema.optional(),
144
+ name: z.string().trim().min(1).max(200).optional(),
145
+ family: z.string().trim().min(1).max(120).optional(),
146
+ releaseDate: z.string().trim().min(1).max(40).optional(),
147
+ attachment: z.boolean().optional(),
148
+ reasoning: z.boolean().optional(),
149
+ temperature: z.boolean().optional(),
150
+ toolCall: z.boolean().optional(),
151
+ status: runnerProviderModelStatusSchema.optional(),
152
+ limit: runnerProviderModelLimitSchema.optional(),
153
+ modalities: runnerProviderModelModalitiesSchema.optional(),
154
+ supportedReasoningEfforts: z.array(runnerReasoningEffortSchema).max(8).optional(),
155
+ variants: z.record(z.string().trim().min(1).max(120), runnerProviderModelVariantSchema).optional()
156
+ }).strict();
157
+ var runnerProviderConfigSchema = z.object({
158
+ id: runnerProviderIdSchema.optional(),
159
+ name: z.string().trim().min(1).max(160).optional(),
160
+ api: z.string().trim().min(1).max(80).optional(),
161
+ models: z.record(runnerProviderModelIdSchema, runnerProviderModelSchema).default({})
162
+ }).strict();
163
+ var runnerProviderCatalogSchema = z.record(runnerProviderIdSchema, runnerProviderConfigSchema);
126
164
  var runnerToolModelPreferenceSchema = z.object({
127
165
  tool: runnerToolSelectionSchema.optional(),
128
166
  invocationChannel: runnerInvocationChannelSchema.optional(),
129
- model: z.string().trim().min(1).max(160).optional()
167
+ model: z.string().trim().min(1).max(160).optional(),
168
+ providerId: runnerProviderIdSchema.optional(),
169
+ modelId: runnerProviderModelIdSchema.optional(),
170
+ modelVariant: z.string().trim().min(1).max(120).optional(),
171
+ reasoningEffort: runnerReasoningEffortSchema.optional()
130
172
  });
131
173
  var runnerToolCapabilitySchema = z.object({
132
174
  name: runnerToolNameSchema,
@@ -138,6 +180,7 @@ var runnerToolCapabilitySchema = z.object({
138
180
  supportsSessionReuse: z.boolean(),
139
181
  resumabilityScope: sessionResumabilityScopeSchema,
140
182
  supportsModelSelection: z.boolean(),
183
+ providerCatalog: runnerProviderCatalogSchema.optional(),
141
184
  supportsBranchIsolation: z.boolean().optional(),
142
185
  supportsGitWorktreeIsolation: z.boolean().optional()
143
186
  });
@@ -159,6 +202,7 @@ var runnerResourceUsageSchema = z.object({
159
202
  var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
160
203
  var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
161
204
  var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
205
+ var repositoryBrainAutoSyncStatusSchema = z.enum(["disabled", "enabledInactive", "active", "synced", "failed", "blocked", "conflicted"]);
162
206
  var projectStatusSchema = z.enum(["active", "archived"]);
163
207
  var workspaceScopeKindSchema = z.enum(["personal", "organization"]);
164
208
  var personalWorkspaceScopeSchema = z.object({
@@ -229,6 +273,7 @@ var repositoryLinkItemSchema = baseItemSchema.extend({
229
273
  linkedByUserId: z.string().min(1).optional(),
230
274
  linkSource: repositoryLinkSourceSchema.optional(),
231
275
  cloneStatus: repositoryCloneStatusSchema.optional(),
276
+ autoSyncEnabled: z.boolean().optional(),
232
277
  lastValidatedAt: isoDateTimeSchema.optional(),
233
278
  status: z.enum(["active", "revoked"]).default("active")
234
279
  });
@@ -350,13 +395,29 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
350
395
  capabilities: z.array(runnerToolCapabilitySchema).optional(),
351
396
  requestedTool: runnerToolSelectionSchema.optional(),
352
397
  requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
398
+ requestedProviderId: runnerProviderIdSchema.optional(),
399
+ requestedModelId: runnerProviderModelIdSchema.optional(),
400
+ requestedModelVariant: z.string().trim().min(1).max(120).optional(),
401
+ requestedReasoningEffort: runnerReasoningEffortSchema.optional(),
353
402
  effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
354
403
  effectiveInvocationChannel: runnerEffectiveInvocationChannelSchema.optional(),
355
404
  effectiveModel: z.string().min(1).optional(),
405
+ effectiveProviderId: runnerProviderIdSchema.optional(),
406
+ effectiveModelId: runnerProviderModelIdSchema.optional(),
407
+ effectiveModelVariant: z.string().trim().min(1).max(120).optional(),
408
+ effectiveReasoningEffort: runnerReasoningEffortSchema.optional(),
356
409
  preferenceSource: runnerPreferenceSourceSchema.optional(),
357
410
  preferenceStatus: runnerPreferenceStatusSchema.optional(),
358
411
  preferenceMessage: z.string().optional(),
359
412
  resourceUsage: runnerResourceUsageSchema.optional(),
413
+ autoSyncStatus: repositoryBrainAutoSyncStatusSchema.optional(),
414
+ autoSyncMessage: z.string().max(400).optional(),
415
+ autoSyncLastStartedAt: isoDateTimeSchema.optional(),
416
+ autoSyncLastSuccessAt: isoDateTimeSchema.optional(),
417
+ autoSyncLastFailureAt: isoDateTimeSchema.optional(),
418
+ autoSyncPushedCount: z.number().int().nonnegative().optional(),
419
+ autoSyncSkippedCount: z.number().int().nonnegative().optional(),
420
+ autoSyncConflictCount: z.number().int().nonnegative().optional(),
360
421
  lastSeenAt: isoDateTimeSchema
361
422
  });
362
423
  var runnerSettingsItemSchema = baseItemSchema.extend({
@@ -1436,6 +1497,13 @@ var ApiClient = class {
1436
1497
  { method: "GET" }
1437
1498
  );
1438
1499
  }
1500
+ async listRepositoryLinks(projectId) {
1501
+ return this.request(
1502
+ `/projects/${projectId}/repository-links`,
1503
+ z3.object({ repositoryLinks: z3.array(repositoryLinkItemSchema) }),
1504
+ { method: "GET" }
1505
+ );
1506
+ }
1439
1507
  async listPlanReviewMessages(projectId, documentId) {
1440
1508
  const suffix = documentId ? `?documentId=${encodeURIComponent(documentId)}` : "";
1441
1509
  return this.request(
@@ -1503,6 +1571,10 @@ var ApiClient = class {
1503
1571
  tool: runnerToolSelectionSchema,
1504
1572
  invocationChannel: runnerInvocationChannelSchema,
1505
1573
  model: z3.string().optional(),
1574
+ providerId: z3.string().optional(),
1575
+ modelId: z3.string().optional(),
1576
+ modelVariant: z3.string().optional(),
1577
+ reasoningEffort: runnerReasoningEffortSchema.optional(),
1506
1578
  source: runnerPreferenceSourceSchema
1507
1579
  })
1508
1580
  }),
@@ -1725,10 +1797,87 @@ async function writePromptFile(filePath, prompt) {
1725
1797
 
1726
1798
  // src/local-tool-runner.ts
1727
1799
  import { spawn } from "node:child_process";
1728
- import { mkdtemp, rm, writeFile as writeFile4 } from "node:fs/promises";
1800
+ import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile4 } from "node:fs/promises";
1729
1801
  import os2 from "node:os";
1730
1802
  import path5 from "node:path";
1731
1803
  var localToolNames = runnerToolNames;
1804
+ var allReasoningEfforts = ["auto", "low", "medium", "high", "xhigh"];
1805
+ var highReasoningEfforts = ["auto", "high", "xhigh"];
1806
+ var builtInProviderCatalogs = {
1807
+ opencode: {
1808
+ anthropic: {
1809
+ id: "anthropic",
1810
+ name: "Anthropic",
1811
+ api: "anthropic",
1812
+ models: {
1813
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts),
1814
+ "claude-sonnet-4.5": reasoningModel("claude-sonnet-4.5", "Claude Sonnet 4.5", "claude", highReasoningEfforts)
1815
+ }
1816
+ },
1817
+ openai: {
1818
+ id: "openai",
1819
+ name: "OpenAI",
1820
+ api: "openai",
1821
+ models: {
1822
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts)
1823
+ }
1824
+ }
1825
+ },
1826
+ claude: {
1827
+ anthropic: {
1828
+ id: "anthropic",
1829
+ name: "Anthropic",
1830
+ api: "anthropic",
1831
+ models: {
1832
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts),
1833
+ "claude-sonnet-4.5": reasoningModel("claude-sonnet-4.5", "Claude Sonnet 4.5", "claude", highReasoningEfforts)
1834
+ }
1835
+ }
1836
+ },
1837
+ codex: {
1838
+ openai: {
1839
+ id: "openai",
1840
+ name: "OpenAI",
1841
+ api: "openai",
1842
+ models: {
1843
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts)
1844
+ }
1845
+ }
1846
+ },
1847
+ copilot: {
1848
+ "github-copilot": {
1849
+ id: "github-copilot",
1850
+ name: "GitHub Copilot",
1851
+ api: "copilot",
1852
+ models: {
1853
+ "gpt-5": reasoningModel("gpt-5", "GPT-5", "gpt", allReasoningEfforts),
1854
+ "claude-opus-4.6": reasoningModel("claude-opus-4.6", "Claude Opus 4.6", "claude", highReasoningEfforts)
1855
+ }
1856
+ }
1857
+ },
1858
+ gemini: {
1859
+ google: {
1860
+ id: "google",
1861
+ name: "Google",
1862
+ api: "google",
1863
+ models: {
1864
+ "gemini-3-pro": reasoningModel("gemini-3-pro", "Gemini 3 Pro", "gemini", highReasoningEfforts)
1865
+ }
1866
+ }
1867
+ }
1868
+ };
1869
+ function reasoningModel(id, name, family, supportedReasoningEfforts) {
1870
+ return {
1871
+ id,
1872
+ name,
1873
+ family,
1874
+ status: "active",
1875
+ reasoning: true,
1876
+ toolCall: true,
1877
+ supportedReasoningEfforts,
1878
+ variants: { standard: {} }
1879
+ };
1880
+ }
1732
1881
  var localToolAdapters = [
1733
1882
  {
1734
1883
  name: "opencode",
@@ -1737,13 +1886,15 @@ var localToolAdapters = [
1737
1886
  sdkDisplayCommand: "@opencode-ai/sdk createOpencode().client.session.prompt()",
1738
1887
  sdkRequiresExecutable: true,
1739
1888
  executable: "opencode",
1889
+ supportsModelSelection: true,
1740
1890
  supportsSessionReuse: true,
1741
1891
  resumabilityScope: "localMachine",
1892
+ providerCatalog: builtInProviderCatalogs.opencode,
1742
1893
  runWithSdk: runOpencodeSdk,
1743
- buildInvocation: ({ prompt }) => ({
1894
+ buildInvocation: ({ prompt, model }) => ({
1744
1895
  command: "opencode",
1745
- args: ["run", prompt],
1746
- displayCommand: "opencode run <generated prompt>"
1896
+ args: ["run", ...model ? ["--model", model] : [], prompt],
1897
+ displayCommand: model ? "opencode run --model <selected model> <generated prompt>" : "opencode run <generated prompt>"
1747
1898
  })
1748
1899
  },
1749
1900
  {
@@ -1752,13 +1903,15 @@ var localToolAdapters = [
1752
1903
  sdkPackageName: "@anthropic-ai/claude-agent-sdk",
1753
1904
  sdkDisplayCommand: "@anthropic-ai/claude-agent-sdk query()",
1754
1905
  executable: "claude",
1906
+ supportsModelSelection: true,
1755
1907
  supportsSessionReuse: false,
1756
1908
  resumabilityScope: "none",
1909
+ providerCatalog: builtInProviderCatalogs.claude,
1757
1910
  runWithSdk: runClaudeSdk,
1758
- buildInvocation: ({ prompt }) => ({
1911
+ buildInvocation: ({ prompt, model }) => ({
1759
1912
  command: "claude",
1760
- args: ["-p", prompt],
1761
- displayCommand: "claude -p <generated prompt>"
1913
+ args: [...model ? ["--model", model] : [], "-p", prompt],
1914
+ displayCommand: model ? "claude --model <selected model> -p <generated prompt>" : "claude -p <generated prompt>"
1762
1915
  })
1763
1916
  },
1764
1917
  {
@@ -1767,13 +1920,15 @@ var localToolAdapters = [
1767
1920
  sdkPackageName: "@openai/codex-sdk",
1768
1921
  sdkDisplayCommand: "@openai/codex-sdk Codex.startThread().run()",
1769
1922
  executable: "codex",
1923
+ supportsModelSelection: true,
1770
1924
  supportsSessionReuse: false,
1771
1925
  resumabilityScope: "none",
1926
+ providerCatalog: builtInProviderCatalogs.codex,
1772
1927
  runWithSdk: runCodexSdk,
1773
- buildInvocation: ({ prompt }) => ({
1928
+ buildInvocation: ({ prompt, model }) => ({
1774
1929
  command: "codex",
1775
- args: ["exec", prompt],
1776
- displayCommand: "codex exec <generated prompt>"
1930
+ args: ["exec", ...model ? ["--model", model] : [], prompt],
1931
+ displayCommand: model ? "codex exec --model <selected model> <generated prompt>" : "codex exec <generated prompt>"
1777
1932
  })
1778
1933
  },
1779
1934
  {
@@ -1784,18 +1939,21 @@ var localToolAdapters = [
1784
1939
  supportsModelSelection: true,
1785
1940
  supportsSessionReuse: false,
1786
1941
  resumabilityScope: "none",
1942
+ providerCatalog: builtInProviderCatalogs.copilot,
1787
1943
  runWithSdk: runCopilotSdk
1788
1944
  },
1789
1945
  {
1790
1946
  name: "gemini",
1791
1947
  description: "Gemini CLI adapter using prompt mode.",
1792
1948
  executable: "gemini",
1949
+ supportsModelSelection: true,
1793
1950
  supportsSessionReuse: false,
1794
1951
  resumabilityScope: "none",
1795
- buildInvocation: ({ prompt }) => ({
1952
+ providerCatalog: builtInProviderCatalogs.gemini,
1953
+ buildInvocation: ({ prompt, model }) => ({
1796
1954
  command: "gemini",
1797
- args: ["-p", prompt],
1798
- displayCommand: "gemini -p <generated prompt>"
1955
+ args: [...model ? ["--model", model] : [], "-p", prompt],
1956
+ displayCommand: model ? "gemini --model <selected model> -p <generated prompt>" : "gemini -p <generated prompt>"
1799
1957
  })
1800
1958
  },
1801
1959
  {
@@ -1831,6 +1989,7 @@ async function detectLocalTools() {
1831
1989
  localToolAdapters.map(async (adapter) => {
1832
1990
  const sdkAvailable = await isSdkAvailable(adapter);
1833
1991
  const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
1992
+ const providerCatalog = await detectProviderCatalog(adapter);
1834
1993
  return {
1835
1994
  name: adapter.name,
1836
1995
  description: adapter.description,
@@ -1840,7 +1999,8 @@ async function detectLocalTools() {
1840
1999
  execution: sdkAvailable ? "sdk" : commandAvailable ? "command" : "unavailable",
1841
2000
  supportsSessionReuse: Boolean(adapter.supportsSessionReuse),
1842
2001
  resumabilityScope: adapter.resumabilityScope ?? "none",
1843
- supportsModelSelection: Boolean(adapter.supportsModelSelection)
2002
+ supportsModelSelection: Boolean(adapter.supportsModelSelection),
2003
+ ...providerCatalog ? { providerCatalog } : {}
1844
2004
  };
1845
2005
  })
1846
2006
  );
@@ -1849,6 +2009,7 @@ async function runLocalTool(options) {
1849
2009
  const promptTempDir = await mkdtemp(path5.join(os2.tmpdir(), "amistio-prompt-"));
1850
2010
  const promptFilePath = path5.join(promptTempDir, "prompt.md");
1851
2011
  await writeFile4(promptFilePath, options.prompt, "utf8");
2012
+ const modelConfig = normalizeModelOptions(options);
1852
2013
  try {
1853
2014
  const runnerOptions = {
1854
2015
  rootDir: options.rootDir,
@@ -1856,7 +2017,7 @@ async function runLocalTool(options) {
1856
2017
  promptFilePath,
1857
2018
  tool: options.tool ?? "auto",
1858
2019
  invocationChannel: options.invocationChannel ?? "auto",
1859
- ...options.model ? { model: options.model } : {}
2020
+ ...modelConfig
1860
2021
  };
1861
2022
  if (options.toolCommand) {
1862
2023
  runnerOptions.toolCommand = options.toolCommand;
@@ -1867,6 +2028,7 @@ async function runLocalTool(options) {
1867
2028
  prompt: options.prompt,
1868
2029
  promptFilePath,
1869
2030
  streamOutput: Boolean(options.streamOutput),
2031
+ ...modelConfig,
1870
2032
  ...options.session ? { session: options.session } : {}
1871
2033
  }, options.timeoutMs);
1872
2034
  return {
@@ -1874,7 +2036,7 @@ async function runLocalTool(options) {
1874
2036
  displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
1875
2037
  supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
1876
2038
  resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
1877
- ...options.model ? { model: options.model } : {},
2039
+ ...modelConfig,
1878
2040
  ...result
1879
2041
  };
1880
2042
  } finally {
@@ -1883,13 +2045,14 @@ async function runLocalTool(options) {
1883
2045
  }
1884
2046
  async function createToolRunPreview(options) {
1885
2047
  const promptFilePath = path5.join(os2.tmpdir(), "amistio-generated-prompt.md");
2048
+ const modelConfig = normalizeModelOptions(options);
1886
2049
  const runnerOptions = {
1887
2050
  rootDir: options.rootDir,
1888
2051
  prompt: options.prompt,
1889
2052
  promptFilePath,
1890
2053
  tool: options.tool ?? "auto",
1891
2054
  invocationChannel: options.invocationChannel ?? "auto",
1892
- ...options.model ? { model: options.model } : {}
2055
+ ...modelConfig
1893
2056
  };
1894
2057
  if (options.toolCommand) {
1895
2058
  runnerOptions.toolCommand = options.toolCommand;
@@ -1900,7 +2063,20 @@ async function createToolRunPreview(options) {
1900
2063
  displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
1901
2064
  supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
1902
2065
  resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
1903
- ...options.model ? { model: options.model } : {}
2066
+ ...modelConfig
2067
+ };
2068
+ }
2069
+ function normalizeModelOptions(options) {
2070
+ const model = options.model?.trim() || (options.providerId?.trim() && options.modelId?.trim() ? `${options.providerId.trim()}/${options.modelId.trim()}` : options.modelId?.trim()) || void 0;
2071
+ const providerId = options.providerId?.trim();
2072
+ const modelId = options.modelId?.trim();
2073
+ const modelVariant = options.modelVariant?.trim();
2074
+ return {
2075
+ ...model ? { model } : {},
2076
+ ...providerId ? { providerId } : {},
2077
+ ...modelId ? { modelId } : {},
2078
+ ...modelVariant ? { modelVariant } : {},
2079
+ ...options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}
1904
2080
  };
1905
2081
  }
1906
2082
  function createCustomToolInvocation(commandTemplate, input) {
@@ -1925,9 +2101,13 @@ async function createToolRunner(options) {
1925
2101
  if (tool === "none") {
1926
2102
  throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
1927
2103
  }
1928
- const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model), options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
1929
- if (options.model && !adapter.supportsModelSelection) {
1930
- throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
2104
+ const requiresModelSelection = Boolean(options.model || options.providerId || options.modelId || options.modelVariant || options.reasoningEffort && options.reasoningEffort !== "auto");
2105
+ if (requiresModelSelection && !options.model) {
2106
+ throw new Error("Provider-backed model configuration requires --model or --provider with --model-id.");
2107
+ }
2108
+ const adapter = tool === "auto" ? await selectFirstAvailableAdapter(requiresModelSelection, options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
2109
+ if (requiresModelSelection && !adapter.supportsModelSelection) {
2110
+ throw new Error(`Model selection is not supported by ${adapter.name}. Remove model configuration or choose a model-aware adapter.`);
1931
2111
  }
1932
2112
  if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
1933
2113
  return {
@@ -2051,6 +2231,147 @@ async function commandExists(command) {
2051
2231
  lookup.on("close", (exitCode) => resolve(exitCode === 0));
2052
2232
  });
2053
2233
  }
2234
+ async function detectProviderCatalog(adapter) {
2235
+ const opencodeCatalog = adapter.name === "opencode" ? await loadOpencodeProviderCatalog() : void 0;
2236
+ return mergeProviderCatalogs(adapter.providerCatalog, opencodeCatalog);
2237
+ }
2238
+ async function loadOpencodeProviderCatalog() {
2239
+ const configPaths = [
2240
+ path5.join(os2.homedir(), ".config", "opencode", "opencode.json"),
2241
+ path5.join(os2.homedir(), ".config", "opencode", "config.json"),
2242
+ path5.join(process.cwd(), "opencode.json")
2243
+ ];
2244
+ for (const configPath of configPaths) {
2245
+ try {
2246
+ const parsed = JSON.parse(await readFile3(configPath, "utf8"));
2247
+ const providerValue = isRecord(parsed) ? parsed.provider : void 0;
2248
+ const catalog = sanitizeProviderCatalog(providerValue);
2249
+ if (catalog) return catalog;
2250
+ } catch {
2251
+ }
2252
+ }
2253
+ return void 0;
2254
+ }
2255
+ function mergeProviderCatalogs(...catalogs) {
2256
+ const merged = {};
2257
+ for (const catalog of catalogs) {
2258
+ if (!catalog) continue;
2259
+ for (const [providerId, provider] of Object.entries(catalog)) {
2260
+ const existing = isRecord(merged[providerId]) ? merged[providerId] : void 0;
2261
+ const existingModels = existing && isRecord(existing.models) ? existing.models : {};
2262
+ merged[providerId] = {
2263
+ ...existing,
2264
+ ...provider,
2265
+ id: provider.id ?? providerId,
2266
+ models: { ...existingModels, ...provider.models }
2267
+ };
2268
+ }
2269
+ }
2270
+ const parsed = runnerProviderCatalogSchema.safeParse(merged);
2271
+ return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
2272
+ }
2273
+ function sanitizeProviderCatalog(value) {
2274
+ if (!isRecord(value)) return void 0;
2275
+ const catalog = {};
2276
+ for (const [providerId, providerValue] of Object.entries(value)) {
2277
+ const provider = sanitizeProviderConfig(providerId, providerValue);
2278
+ if (provider) catalog[providerId] = provider;
2279
+ }
2280
+ const parsed = runnerProviderCatalogSchema.safeParse(catalog);
2281
+ return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
2282
+ }
2283
+ function sanitizeProviderConfig(providerId, value) {
2284
+ if (!isRecord(value)) return void 0;
2285
+ const models = sanitizeProviderModels(value.models);
2286
+ if (!Object.keys(models).length) return void 0;
2287
+ return {
2288
+ id: stringValue(value.id) ?? providerId,
2289
+ ...stringValue(value.name) ? { name: stringValue(value.name) } : {},
2290
+ ...stringValue(value.api) ? { api: stringValue(value.api) } : {},
2291
+ models
2292
+ };
2293
+ }
2294
+ function sanitizeProviderModels(value) {
2295
+ if (!isRecord(value)) return {};
2296
+ const models = {};
2297
+ for (const [modelId, modelValue] of Object.entries(value)) {
2298
+ const model = sanitizeProviderModel(modelId, modelValue);
2299
+ if (model) models[modelId] = model;
2300
+ }
2301
+ return models;
2302
+ }
2303
+ function sanitizeProviderModel(modelId, value) {
2304
+ if (!isRecord(value)) return void 0;
2305
+ const reasoning = booleanValue(value.reasoning);
2306
+ const releaseDate = stringValue(value.release_date) ?? stringValue(value.releaseDate);
2307
+ const toolCall = booleanValue(value.tool_call) ?? booleanValue(value.toolCall);
2308
+ const limit = sanitizeLimit(value.limit);
2309
+ const modalities = sanitizeModalities(value.modalities);
2310
+ const variants = sanitizeVariants(value.variants);
2311
+ const model = {
2312
+ id: stringValue(value.id) ?? modelId,
2313
+ ...stringValue(value.name) ? { name: stringValue(value.name) } : {},
2314
+ ...stringValue(value.family) ? { family: stringValue(value.family) } : {},
2315
+ ...releaseDate ? { releaseDate } : {},
2316
+ ...booleanValue(value.attachment) !== void 0 ? { attachment: booleanValue(value.attachment) } : {},
2317
+ ...reasoning !== void 0 ? { reasoning } : {},
2318
+ ...booleanValue(value.temperature) !== void 0 ? { temperature: booleanValue(value.temperature) } : {},
2319
+ ...toolCall !== void 0 ? { toolCall } : {},
2320
+ ...stringValue(value.status) ? { status: stringValue(value.status) } : {},
2321
+ ...limit ? { limit } : {},
2322
+ ...modalities ? { modalities } : {},
2323
+ ...variants ? { variants } : {}
2324
+ };
2325
+ const supportedReasoningEfforts = sanitizeReasoningEfforts(value.supportedReasoningEfforts) ?? sanitizeReasoningEfforts(value.supported_reasoning_efforts) ?? (reasoning ? allReasoningEfforts : void 0);
2326
+ if (supportedReasoningEfforts) model.supportedReasoningEfforts = supportedReasoningEfforts;
2327
+ return model;
2328
+ }
2329
+ function sanitizeLimit(value) {
2330
+ if (!isRecord(value)) return void 0;
2331
+ const limit = {
2332
+ ...numberValue(value.context) !== void 0 ? { context: numberValue(value.context) } : {},
2333
+ ...numberValue(value.input) !== void 0 ? { input: numberValue(value.input) } : {},
2334
+ ...numberValue(value.output) !== void 0 ? { output: numberValue(value.output) } : {}
2335
+ };
2336
+ return Object.keys(limit).length ? limit : void 0;
2337
+ }
2338
+ function sanitizeModalities(value) {
2339
+ if (!isRecord(value)) return void 0;
2340
+ const modalities = {
2341
+ ...stringArrayValue(value.input) ? { input: stringArrayValue(value.input) } : {},
2342
+ ...stringArrayValue(value.output) ? { output: stringArrayValue(value.output) } : {}
2343
+ };
2344
+ return Object.keys(modalities).length ? modalities : void 0;
2345
+ }
2346
+ function sanitizeVariants(value) {
2347
+ if (!isRecord(value)) return void 0;
2348
+ const variants = {};
2349
+ for (const [variantId, variantValue] of Object.entries(value)) {
2350
+ variants[variantId] = isRecord(variantValue) && booleanValue(variantValue.disabled) !== void 0 ? { disabled: booleanValue(variantValue.disabled) } : {};
2351
+ }
2352
+ return Object.keys(variants).length ? variants : void 0;
2353
+ }
2354
+ function sanitizeReasoningEfforts(value) {
2355
+ const values = stringArrayValue(value);
2356
+ if (!values) return void 0;
2357
+ const efforts = values.filter((candidate) => allReasoningEfforts.includes(candidate));
2358
+ return efforts.length ? efforts : void 0;
2359
+ }
2360
+ function isRecord(value) {
2361
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2362
+ }
2363
+ function stringValue(value) {
2364
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2365
+ }
2366
+ function numberValue(value) {
2367
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2368
+ }
2369
+ function booleanValue(value) {
2370
+ return typeof value === "boolean" ? value : void 0;
2371
+ }
2372
+ function stringArrayValue(value) {
2373
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
2374
+ }
2054
2375
  async function executeToolInvocation(invocation, rootDir, streamOutput, timeoutMs) {
2055
2376
  return new Promise((resolve, reject) => {
2056
2377
  const child = spawn(invocation.command, invocation.args, {
@@ -2282,9 +2603,6 @@ function extractTextParts(parts) {
2282
2603
  }
2283
2604
  return parts.filter(isRecord).map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
2284
2605
  }
2285
- function isRecord(value) {
2286
- return typeof value === "object" && value !== null;
2287
- }
2288
2606
  function errorMessage(error) {
2289
2607
  return error instanceof Error ? error.message : String(error);
2290
2608
  }
@@ -2296,7 +2614,7 @@ function shellQuote(value) {
2296
2614
  import { spawn as spawn2 } from "node:child_process";
2297
2615
  import { createHash as createHash2 } from "node:crypto";
2298
2616
  import { openSync } from "node:fs";
2299
- import { mkdir as mkdir5, readdir as readdir2, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
2617
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
2300
2618
  import os3 from "node:os";
2301
2619
  import path6 from "node:path";
2302
2620
  function currentRunnerMode() {
@@ -2463,7 +2781,7 @@ function runnerDaemonKey(input) {
2463
2781
  }
2464
2782
  async function readRunnerDaemonMetadataFile(filePath) {
2465
2783
  try {
2466
- const parsed = JSON.parse(await readFile3(filePath, "utf8"));
2784
+ const parsed = JSON.parse(await readFile4(filePath, "utf8"));
2467
2785
  if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
2468
2786
  return void 0;
2469
2787
  }
@@ -2476,7 +2794,7 @@ async function readRunnerDaemonMetadataFile(filePath) {
2476
2794
  // src/runner-service.ts
2477
2795
  import { spawn as spawn3 } from "node:child_process";
2478
2796
  import { createHash as createHash3 } from "node:crypto";
2479
- import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
2797
+ import { mkdir as mkdir6, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
2480
2798
  import os4 from "node:os";
2481
2799
  import path7 from "node:path";
2482
2800
  function detectRunnerServicePlatform(platform = process.platform) {
@@ -2542,7 +2860,7 @@ async function removeRunnerService(input) {
2542
2860
  }
2543
2861
  async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
2544
2862
  try {
2545
- const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
2863
+ const parsed = JSON.parse(await readFile5(runnerServiceMetadataPath(input, metadataDir), "utf8"));
2546
2864
  if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
2547
2865
  return void 0;
2548
2866
  }
@@ -2822,10 +3140,28 @@ function tokens(value) {
2822
3140
  }
2823
3141
 
2824
3142
  // src/sync.ts
2825
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
3143
+ import { execFile as execFile3 } from "node:child_process";
3144
+ import { createHash as createHash4 } from "node:crypto";
3145
+ import { mkdir as mkdir7, readdir as readdir3, readFile as readFile6, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
2826
3146
  import path8 from "node:path";
3147
+ import { promisify as promisify3 } from "node:util";
3148
+ var execFileAsync3 = promisify3(execFile3);
2827
3149
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
2828
3150
  var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
3151
+ var documentTypeByRoot = {
3152
+ architecture: "architecture",
3153
+ context: "context",
3154
+ decisions: "decision",
3155
+ features: "feature",
3156
+ memory: "memory",
3157
+ plans: "plan",
3158
+ prompts: "prompt",
3159
+ workflows: "workflow"
3160
+ };
3161
+ var autoSyncMetadataPaths = /* @__PURE__ */ new Set(["docs/context/amistio-project.md", "context/amistio-project.md"]);
3162
+ var autoSyncExcludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
3163
+ var autoSyncGeneratedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
3164
+ var defaultAutoSyncMaxFileKb = 256;
2829
3165
  async function collectSyncStatus(rootDir, webDocuments = []) {
2830
3166
  const localDocuments = await readLocalSyncedDocuments(rootDir);
2831
3167
  const normalizedWebDocuments = webDocuments.map((document) => ({ ...document, repoPath: canonicalControlPlaneRepoPath(document.repoPath) }));
@@ -2902,7 +3238,7 @@ async function readLocalSyncedDocuments(rootDir) {
2902
3238
  const markdownFiles = await findMarkdownFiles(rootDir);
2903
3239
  const documents = [];
2904
3240
  for (const fullPath of markdownFiles) {
2905
- const raw = await readFile5(fullPath, "utf8");
3241
+ const raw = await readFile6(fullPath, "utf8");
2906
3242
  const parsed = parseSyncedMarkdown(raw);
2907
3243
  if (!parsed) {
2908
3244
  continue;
@@ -2985,6 +3321,29 @@ async function collectDirtyDocumentsForPush(rootDir, metadata) {
2985
3321
  });
2986
3322
  });
2987
3323
  }
3324
+ async function collectAutoSyncDocumentsForPush(rootDir, metadata, existingDocuments = [], options = {}) {
3325
+ const dirtyDocuments = await collectDirtyDocumentsForPush(rootDir, metadata);
3326
+ const external = await collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options);
3327
+ return {
3328
+ documents: [...dirtyDocuments, ...external.documents],
3329
+ managedDocumentIds: dirtyDocuments.map((document) => document.documentId),
3330
+ skipped: external.skipped,
3331
+ counts: autoSyncSkipCounts(external.skipped)
3332
+ };
3333
+ }
3334
+ function autoSyncSkipCounts(skipped) {
3335
+ return {
3336
+ metadata: skipped.filter((item) => item.reason === "metadata").length,
3337
+ template: skipped.filter((item) => item.reason === "template").length,
3338
+ unsupported: skipped.filter((item) => item.reason === "unsupported").length,
3339
+ tooLarge: skipped.filter((item) => item.reason === "tooLarge").length,
3340
+ alreadyManaged: skipped.filter((item) => item.reason === "alreadyManaged").length,
3341
+ unchanged: skipped.filter((item) => item.reason === "unchanged").length,
3342
+ conflicted: skipped.filter((item) => item.reason === "conflicted").length,
3343
+ excluded: skipped.filter((item) => item.reason === "excluded").length,
3344
+ unreadable: skipped.filter((item) => item.reason === "unreadable").length
3345
+ };
3346
+ }
2988
3347
  function createSyncedDocumentMarkdown(document) {
2989
3348
  return [
2990
3349
  "---",
@@ -3014,7 +3373,7 @@ function parseSyncedMarkdown(content) {
3014
3373
  }
3015
3374
  async function readExistingSyncedDocument(fullPath) {
3016
3375
  try {
3017
- const raw = await readFile5(fullPath, "utf8");
3376
+ const raw = await readFile6(fullPath, "utf8");
3018
3377
  const parsed = parseSyncedMarkdown(raw);
3019
3378
  if (!parsed) {
3020
3379
  return { exists: true };
@@ -3091,6 +3450,145 @@ function inferTitle(content, repoPath) {
3091
3450
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
3092
3451
  return heading || path8.basename(repoPath, path8.extname(repoPath));
3093
3452
  }
3453
+ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
3454
+ const root = path8.resolve(rootDir);
3455
+ const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
3456
+ const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
3457
+ const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
3458
+ const repoPaths = await listAutoSyncCandidatePaths(root);
3459
+ const documents = [];
3460
+ const skipped = [];
3461
+ for (const repoPath of repoPaths) {
3462
+ const normalizedRepoPath = normalizeRepoPath3(repoPath);
3463
+ const canonicalRepoPath = canonicalControlPlaneRepoPath(normalizedRepoPath);
3464
+ const skipReason = autoSyncPathSkipReason(normalizedRepoPath);
3465
+ if (skipReason) {
3466
+ skipped.push({ repoPath: normalizedRepoPath, reason: skipReason });
3467
+ continue;
3468
+ }
3469
+ const fullPath = safeRepoPath(root, normalizedRepoPath);
3470
+ const fileStat = await stat3(fullPath).catch(() => void 0);
3471
+ if (!fileStat?.isFile()) {
3472
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3473
+ continue;
3474
+ }
3475
+ if (fileStat.size > maxBytes) {
3476
+ skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
3477
+ continue;
3478
+ }
3479
+ const content = await readFile6(fullPath, "utf8").catch(() => void 0);
3480
+ if (content === void 0) {
3481
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3482
+ continue;
3483
+ }
3484
+ if (parseSyncedMarkdown(content)) {
3485
+ skipped.push({ repoPath: normalizedRepoPath, reason: "alreadyManaged" });
3486
+ continue;
3487
+ }
3488
+ const documentType = documentTypeForRepoPath(canonicalRepoPath);
3489
+ if (!documentType) {
3490
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unsupported" });
3491
+ continue;
3492
+ }
3493
+ const contentHash = sha256ContentHash(content);
3494
+ const documentId = stableExternalDocumentId(metadata, canonicalRepoPath);
3495
+ const existing = existingById.get(documentId);
3496
+ if (existing?.syncState === "conflicted" || existing?.status === "conflicted") {
3497
+ skipped.push({ repoPath: normalizedRepoPath, reason: "conflicted" });
3498
+ continue;
3499
+ }
3500
+ const lastAutoSyncedHash = typeof existing?.frontmatter.autoSyncedSourceHash === "string" ? existing.frontmatter.autoSyncedSourceHash : void 0;
3501
+ if (existing?.contentHash === contentHash || lastAutoSyncedHash === contentHash) {
3502
+ skipped.push({ repoPath: normalizedRepoPath, reason: "unchanged" });
3503
+ continue;
3504
+ }
3505
+ const revision = existing ? existing.revision + 1 : 0;
3506
+ documents.push(brainDocumentItemSchema.parse({
3507
+ id: documentId,
3508
+ type: "brainDocument",
3509
+ schemaVersion: 1,
3510
+ accountId: metadata.amistioAccountId,
3511
+ projectId: metadata.amistioProjectId,
3512
+ documentId,
3513
+ documentType,
3514
+ title: inferTitle(content, canonicalRepoPath),
3515
+ status: "reviewing",
3516
+ repoPath: canonicalRepoPath,
3517
+ content,
3518
+ contentHash,
3519
+ frontmatter: {
3520
+ ...existing?.frontmatter ?? {},
3521
+ externalBrainPath: normalizedRepoPath,
3522
+ autoSyncedByCommand: "amistio sync watch",
3523
+ autoSyncedAt: syncedAt,
3524
+ autoSyncedSourceHash: contentHash,
3525
+ ...lastAutoSyncedHash ? { amistioContentHash: lastAutoSyncedHash } : {}
3526
+ },
3527
+ revision,
3528
+ source: "repo",
3529
+ syncState: "dirtyInRepo",
3530
+ createdAt: existing?.createdAt ?? syncedAt,
3531
+ updatedAt: syncedAt
3532
+ }));
3533
+ }
3534
+ return { documents, skipped };
3535
+ }
3536
+ async function listAutoSyncCandidatePaths(rootDir) {
3537
+ const gitFiles = await execFileAsync3("git", ["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).then(({ stdout }) => stdout).catch(() => void 0);
3538
+ if (gitFiles !== void 0) {
3539
+ return uniqueSortedRepoPaths(gitFiles.split("\n").map(normalizeRepoPath3).filter((repoPath) => repoPath && isRecognizedBrainRepoPath(repoPath)));
3540
+ }
3541
+ const files = [];
3542
+ for (const syncRoot of [...syncRoots, ...legacySyncRoots]) {
3543
+ const fullRoot = path8.join(rootDir, syncRoot);
3544
+ if (await exists2(fullRoot)) {
3545
+ await walkAutoSyncFiles(rootDir, fullRoot, files);
3546
+ }
3547
+ }
3548
+ return uniqueSortedRepoPaths(files);
3549
+ }
3550
+ async function walkAutoSyncFiles(rootDir, directory, files) {
3551
+ for (const entry of await readdir3(directory, { withFileTypes: true }).catch(() => [])) {
3552
+ const fullPath = path8.join(directory, entry.name);
3553
+ const repoPath = normalizeRepoPath3(path8.relative(rootDir, fullPath));
3554
+ if (entry.isDirectory()) {
3555
+ if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
3556
+ await walkAutoSyncFiles(rootDir, fullPath, files);
3557
+ }
3558
+ } else if (entry.isFile() && isRecognizedBrainRepoPath(repoPath)) {
3559
+ files.push(repoPath);
3560
+ }
3561
+ }
3562
+ }
3563
+ function autoSyncPathSkipReason(repoPath) {
3564
+ if (autoSyncMetadataPaths.has(repoPath)) return "metadata";
3565
+ if (isControlPlaneTemplateRepoPath(repoPath)) return "template";
3566
+ if (!/\.(md|mdx)$/i.test(repoPath)) return "unsupported";
3567
+ const basename = repoPath.split("/").at(-1)?.toLowerCase() ?? "";
3568
+ if (basename.startsWith(".") || basename.endsWith(".lock") || basename.includes(".env") || basename.includes("secret") || basename.includes("credential")) return "excluded";
3569
+ if (repoPath.split("/").some((segment) => autoSyncExcludedDirectoryNames.has(segment) || autoSyncGeneratedPathSegments.has(segment))) return "excluded";
3570
+ return void 0;
3571
+ }
3572
+ function isRecognizedBrainRepoPath(repoPath) {
3573
+ const normalized = normalizeRepoPath3(repoPath);
3574
+ const [firstSegment, secondSegment] = normalized.split("/");
3575
+ return Boolean(firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
3576
+ }
3577
+ function documentTypeForRepoPath(repoPath) {
3578
+ const normalized = normalizeRepoPath3(repoPath);
3579
+ const segments = normalized.split("/");
3580
+ const root = segments[0] === "docs" ? segments[1] : segments[0];
3581
+ return root && root in documentTypeByRoot ? documentTypeByRoot[root] : void 0;
3582
+ }
3583
+ function stableExternalDocumentId(metadata, repoPath) {
3584
+ return `doc_external_${createHash4("sha256").update(`${metadata.amistioAccountId}:${metadata.amistioProjectId}:${metadata.repositoryLinkId}:${repoPath}`).digest("hex").slice(0, 24)}`;
3585
+ }
3586
+ function normalizeRepoPath3(repoPath) {
3587
+ return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
3588
+ }
3589
+ function uniqueSortedRepoPaths(repoPaths) {
3590
+ return [...new Set(repoPaths)].sort((first, second) => first.localeCompare(second));
3591
+ }
3094
3592
  function parseFrontmatterFromSyncedDocument(frontmatter) {
3095
3593
  return {
3096
3594
  amistioDocumentId: frontmatter.amistioDocumentId,
@@ -3110,7 +3608,7 @@ async function exists2(filePath) {
3110
3608
  }
3111
3609
 
3112
3610
  // src/tool-session-store.ts
3113
- import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
3611
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
3114
3612
  import os5 from "node:os";
3115
3613
  import path9 from "node:path";
3116
3614
  var LocalToolSessionStore = class {
@@ -3131,7 +3629,7 @@ var LocalToolSessionStore = class {
3131
3629
  }
3132
3630
  async read() {
3133
3631
  try {
3134
- return JSON.parse(await readFile6(this.filePath, "utf8"));
3632
+ return JSON.parse(await readFile7(this.filePath, "utf8"));
3135
3633
  } catch {
3136
3634
  return {};
3137
3635
  }
@@ -3431,7 +3929,7 @@ function stripJsonFence(value) {
3431
3929
  }
3432
3930
 
3433
3931
  // src/runner-status.ts
3434
- import { createHash as createHash4 } from "node:crypto";
3932
+ import { createHash as createHash5 } from "node:crypto";
3435
3933
  var watchStateReminderMs = 60 * 1e3;
3436
3934
  function formatWatchStartupContext(input) {
3437
3935
  return [
@@ -3452,7 +3950,7 @@ function watchStateKey(action) {
3452
3950
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
3453
3951
  }
3454
3952
  function stableRunnerId(input) {
3455
- const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3953
+ const digest = createHash5("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
3456
3954
  return `runner_${digest}`;
3457
3955
  }
3458
3956
 
@@ -3576,12 +4074,12 @@ function roundNumber(value, digits) {
3576
4074
  }
3577
4075
 
3578
4076
  // src/importer.ts
3579
- import { execFile as execFile3 } from "node:child_process";
3580
- import { createHash as createHash5 } from "node:crypto";
3581
- import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
4077
+ import { execFile as execFile4 } from "node:child_process";
4078
+ import { createHash as createHash6 } from "node:crypto";
4079
+ import { readdir as readdir4, readFile as readFile8, stat as stat4 } from "node:fs/promises";
3582
4080
  import path10 from "node:path";
3583
- import { promisify as promisify3 } from "node:util";
3584
- var execFileAsync3 = promisify3(execFile3);
4081
+ import { promisify as promisify4 } from "node:util";
4082
+ var execFileAsync4 = promisify4(execFile4);
3585
4083
  var defaultMaxFileKb = 256;
3586
4084
  var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
3587
4085
  var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
@@ -3644,7 +4142,7 @@ async function scanLegacyDocuments(options) {
3644
4142
  skipped.push({ repoPath, reason: "tooLarge" });
3645
4143
  continue;
3646
4144
  }
3647
- const content = await readFile7(fullPath, "utf8").catch(() => void 0);
4145
+ const content = await readFile8(fullPath, "utf8").catch(() => void 0);
3648
4146
  if (content === void 0) {
3649
4147
  skipped.push({ repoPath, reason: "unreadable" });
3650
4148
  continue;
@@ -3725,7 +4223,7 @@ function parseOptionalOriginCloneUrl(originUrl) {
3725
4223
  async function listRepositoryPaths(rootDir) {
3726
4224
  const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
3727
4225
  if (gitFiles !== void 0) {
3728
- return gitFiles.split("\n").map((line) => normalizeRepoPath3(line)).filter((line) => line.length > 0);
4226
+ return gitFiles.split("\n").map((line) => normalizeRepoPath4(line)).filter((line) => line.length > 0);
3729
4227
  }
3730
4228
  const files = [];
3731
4229
  await walkRepository(rootDir, rootDir, files);
@@ -3735,7 +4233,7 @@ async function walkRepository(rootDir, directory, files) {
3735
4233
  const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
3736
4234
  for (const entry of entries) {
3737
4235
  const fullPath = path10.join(directory, entry.name);
3738
- const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
4236
+ const repoPath = normalizeRepoPath4(path10.relative(rootDir, fullPath));
3739
4237
  if (entry.isDirectory()) {
3740
4238
  if (!excludedDirectoryNames.has(entry.name)) {
3741
4239
  await walkRepository(rootDir, fullPath, files);
@@ -3755,7 +4253,7 @@ function matchesIncludeExclude(repoPath, include, exclude) {
3755
4253
  return true;
3756
4254
  }
3757
4255
  function wildcardMatch(pattern, repoPath) {
3758
- const normalizedPattern = normalizeRepoPath3(pattern);
4256
+ const normalizedPattern = normalizeRepoPath4(pattern);
3759
4257
  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, ".*");
3760
4258
  return new RegExp(`^${escaped}$`).test(repoPath);
3761
4259
  }
@@ -3808,7 +4306,7 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
3808
4306
  return uniquePath;
3809
4307
  }
3810
4308
  function isCanonicalControlPlanePath(repoPath) {
3811
- const [firstSegment, secondSegment] = normalizeRepoPath3(repoPath).split("/");
4309
+ const [firstSegment, secondSegment] = normalizeRepoPath4(repoPath).split("/");
3812
4310
  return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
3813
4311
  }
3814
4312
  function isLegacyControlPlanePath(repoPath) {
@@ -3841,13 +4339,13 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
3841
4339
  return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
3842
4340
  }
3843
4341
  function hashText(value, length) {
3844
- return createHash5("sha256").update(value).digest("hex").slice(0, length);
4342
+ return createHash6("sha256").update(value).digest("hex").slice(0, length);
3845
4343
  }
3846
- function normalizeRepoPath3(value) {
4344
+ function normalizeRepoPath4(value) {
3847
4345
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
3848
4346
  }
3849
4347
  async function runGit2(args) {
3850
- const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
4348
+ const { stdout } = await execFileAsync4("git", args, { maxBuffer: 10 * 1024 * 1024 });
3851
4349
  return stdout.trim();
3852
4350
  }
3853
4351
 
@@ -3881,6 +4379,19 @@ function buildBackgroundRunnerArgs(options) {
3881
4379
  if (options.model) {
3882
4380
  args.push("--model", options.model);
3883
4381
  }
4382
+ const providerId = options.providerId ?? options.provider;
4383
+ if (providerId) {
4384
+ args.push("--provider", providerId);
4385
+ }
4386
+ if (options.modelId) {
4387
+ args.push("--model-id", options.modelId);
4388
+ }
4389
+ if (options.modelVariant) {
4390
+ args.push("--model-variant", options.modelVariant);
4391
+ }
4392
+ if (options.reasoningEffort) {
4393
+ args.push("--reasoning-effort", options.reasoningEffort);
4394
+ }
3884
4395
  if (options.maxIterations !== void 0) {
3885
4396
  args.push("--max-iterations", String(options.maxIterations));
3886
4397
  }
@@ -3931,11 +4442,11 @@ function truncateProcessOutput(value) {
3931
4442
  }
3932
4443
 
3933
4444
  // src/git-worktree.ts
3934
- import { execFile as execFile4 } from "node:child_process";
4445
+ import { execFile as execFile5 } from "node:child_process";
3935
4446
  import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
3936
4447
  import path12 from "node:path";
3937
- import { promisify as promisify4 } from "node:util";
3938
- var execFileAsync4 = promisify4(execFile4);
4448
+ import { promisify as promisify5 } from "node:util";
4449
+ var execFileAsync5 = promisify5(execFile5);
3939
4450
  function needsGitWorktreeIsolation(workItem) {
3940
4451
  return (workItem.workKind ?? "implementation") === "implementation";
3941
4452
  }
@@ -3996,11 +4507,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
3996
4507
  }
3997
4508
  }
3998
4509
  async function gitOutput(cwd, args) {
3999
- const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
4510
+ const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
4000
4511
  return stdout.trim();
4001
4512
  }
4002
4513
  async function gitCommandSucceeds(cwd, args) {
4003
- return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
4514
+ return execFileAsync5("git", args, { cwd }).then(() => true, () => false);
4004
4515
  }
4005
4516
  async function pathExists(value) {
4006
4517
  return stat5(value).then(() => true, () => false);
@@ -4255,6 +4766,45 @@ sync.command("push").description("Push local brain changes to Amistio for review
4255
4766
  await materializeBrainDocuments(options.root, documents, { allowDirtyOverwrite: true });
4256
4767
  console.log(`Pushed ${documents.length} local document${documents.length === 1 ? "" : "s"} for web review.`);
4257
4768
  });
4769
+ sync.command("watch").description("Watch repository brain folders and auto-sync eligible local changes").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable watcher runner ID").option("--interval-seconds <seconds>", "Polling interval", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-file-kb <kb>", "Maximum Markdown/MDX file size to auto-sync", parsePositiveInteger, 256).option("--once", "Run one auto-sync cycle and exit").action(async (options) => {
4770
+ const context = await loadPairedApiContext(options.root, options.apiUrl);
4771
+ if (!context) {
4772
+ console.log("Repository is not paired. Run `amistio pair` first.");
4773
+ return;
4774
+ }
4775
+ if (!context.token) {
4776
+ console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
4777
+ process.exitCode = 1;
4778
+ return;
4779
+ }
4780
+ const runnerId = options.runnerId ?? stableRunnerId({
4781
+ accountId: context.metadata.amistioAccountId,
4782
+ projectId: context.metadata.amistioProjectId,
4783
+ repositoryLinkId: context.metadata.repositoryLinkId,
4784
+ machineId: runnerMachineId()
4785
+ });
4786
+ console.log(`Auto-sync watcher: ${runnerId}`);
4787
+ console.log(`Project: ${context.metadata.amistioProjectId}`);
4788
+ console.log(`Repository link: ${context.metadata.repositoryLinkId}`);
4789
+ let iterations = 0;
4790
+ while (true) {
4791
+ iterations += 1;
4792
+ const result = await runAutoSyncCycle({
4793
+ context,
4794
+ maxFileKb: options.maxFileKb,
4795
+ quietDisabled: false,
4796
+ root: options.root,
4797
+ runnerId
4798
+ });
4799
+ console.log(formatAutoSyncCycleResult(result));
4800
+ if (options.once || result.status === "disabled") return;
4801
+ if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
4802
+ console.log(`Auto-sync watcher stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
4803
+ return;
4804
+ }
4805
+ await delay(options.intervalSeconds * 1e3);
4806
+ }
4807
+ });
4258
4808
  var work = program.command("work").description("Inspect approved work items");
4259
4809
  work.command("list").description("List queued work without claiming it").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
4260
4810
  const context = await loadPairedApiContext(options.root, options.apiUrl);
@@ -4311,7 +4861,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
4311
4861
  }
4312
4862
  console.log("custom - pass --tool-command to use any other local runner command.");
4313
4863
  });
4314
- 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) => {
4864
+ 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("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).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) => {
4315
4865
  const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
4316
4866
  const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
4317
4867
  if (options.promptOut) {
@@ -4323,7 +4873,8 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4323
4873
  return;
4324
4874
  }
4325
4875
  const sessionPolicy = normalizeSessionPolicy(options.session);
4326
- const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
4876
+ const localModelConfig = localModelConfigOptions(options);
4877
+ const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...localModelConfig });
4327
4878
  console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
4328
4879
  const result = await runLocalTool({
4329
4880
  rootDir: options.root,
@@ -4331,7 +4882,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4331
4882
  tool: options.tool,
4332
4883
  invocationChannel: options.invocationChannel,
4333
4884
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
4334
- ...options.model ? { model: options.model } : {},
4885
+ ...localModelConfig,
4335
4886
  streamOutput: options.stream,
4336
4887
  ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
4337
4888
  });
@@ -4345,7 +4896,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
4345
4896
  process.exitCode = result.exitCode;
4346
4897
  }
4347
4898
  });
4348
- program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").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("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
4899
+ program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").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("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).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("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
4349
4900
  const context = await loadPairedApiContext(options.root, options.apiUrl);
4350
4901
  if (!context) {
4351
4902
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -4517,7 +5068,7 @@ runner.command("stop").description("Stop a background runner for the paired repo
4517
5068
  console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
4518
5069
  });
4519
5070
  var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
4520
- runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, 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("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
5071
+ runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, 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("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
4521
5072
  const context = await loadPairedApiContext(options.root, options.apiUrl);
4522
5073
  if (!context) {
4523
5074
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -4607,6 +5158,16 @@ runnerService.command("remove").description("Remove the startup service for this
4607
5158
  });
4608
5159
  async function runWatchIteration({ command, context, options, runnerId }) {
4609
5160
  try {
5161
+ if (options.watch && !options.dryRun) {
5162
+ await runAutoSyncCycle({
5163
+ context,
5164
+ maxFileKb: 256,
5165
+ quiet: true,
5166
+ quietDisabled: true,
5167
+ root: options.root,
5168
+ runnerId
5169
+ });
5170
+ }
4610
5171
  return await runNextWorkItem({
4611
5172
  apiClient: context.client,
4612
5173
  projectId: context.metadata.amistioProjectId,
@@ -4617,6 +5178,10 @@ async function runWatchIteration({ command, context, options, runnerId }) {
4617
5178
  ...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
4618
5179
  ...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
4619
5180
  ...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
5181
+ ...command.getOptionValueSource("provider") === "cli" && options.provider ? { explicitProviderId: options.provider } : {},
5182
+ ...command.getOptionValueSource("modelId") === "cli" && options.modelId ? { explicitModelId: options.modelId } : {},
5183
+ ...command.getOptionValueSource("modelVariant") === "cli" && options.modelVariant ? { explicitModelVariant: options.modelVariant } : {},
5184
+ ...command.getOptionValueSource("reasoningEffort") === "cli" && options.reasoningEffort ? { explicitReasoningEffort: options.reasoningEffort } : {},
4620
5185
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
4621
5186
  dryRun: Boolean(options.dryRun),
4622
5187
  stream: options.stream,
@@ -4661,6 +5226,64 @@ ${detail}`);
4661
5226
  return { status: "failed", exitCode: 1, message };
4662
5227
  }
4663
5228
  }
5229
+ async function runAutoSyncCycle({ context, maxFileKb, quiet, quietDisabled, root, runnerId }) {
5230
+ const projectId = context.metadata.amistioProjectId;
5231
+ const repositoryLinkId = context.metadata.repositoryLinkId;
5232
+ const heartbeatBase = runnerHeartbeatMetadata(void 0);
5233
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5234
+ try {
5235
+ const { repositoryLinks } = await context.client.listRepositoryLinks(projectId);
5236
+ const repositoryLink = repositoryLinks.find((link) => link.repositoryLinkId === repositoryLinkId && link.status !== "revoked");
5237
+ if (!repositoryLink) {
5238
+ const message2 = "Repository link is not active for auto-sync.";
5239
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", { ...heartbeatBase, autoSyncStatus: "blocked", autoSyncMessage: message2, autoSyncLastFailureAt: startedAt }).catch(() => void 0);
5240
+ return { status: "blocked", message: message2, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5241
+ }
5242
+ if (!repositoryLink.autoSyncEnabled) {
5243
+ const message2 = "Repository brain auto-sync is disabled.";
5244
+ if (!quietDisabled) {
5245
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "disabled", autoSyncMessage: message2 }).catch(() => void 0);
5246
+ }
5247
+ return { status: "disabled", message: message2, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5248
+ }
5249
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "active", autoSyncMessage: "Scanning repository brain folders.", autoSyncLastStartedAt: startedAt }).catch(() => void 0);
5250
+ const { documents: existingDocuments } = await context.client.listBrainDocuments(projectId);
5251
+ const collection = await collectAutoSyncDocumentsForPush(root, context.metadata, existingDocuments, { maxFileKb, syncedAt: startedAt });
5252
+ if (!collection.documents.length) {
5253
+ const skippedCount2 = collection.skipped.length;
5254
+ const message2 = skippedCount2 ? `No local brain changes pushed; ${skippedCount2} file${skippedCount2 === 1 ? "" : "s"} skipped or unchanged.` : "No local brain changes found.";
5255
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: "synced", autoSyncMessage: message2, autoSyncLastSuccessAt: startedAt, autoSyncPushedCount: 0, autoSyncSkippedCount: skippedCount2, autoSyncConflictCount: 0 }).catch(() => void 0);
5256
+ return { status: "synced", message: message2, pushedCount: 0, skippedCount: skippedCount2, conflictCount: 0, collection };
5257
+ }
5258
+ const { documents } = await context.client.pushBrainDocuments(projectId, collection.documents);
5259
+ const conflictedDocuments = documents.filter((document) => document.syncState === "conflicted" || document.status === "conflicted");
5260
+ const managedDocumentIds = new Set(collection.managedDocumentIds);
5261
+ const managedDocuments = documents.filter((document) => managedDocumentIds.has(document.documentId) && document.syncState !== "conflicted" && document.status !== "conflicted");
5262
+ if (managedDocuments.length) {
5263
+ await materializeBrainDocuments(root, managedDocuments, { allowDirtyOverwrite: true });
5264
+ }
5265
+ const conflictCount = conflictedDocuments.length;
5266
+ const skippedCount = collection.skipped.length;
5267
+ const pushedCount = documents.length - conflictCount;
5268
+ const status = conflictCount ? "conflicted" : "synced";
5269
+ const message = conflictCount ? `Auto-sync found ${conflictCount} conflict${conflictCount === 1 ? "" : "s"}.` : `Auto-sync pushed ${pushedCount} brain document${pushedCount === 1 ? "" : "s"}.`;
5270
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", { ...heartbeatBase, autoSyncStatus: status, autoSyncMessage: message, ...conflictCount ? { autoSyncLastFailureAt: startedAt } : { autoSyncLastSuccessAt: startedAt }, autoSyncPushedCount: pushedCount, autoSyncSkippedCount: skippedCount, autoSyncConflictCount: conflictCount }).catch(() => void 0);
5271
+ if (!quiet && conflictCount) {
5272
+ for (const document of conflictedDocuments) {
5273
+ console.log(`conflicted: ${document.repoPath} - web and repository revisions both changed`);
5274
+ }
5275
+ }
5276
+ return { status, message, pushedCount, skippedCount, conflictCount, collection };
5277
+ } catch (error) {
5278
+ const message = `Auto-sync failed: ${errorMessage3(error)}`;
5279
+ await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", { ...heartbeatBase, autoSyncStatus: "failed", autoSyncMessage: message, autoSyncLastFailureAt: startedAt }).catch(() => void 0);
5280
+ return { status: "failed", message, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
5281
+ }
5282
+ }
5283
+ function formatAutoSyncCycleResult(result) {
5284
+ const details = [`pushed ${result.pushedCount}`, `skipped ${result.skippedCount}`, `conflicts ${result.conflictCount}`];
5285
+ return `${result.message} (${details.join("; ")})`;
5286
+ }
4664
5287
  async function runNextWorkItem({
4665
5288
  apiClient,
4666
5289
  dryRun,
@@ -4671,6 +5294,10 @@ async function runNextWorkItem({
4671
5294
  sessionPolicy,
4672
5295
  stream,
4673
5296
  explicitModel,
5297
+ explicitModelId,
5298
+ explicitModelVariant,
5299
+ explicitProviderId,
5300
+ explicitReasoningEffort,
4674
5301
  explicitInvocationChannel,
4675
5302
  explicitTool,
4676
5303
  maxPreflightAttempts,
@@ -4685,6 +5312,10 @@ async function runNextWorkItem({
4685
5312
  projectId,
4686
5313
  ...explicitInvocationChannel ? { explicitInvocationChannel } : {},
4687
5314
  ...explicitModel ? { explicitModel } : {},
5315
+ ...explicitProviderId ? { explicitProviderId } : {},
5316
+ ...explicitModelId ? { explicitModelId } : {},
5317
+ ...explicitModelVariant ? { explicitModelVariant } : {},
5318
+ ...explicitReasoningEffort ? { explicitReasoningEffort } : {},
4688
5319
  ...explicitTool ? { explicitTool } : {},
4689
5320
  ...toolCommand ? { toolCommand } : {}
4690
5321
  });
@@ -4734,7 +5365,8 @@ async function runNextWorkItem({
4734
5365
  ...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
4735
5366
  ...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
4736
5367
  });
4737
- const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
5368
+ const resolvedModelConfig = toolConfigModelOptions(toolConfig);
5369
+ const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...resolvedModelConfig });
4738
5370
  const sessionContext = await prepareToolSession({
4739
5371
  apiClient,
4740
5372
  projectId,
@@ -4768,7 +5400,7 @@ async function runNextWorkItem({
4768
5400
  tool: toolConfig.tool,
4769
5401
  invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
4770
5402
  ...toolCommand ? { toolCommand } : {},
4771
- ...toolConfig.model ? { model: toolConfig.model } : {},
5403
+ ...resolvedModelConfig,
4772
5404
  streamOutput: stream,
4773
5405
  timeoutMs: toolTimeoutMs,
4774
5406
  ...sessionContext.toolSession ? {
@@ -5656,11 +6288,17 @@ function parseInvocationChannel(value) {
5656
6288
  }
5657
6289
  throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
5658
6290
  }
6291
+ function parseReasoningEffort(value) {
6292
+ if (value === "auto" || value === "low" || value === "medium" || value === "high" || value === "xhigh") {
6293
+ return value;
6294
+ }
6295
+ throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
6296
+ }
5659
6297
  function inferRepoName(root) {
5660
6298
  return path13.basename(path13.resolve(root)) || "repository";
5661
6299
  }
5662
6300
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
5663
- return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
6301
+ return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
5664
6302
  }
5665
6303
  function defaultApiUrl() {
5666
6304
  const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
@@ -5675,9 +6313,10 @@ function formatApiUrlFlag(apiUrl) {
5675
6313
  function formatShellArg(value) {
5676
6314
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
5677
6315
  }
5678
- async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
6316
+ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitModelId, explicitModelVariant, explicitProviderId, explicitReasoningEffort, explicitTool, projectId, toolCommand }) {
5679
6317
  const capabilities = toRunnerToolCapabilities(await detectLocalTools());
5680
6318
  if (toolCommand) {
6319
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
5681
6320
  return {
5682
6321
  ready: true,
5683
6322
  tool: explicitTool ?? "auto",
@@ -5688,33 +6327,42 @@ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, e
5688
6327
  requestedInvocationChannel: explicitInvocationChannel ?? "command",
5689
6328
  effectiveInvocationChannel: "command",
5690
6329
  ...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
5691
- ...explicitModel ? { model: explicitModel } : {},
6330
+ ...modelConfig2,
5692
6331
  message: "Using local custom tool command."
5693
6332
  };
5694
6333
  }
5695
6334
  if (explicitTool === "none") {
5696
- if (explicitModel) {
5697
- return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", model: explicitModel });
6335
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
6336
+ if (hasModelConfig(modelConfig2)) {
6337
+ return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "Model configuration cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
5698
6338
  }
5699
6339
  return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
5700
6340
  }
5701
6341
  if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
5702
- return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...explicitModel ? { model: explicitModel } : {} });
6342
+ const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
6343
+ return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
5703
6344
  }
5704
6345
  const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
5705
6346
  const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
5706
6347
  const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
5707
- const model = explicitModel ?? remotePreference?.model;
5708
- const source = explicitTool || explicitInvocationChannel || explicitModel ? "cli" : remotePreference?.source ?? "default";
5709
- return resolveRequestedTool({ capabilities, requestedInvocationChannel, requestedTool, source, ...model ? { model } : {} });
6348
+ const modelConfig = normalizeModelConfig({
6349
+ model: explicitModel ?? remotePreference?.model,
6350
+ providerId: explicitProviderId ?? remotePreference?.providerId,
6351
+ modelId: explicitModelId ?? remotePreference?.modelId,
6352
+ modelVariant: explicitModelVariant ?? remotePreference?.modelVariant,
6353
+ reasoningEffort: explicitReasoningEffort ?? remotePreference?.reasoningEffort
6354
+ });
6355
+ const source = explicitTool || explicitInvocationChannel || hasExplicitModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort }) ? "cli" : remotePreference?.source ?? "default";
6356
+ return resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source });
5710
6357
  }
5711
- function resolveRequestedTool({ capabilities, model, requestedInvocationChannel, requestedTool, source }) {
6358
+ function resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source }) {
6359
+ const needsModelSelection = hasModelConfig(modelConfig);
5712
6360
  if (requestedTool === "auto") {
5713
- const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!model || capability2.supportsModelSelection));
6361
+ const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!needsModelSelection || capability2.supportsModelSelection));
5714
6362
  if (!candidate) {
5715
6363
  const anyAvailable = capabilities.some((capability2) => capability2.available);
5716
6364
  const anyChannelAvailable = capabilities.some((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel));
5717
- const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : model ? "modelUnsupported" : "unavailable";
6365
+ const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : needsModelSelection ? "modelUnsupported" : "unavailable";
5718
6366
  return unavailableToolConfig({
5719
6367
  capabilities,
5720
6368
  source,
@@ -5722,23 +6370,31 @@ function resolveRequestedTool({ capabilities, model, requestedInvocationChannel,
5722
6370
  requestedTool,
5723
6371
  requestedInvocationChannel,
5724
6372
  tool: "auto",
5725
- ...model ? { model } : {},
5726
- 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."
6373
+ ...modelConfig,
6374
+ message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` : needsModelSelection ? "No installed local tool can honor the selected provider/model configuration." : "No supported local AI tool is installed."
5727
6375
  });
5728
6376
  }
5729
- return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...model ? { model } : {} };
6377
+ const modelResolution2 = resolveProviderModelConfig(candidate, modelConfig);
6378
+ if (!modelResolution2.ready) {
6379
+ return unavailableToolConfig({ capabilities, source, status: modelResolution2.status, requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), tool: "auto", ...modelConfig, message: modelResolution2.message });
6380
+ }
6381
+ return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...modelResolution2.config };
5730
6382
  }
5731
6383
  const capability = capabilities.find((candidate) => candidate.name === requestedTool);
5732
6384
  if (!capability?.available) {
5733
- return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
6385
+ return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...modelConfig, message: `${requestedTool} is selected but is not available on this runner.` });
5734
6386
  }
5735
6387
  if (!capabilitySupportsInvocationChannel(capability, requestedInvocationChannel)) {
5736
- 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.` });
6388
+ return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...modelConfig, message: `${requestedTool} is available but does not support ${requestedInvocationChannel} invocation on this runner.` });
6389
+ }
6390
+ if (needsModelSelection && !capability.supportsModelSelection) {
6391
+ return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, ...modelConfig, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
5737
6392
  }
5738
- if (model && !capability.supportsModelSelection) {
5739
- 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.` });
6393
+ const modelResolution = resolveProviderModelConfig(capability, modelConfig);
6394
+ if (!modelResolution.ready) {
6395
+ return unavailableToolConfig({ capabilities, source, status: modelResolution.status, requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, ...modelConfig, message: modelResolution.message });
5740
6396
  }
5741
- return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...model ? { model } : {} };
6397
+ return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...modelResolution.config };
5742
6398
  }
5743
6399
  function unavailableToolConfig(input) {
5744
6400
  return {
@@ -5752,9 +6408,116 @@ function unavailableToolConfig(input) {
5752
6408
  ...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
5753
6409
  ...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
5754
6410
  ...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
5755
- ...input.model ? { model: input.model } : {}
6411
+ ...normalizeModelConfig(input)
5756
6412
  };
5757
6413
  }
6414
+ function resolveProviderModelConfig(capability, modelConfig) {
6415
+ const normalized = normalizeModelConfig(modelConfig);
6416
+ if (!hasModelConfig(normalized)) {
6417
+ return { ready: true, config: {} };
6418
+ }
6419
+ if (!capability.supportsModelSelection) {
6420
+ return { ready: false, status: "modelUnsupported", message: `${capability.name} does not support Amistio model selection yet.` };
6421
+ }
6422
+ if (!normalized.providerId && !normalized.modelId && !normalized.modelVariant && (!normalized.reasoningEffort || normalized.reasoningEffort === "auto")) {
6423
+ return { ready: true, config: normalized };
6424
+ }
6425
+ const catalog = capability.providerCatalog;
6426
+ if (!catalog) {
6427
+ return { ready: false, status: "modelUnsupported", message: `${capability.name} does not report a provider model catalog yet.` };
6428
+ }
6429
+ const providerId = normalized.providerId ?? inferProviderIdForModel(catalog, normalized.modelId ?? normalized.model);
6430
+ if (!providerId) {
6431
+ return { ready: false, status: "modelUnsupported", message: "Select a provider id with the provider-backed model preference." };
6432
+ }
6433
+ const provider = catalog[providerId];
6434
+ if (!provider) {
6435
+ return { ready: false, status: "modelUnsupported", message: `${providerId} is not available in ${capability.name}'s provider catalog.` };
6436
+ }
6437
+ const modelId = normalized.modelId ?? inferModelId(provider.models, normalized.model);
6438
+ if (!modelId) {
6439
+ return { ready: false, status: "modelUnsupported", message: "Select a model id with the provider-backed model preference." };
6440
+ }
6441
+ const model = provider.models[modelId];
6442
+ if (!model) {
6443
+ return { ready: false, status: "modelUnsupported", message: `${providerId}/${modelId} is not available in ${capability.name}'s provider catalog.` };
6444
+ }
6445
+ if (normalized.modelVariant) {
6446
+ const variant = model.variants?.[normalized.modelVariant];
6447
+ if (!variant || variant.disabled) {
6448
+ return { ready: false, status: "variantUnsupported", message: `${providerId}/${modelId} does not support variant ${normalized.modelVariant}.` };
6449
+ }
6450
+ }
6451
+ if (normalized.reasoningEffort && normalized.reasoningEffort !== "auto") {
6452
+ const supportedEfforts = model.supportedReasoningEfforts ?? (model.reasoning ? ["auto", "low", "medium", "high", "xhigh"] : []);
6453
+ if (!supportedEfforts.includes(normalized.reasoningEffort)) {
6454
+ return { ready: false, status: "reasoningUnsupported", message: `${providerId}/${modelId} does not support ${normalized.reasoningEffort} reasoning effort.` };
6455
+ }
6456
+ }
6457
+ return {
6458
+ ready: true,
6459
+ config: {
6460
+ model: normalized.model ?? `${providerId}/${modelId}`,
6461
+ providerId,
6462
+ modelId,
6463
+ ...normalized.modelVariant ? { modelVariant: normalized.modelVariant } : {},
6464
+ ...normalized.reasoningEffort ? { reasoningEffort: normalized.reasoningEffort } : {}
6465
+ }
6466
+ };
6467
+ }
6468
+ function normalizeModelConfig(config) {
6469
+ const model = cleanPreferenceText(config.model);
6470
+ const providerId = cleanPreferenceText(config.providerId);
6471
+ const modelId = cleanPreferenceText(config.modelId);
6472
+ const modelVariant = cleanPreferenceText(config.modelVariant);
6473
+ return {
6474
+ ...model ? { model } : {},
6475
+ ...providerId ? { providerId } : {},
6476
+ ...modelId ? { modelId } : {},
6477
+ ...modelVariant ? { modelVariant } : {},
6478
+ ...config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}
6479
+ };
6480
+ }
6481
+ function hasExplicitModelConfig(config) {
6482
+ return hasModelConfig(normalizeModelConfig(config));
6483
+ }
6484
+ function hasModelConfig(config) {
6485
+ return Boolean(config.model || config.providerId || config.modelId || config.modelVariant || config.reasoningEffort);
6486
+ }
6487
+ function cleanPreferenceText(value) {
6488
+ const trimmed = value?.trim();
6489
+ return trimmed ? trimmed : void 0;
6490
+ }
6491
+ function inferProviderIdForModel(catalog, model) {
6492
+ if (!model) return void 0;
6493
+ if (model.includes("/")) {
6494
+ const [providerId, modelId] = model.split("/", 2);
6495
+ if (providerId && modelId && catalog[providerId]?.models[modelId]) return providerId;
6496
+ }
6497
+ return Object.entries(catalog).find(([, provider]) => Boolean(provider.models[model]))?.[0];
6498
+ }
6499
+ function inferModelId(models, model) {
6500
+ if (!model) return void 0;
6501
+ if (models[model]) return model;
6502
+ const slashIndex = model.indexOf("/");
6503
+ if (slashIndex !== -1) {
6504
+ const modelId = model.slice(slashIndex + 1);
6505
+ if (models[modelId]) return modelId;
6506
+ }
6507
+ return void 0;
6508
+ }
6509
+ function toolConfigModelOptions(toolConfig) {
6510
+ return normalizeModelConfig(toolConfig);
6511
+ }
6512
+ function localModelConfigOptions(options) {
6513
+ return normalizeModelConfig({
6514
+ model: options.model,
6515
+ providerId: options.providerId ?? options.provider,
6516
+ modelId: options.modelId,
6517
+ modelVariant: options.modelVariant,
6518
+ reasoningEffort: options.reasoningEffort
6519
+ });
6520
+ }
5758
6521
  function capabilitySupportsInvocationChannel(capability, channel) {
5759
6522
  if (channel === "auto") return capability.available;
5760
6523
  if (channel === "sdk") return capability.sdkAvailable;
@@ -5787,6 +6550,8 @@ function runnerIsolationCapabilityMetadata() {
5787
6550
  };
5788
6551
  }
5789
6552
  function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
6553
+ const modelConfig = toolConfig ? toolConfigModelOptions(toolConfig) : {};
6554
+ const effectiveModelConfig = toolConfig?.ready ? modelConfig : {};
5790
6555
  return {
5791
6556
  version: CLI_VERSION,
5792
6557
  mode,
@@ -5796,16 +6561,24 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
5796
6561
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
5797
6562
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
5798
6563
  ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
6564
+ ...modelConfig.providerId ? { requestedProviderId: modelConfig.providerId } : {},
6565
+ ...modelConfig.modelId ? { requestedModelId: modelConfig.modelId } : {},
6566
+ ...modelConfig.modelVariant ? { requestedModelVariant: modelConfig.modelVariant } : {},
6567
+ ...modelConfig.reasoningEffort ? { requestedReasoningEffort: modelConfig.reasoningEffort } : {},
5799
6568
  ...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
5800
6569
  ...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
5801
- ...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
6570
+ ...effectiveModelConfig.model ? { effectiveModel: effectiveModelConfig.model } : {},
6571
+ ...effectiveModelConfig.providerId ? { effectiveProviderId: effectiveModelConfig.providerId } : {},
6572
+ ...effectiveModelConfig.modelId ? { effectiveModelId: effectiveModelConfig.modelId } : {},
6573
+ ...effectiveModelConfig.modelVariant ? { effectiveModelVariant: effectiveModelConfig.modelVariant } : {},
6574
+ ...effectiveModelConfig.reasoningEffort ? { effectiveReasoningEffort: effectiveModelConfig.reasoningEffort } : {},
5802
6575
  ...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
5803
6576
  ...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
5804
6577
  ...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
5805
6578
  };
5806
6579
  }
5807
6580
  function runnerMachineId() {
5808
- return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
6581
+ return createHash7("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
5809
6582
  }
5810
6583
  async function delay(milliseconds) {
5811
6584
  await new Promise((resolve) => setTimeout(resolve, milliseconds));