@amistio/cli 0.1.7 → 0.1.9
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/README.md +14 -2
- package/dist/index.js +1132 -156
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
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
|
|
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({
|
|
@@ -410,6 +471,7 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
|
410
471
|
projectId: z.string().min(1),
|
|
411
472
|
runnerCredentialId: z.string().min(1),
|
|
412
473
|
repositoryLinkId: z.string().min(1),
|
|
474
|
+
runnerId: z.string().min(1).optional(),
|
|
413
475
|
pairedByUserId: z.string().min(1).optional(),
|
|
414
476
|
machineId: z.string().min(1).optional(),
|
|
415
477
|
tokenHash: z.string().min(32),
|
|
@@ -719,6 +781,8 @@ var pairingSessionItemSchema = baseItemSchema.extend({
|
|
|
719
781
|
projectId: z.string().min(1),
|
|
720
782
|
createdByUserId: z.string().min(1),
|
|
721
783
|
expiresAt: isoDateTimeSchema,
|
|
784
|
+
failedAttemptCount: z.number().int().nonnegative().optional(),
|
|
785
|
+
lastFailedAttemptAt: isoDateTimeSchema.optional(),
|
|
722
786
|
status: z.enum(["pending", "confirmed", "expired", "revoked"])
|
|
723
787
|
});
|
|
724
788
|
var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
@@ -1433,6 +1497,13 @@ var ApiClient = class {
|
|
|
1433
1497
|
{ method: "GET" }
|
|
1434
1498
|
);
|
|
1435
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
|
+
}
|
|
1436
1507
|
async listPlanReviewMessages(projectId, documentId) {
|
|
1437
1508
|
const suffix = documentId ? `?documentId=${encodeURIComponent(documentId)}` : "";
|
|
1438
1509
|
return this.request(
|
|
@@ -1500,6 +1571,10 @@ var ApiClient = class {
|
|
|
1500
1571
|
tool: runnerToolSelectionSchema,
|
|
1501
1572
|
invocationChannel: runnerInvocationChannelSchema,
|
|
1502
1573
|
model: z3.string().optional(),
|
|
1574
|
+
providerId: z3.string().optional(),
|
|
1575
|
+
modelId: z3.string().optional(),
|
|
1576
|
+
modelVariant: z3.string().optional(),
|
|
1577
|
+
reasoningEffort: runnerReasoningEffortSchema.optional(),
|
|
1503
1578
|
source: runnerPreferenceSourceSchema
|
|
1504
1579
|
})
|
|
1505
1580
|
}),
|
|
@@ -1722,10 +1797,87 @@ async function writePromptFile(filePath, prompt) {
|
|
|
1722
1797
|
|
|
1723
1798
|
// src/local-tool-runner.ts
|
|
1724
1799
|
import { spawn } from "node:child_process";
|
|
1725
|
-
import { mkdtemp, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
1800
|
+
import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
1726
1801
|
import os2 from "node:os";
|
|
1727
1802
|
import path5 from "node:path";
|
|
1728
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
|
+
}
|
|
1729
1881
|
var localToolAdapters = [
|
|
1730
1882
|
{
|
|
1731
1883
|
name: "opencode",
|
|
@@ -1734,13 +1886,15 @@ var localToolAdapters = [
|
|
|
1734
1886
|
sdkDisplayCommand: "@opencode-ai/sdk createOpencode().client.session.prompt()",
|
|
1735
1887
|
sdkRequiresExecutable: true,
|
|
1736
1888
|
executable: "opencode",
|
|
1889
|
+
supportsModelSelection: true,
|
|
1737
1890
|
supportsSessionReuse: true,
|
|
1738
1891
|
resumabilityScope: "localMachine",
|
|
1892
|
+
providerCatalog: builtInProviderCatalogs.opencode,
|
|
1739
1893
|
runWithSdk: runOpencodeSdk,
|
|
1740
|
-
buildInvocation: ({ prompt }) => ({
|
|
1894
|
+
buildInvocation: ({ prompt, model }) => ({
|
|
1741
1895
|
command: "opencode",
|
|
1742
|
-
args: ["run", prompt],
|
|
1743
|
-
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>"
|
|
1744
1898
|
})
|
|
1745
1899
|
},
|
|
1746
1900
|
{
|
|
@@ -1749,13 +1903,15 @@ var localToolAdapters = [
|
|
|
1749
1903
|
sdkPackageName: "@anthropic-ai/claude-agent-sdk",
|
|
1750
1904
|
sdkDisplayCommand: "@anthropic-ai/claude-agent-sdk query()",
|
|
1751
1905
|
executable: "claude",
|
|
1906
|
+
supportsModelSelection: true,
|
|
1752
1907
|
supportsSessionReuse: false,
|
|
1753
1908
|
resumabilityScope: "none",
|
|
1909
|
+
providerCatalog: builtInProviderCatalogs.claude,
|
|
1754
1910
|
runWithSdk: runClaudeSdk,
|
|
1755
|
-
buildInvocation: ({ prompt }) => ({
|
|
1911
|
+
buildInvocation: ({ prompt, model }) => ({
|
|
1756
1912
|
command: "claude",
|
|
1757
|
-
args: ["-p", prompt],
|
|
1758
|
-
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>"
|
|
1759
1915
|
})
|
|
1760
1916
|
},
|
|
1761
1917
|
{
|
|
@@ -1764,13 +1920,15 @@ var localToolAdapters = [
|
|
|
1764
1920
|
sdkPackageName: "@openai/codex-sdk",
|
|
1765
1921
|
sdkDisplayCommand: "@openai/codex-sdk Codex.startThread().run()",
|
|
1766
1922
|
executable: "codex",
|
|
1923
|
+
supportsModelSelection: true,
|
|
1767
1924
|
supportsSessionReuse: false,
|
|
1768
1925
|
resumabilityScope: "none",
|
|
1926
|
+
providerCatalog: builtInProviderCatalogs.codex,
|
|
1769
1927
|
runWithSdk: runCodexSdk,
|
|
1770
|
-
buildInvocation: ({ prompt }) => ({
|
|
1928
|
+
buildInvocation: ({ prompt, model }) => ({
|
|
1771
1929
|
command: "codex",
|
|
1772
|
-
args: ["exec", prompt],
|
|
1773
|
-
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>"
|
|
1774
1932
|
})
|
|
1775
1933
|
},
|
|
1776
1934
|
{
|
|
@@ -1781,18 +1939,21 @@ var localToolAdapters = [
|
|
|
1781
1939
|
supportsModelSelection: true,
|
|
1782
1940
|
supportsSessionReuse: false,
|
|
1783
1941
|
resumabilityScope: "none",
|
|
1942
|
+
providerCatalog: builtInProviderCatalogs.copilot,
|
|
1784
1943
|
runWithSdk: runCopilotSdk
|
|
1785
1944
|
},
|
|
1786
1945
|
{
|
|
1787
1946
|
name: "gemini",
|
|
1788
1947
|
description: "Gemini CLI adapter using prompt mode.",
|
|
1789
1948
|
executable: "gemini",
|
|
1949
|
+
supportsModelSelection: true,
|
|
1790
1950
|
supportsSessionReuse: false,
|
|
1791
1951
|
resumabilityScope: "none",
|
|
1792
|
-
|
|
1952
|
+
providerCatalog: builtInProviderCatalogs.gemini,
|
|
1953
|
+
buildInvocation: ({ prompt, model }) => ({
|
|
1793
1954
|
command: "gemini",
|
|
1794
|
-
args: ["-p", prompt],
|
|
1795
|
-
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>"
|
|
1796
1957
|
})
|
|
1797
1958
|
},
|
|
1798
1959
|
{
|
|
@@ -1828,6 +1989,7 @@ async function detectLocalTools() {
|
|
|
1828
1989
|
localToolAdapters.map(async (adapter) => {
|
|
1829
1990
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
1830
1991
|
const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
|
|
1992
|
+
const providerCatalog = await detectProviderCatalog(adapter);
|
|
1831
1993
|
return {
|
|
1832
1994
|
name: adapter.name,
|
|
1833
1995
|
description: adapter.description,
|
|
@@ -1837,7 +1999,8 @@ async function detectLocalTools() {
|
|
|
1837
1999
|
execution: sdkAvailable ? "sdk" : commandAvailable ? "command" : "unavailable",
|
|
1838
2000
|
supportsSessionReuse: Boolean(adapter.supportsSessionReuse),
|
|
1839
2001
|
resumabilityScope: adapter.resumabilityScope ?? "none",
|
|
1840
|
-
supportsModelSelection: Boolean(adapter.supportsModelSelection)
|
|
2002
|
+
supportsModelSelection: Boolean(adapter.supportsModelSelection),
|
|
2003
|
+
...providerCatalog ? { providerCatalog } : {}
|
|
1841
2004
|
};
|
|
1842
2005
|
})
|
|
1843
2006
|
);
|
|
@@ -1846,6 +2009,7 @@ async function runLocalTool(options) {
|
|
|
1846
2009
|
const promptTempDir = await mkdtemp(path5.join(os2.tmpdir(), "amistio-prompt-"));
|
|
1847
2010
|
const promptFilePath = path5.join(promptTempDir, "prompt.md");
|
|
1848
2011
|
await writeFile4(promptFilePath, options.prompt, "utf8");
|
|
2012
|
+
const modelConfig = normalizeModelOptions(options);
|
|
1849
2013
|
try {
|
|
1850
2014
|
const runnerOptions = {
|
|
1851
2015
|
rootDir: options.rootDir,
|
|
@@ -1853,7 +2017,7 @@ async function runLocalTool(options) {
|
|
|
1853
2017
|
promptFilePath,
|
|
1854
2018
|
tool: options.tool ?? "auto",
|
|
1855
2019
|
invocationChannel: options.invocationChannel ?? "auto",
|
|
1856
|
-
...
|
|
2020
|
+
...modelConfig
|
|
1857
2021
|
};
|
|
1858
2022
|
if (options.toolCommand) {
|
|
1859
2023
|
runnerOptions.toolCommand = options.toolCommand;
|
|
@@ -1864,14 +2028,15 @@ async function runLocalTool(options) {
|
|
|
1864
2028
|
prompt: options.prompt,
|
|
1865
2029
|
promptFilePath,
|
|
1866
2030
|
streamOutput: Boolean(options.streamOutput),
|
|
2031
|
+
...modelConfig,
|
|
1867
2032
|
...options.session ? { session: options.session } : {}
|
|
1868
|
-
});
|
|
2033
|
+
}, options.timeoutMs);
|
|
1869
2034
|
return {
|
|
1870
2035
|
toolName: runner2.toolName,
|
|
1871
2036
|
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1872
2037
|
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1873
2038
|
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1874
|
-
...
|
|
2039
|
+
...modelConfig,
|
|
1875
2040
|
...result
|
|
1876
2041
|
};
|
|
1877
2042
|
} finally {
|
|
@@ -1880,13 +2045,14 @@ async function runLocalTool(options) {
|
|
|
1880
2045
|
}
|
|
1881
2046
|
async function createToolRunPreview(options) {
|
|
1882
2047
|
const promptFilePath = path5.join(os2.tmpdir(), "amistio-generated-prompt.md");
|
|
2048
|
+
const modelConfig = normalizeModelOptions(options);
|
|
1883
2049
|
const runnerOptions = {
|
|
1884
2050
|
rootDir: options.rootDir,
|
|
1885
2051
|
prompt: options.prompt,
|
|
1886
2052
|
promptFilePath,
|
|
1887
2053
|
tool: options.tool ?? "auto",
|
|
1888
2054
|
invocationChannel: options.invocationChannel ?? "auto",
|
|
1889
|
-
...
|
|
2055
|
+
...modelConfig
|
|
1890
2056
|
};
|
|
1891
2057
|
if (options.toolCommand) {
|
|
1892
2058
|
runnerOptions.toolCommand = options.toolCommand;
|
|
@@ -1897,7 +2063,20 @@ async function createToolRunPreview(options) {
|
|
|
1897
2063
|
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1898
2064
|
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1899
2065
|
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1900
|
-
...
|
|
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 } : {}
|
|
1901
2080
|
};
|
|
1902
2081
|
}
|
|
1903
2082
|
function createCustomToolInvocation(commandTemplate, input) {
|
|
@@ -1922,9 +2101,13 @@ async function createToolRunner(options) {
|
|
|
1922
2101
|
if (tool === "none") {
|
|
1923
2102
|
throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
|
|
1924
2103
|
}
|
|
1925
|
-
const
|
|
1926
|
-
if (
|
|
1927
|
-
throw new Error(
|
|
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.`);
|
|
1928
2111
|
}
|
|
1929
2112
|
if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
|
|
1930
2113
|
return {
|
|
@@ -1950,16 +2133,16 @@ async function createToolRunner(options) {
|
|
|
1950
2133
|
}
|
|
1951
2134
|
throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
|
|
1952
2135
|
}
|
|
1953
|
-
async function executeToolRunner(runner2, input) {
|
|
2136
|
+
async function executeToolRunner(runner2, input, timeoutMs) {
|
|
1954
2137
|
if (runner2.kind === "command") {
|
|
1955
|
-
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput);
|
|
2138
|
+
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput, timeoutMs);
|
|
1956
2139
|
}
|
|
1957
2140
|
try {
|
|
1958
|
-
return await runner2.adapter.runWithSdk(input);
|
|
2141
|
+
return await withTimeout(runner2.adapter.runWithSdk(input), timeoutMs, runner2.displayCommand);
|
|
1959
2142
|
} catch (error) {
|
|
1960
2143
|
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1961
2144
|
const fallback = runner2.adapter.buildInvocation(input);
|
|
1962
|
-
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
|
|
2145
|
+
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput, timeoutMs);
|
|
1963
2146
|
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
|
|
1964
2147
|
return {
|
|
1965
2148
|
...result,
|
|
@@ -2048,7 +2231,148 @@ async function commandExists(command) {
|
|
|
2048
2231
|
lookup.on("close", (exitCode) => resolve(exitCode === 0));
|
|
2049
2232
|
});
|
|
2050
2233
|
}
|
|
2051
|
-
async function
|
|
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
|
+
}
|
|
2375
|
+
async function executeToolInvocation(invocation, rootDir, streamOutput, timeoutMs) {
|
|
2052
2376
|
return new Promise((resolve, reject) => {
|
|
2053
2377
|
const child = spawn(invocation.command, invocation.args, {
|
|
2054
2378
|
cwd: rootDir,
|
|
@@ -2058,7 +2382,33 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
|
|
|
2058
2382
|
});
|
|
2059
2383
|
let stdout = "";
|
|
2060
2384
|
let stderr = "";
|
|
2061
|
-
|
|
2385
|
+
let settled = false;
|
|
2386
|
+
let forceKillTimer;
|
|
2387
|
+
const timeout = timeoutMs && timeoutMs > 0 ? setTimeout(() => {
|
|
2388
|
+
if (settled) return;
|
|
2389
|
+
stderr += `${toolTimeoutMessage(invocation.displayCommand, timeoutMs)}
|
|
2390
|
+
`;
|
|
2391
|
+
child.kill("SIGTERM");
|
|
2392
|
+
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
2393
|
+
forceKillTimer.unref?.();
|
|
2394
|
+
rejectOnce(new Error(toolTimeoutMessage(invocation.displayCommand, timeoutMs)));
|
|
2395
|
+
}, timeoutMs) : void 0;
|
|
2396
|
+
timeout?.unref?.();
|
|
2397
|
+
const resolveOnce = (value) => {
|
|
2398
|
+
if (settled) return;
|
|
2399
|
+
settled = true;
|
|
2400
|
+
if (timeout) clearTimeout(timeout);
|
|
2401
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2402
|
+
resolve(value);
|
|
2403
|
+
};
|
|
2404
|
+
const rejectOnce = (error) => {
|
|
2405
|
+
if (settled) return;
|
|
2406
|
+
settled = true;
|
|
2407
|
+
if (timeout) clearTimeout(timeout);
|
|
2408
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2409
|
+
reject(error);
|
|
2410
|
+
};
|
|
2411
|
+
child.on("error", rejectOnce);
|
|
2062
2412
|
child.stdout.setEncoding("utf8");
|
|
2063
2413
|
child.stderr.setEncoding("utf8");
|
|
2064
2414
|
child.stdout.on("data", (chunk) => {
|
|
@@ -2079,10 +2429,40 @@ async function executeToolInvocation(invocation, rootDir, streamOutput) {
|
|
|
2079
2429
|
}
|
|
2080
2430
|
child.stdin.end();
|
|
2081
2431
|
child.on("close", (exitCode) => {
|
|
2082
|
-
|
|
2432
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2433
|
+
resolveOnce({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
2083
2434
|
});
|
|
2084
2435
|
});
|
|
2085
2436
|
}
|
|
2437
|
+
async function withTimeout(promise, timeoutMs, displayCommand) {
|
|
2438
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
2439
|
+
return promise;
|
|
2440
|
+
}
|
|
2441
|
+
return new Promise((resolve, reject) => {
|
|
2442
|
+
const timeout = setTimeout(() => reject(new Error(toolTimeoutMessage(displayCommand, timeoutMs))), timeoutMs);
|
|
2443
|
+
timeout.unref?.();
|
|
2444
|
+
promise.then(
|
|
2445
|
+
(value) => {
|
|
2446
|
+
clearTimeout(timeout);
|
|
2447
|
+
resolve(value);
|
|
2448
|
+
},
|
|
2449
|
+
(error) => {
|
|
2450
|
+
clearTimeout(timeout);
|
|
2451
|
+
reject(error);
|
|
2452
|
+
}
|
|
2453
|
+
);
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
function toolTimeoutMessage(displayCommand, timeoutMs) {
|
|
2457
|
+
return `Local tool timed out after ${formatTimeoutDuration(timeoutMs)}: ${displayCommand}`;
|
|
2458
|
+
}
|
|
2459
|
+
function formatTimeoutDuration(timeoutMs) {
|
|
2460
|
+
if (timeoutMs < 1e3) {
|
|
2461
|
+
return `${timeoutMs}ms`;
|
|
2462
|
+
}
|
|
2463
|
+
const seconds = timeoutMs / 1e3;
|
|
2464
|
+
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
|
|
2465
|
+
}
|
|
2086
2466
|
async function runOpencodeSdk(input) {
|
|
2087
2467
|
const { createOpencode } = await import("@opencode-ai/sdk");
|
|
2088
2468
|
const previousDirectory = process.cwd();
|
|
@@ -2170,7 +2550,7 @@ async function runCodexSdk(input) {
|
|
|
2170
2550
|
return { exitCode: 0, stdout: result.finalResponse, stderr: "" };
|
|
2171
2551
|
}
|
|
2172
2552
|
async function runCopilotSdk(input) {
|
|
2173
|
-
const { CopilotClient
|
|
2553
|
+
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
2174
2554
|
const client = new CopilotClient({
|
|
2175
2555
|
cwd: input.rootDir,
|
|
2176
2556
|
logLevel: "error"
|
|
@@ -2183,7 +2563,7 @@ async function runCopilotSdk(input) {
|
|
|
2183
2563
|
workingDirectory: input.rootDir,
|
|
2184
2564
|
enableConfigDiscovery: true,
|
|
2185
2565
|
streaming: input.streamOutput,
|
|
2186
|
-
onPermissionRequest:
|
|
2566
|
+
onPermissionRequest: createCopilotPermissionHandler()
|
|
2187
2567
|
});
|
|
2188
2568
|
try {
|
|
2189
2569
|
let streamedOutput = "";
|
|
@@ -2205,15 +2585,24 @@ async function runCopilotSdk(input) {
|
|
|
2205
2585
|
await client.stop();
|
|
2206
2586
|
}
|
|
2207
2587
|
}
|
|
2588
|
+
function createCopilotPermissionHandler(env = process.env) {
|
|
2589
|
+
const allowAllPermissions = isCopilotApproveAllEnabled(env);
|
|
2590
|
+
return (request) => {
|
|
2591
|
+
if (allowAllPermissions || request.kind === "read") {
|
|
2592
|
+
return { kind: "approve-once" };
|
|
2593
|
+
}
|
|
2594
|
+
return { kind: "reject" };
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
function isCopilotApproveAllEnabled(env = process.env) {
|
|
2598
|
+
return /^(1|true|yes)$/i.test(env.AMISTIO_COPILOT_APPROVE_ALL ?? "");
|
|
2599
|
+
}
|
|
2208
2600
|
function extractTextParts(parts) {
|
|
2209
2601
|
if (!Array.isArray(parts)) {
|
|
2210
2602
|
return "";
|
|
2211
2603
|
}
|
|
2212
2604
|
return parts.filter(isRecord).map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
|
|
2213
2605
|
}
|
|
2214
|
-
function isRecord(value) {
|
|
2215
|
-
return typeof value === "object" && value !== null;
|
|
2216
|
-
}
|
|
2217
2606
|
function errorMessage(error) {
|
|
2218
2607
|
return error instanceof Error ? error.message : String(error);
|
|
2219
2608
|
}
|
|
@@ -2225,7 +2614,7 @@ function shellQuote(value) {
|
|
|
2225
2614
|
import { spawn as spawn2 } from "node:child_process";
|
|
2226
2615
|
import { createHash as createHash2 } from "node:crypto";
|
|
2227
2616
|
import { openSync } from "node:fs";
|
|
2228
|
-
import { mkdir as mkdir5, readdir as readdir2, readFile as
|
|
2617
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
|
|
2229
2618
|
import os3 from "node:os";
|
|
2230
2619
|
import path6 from "node:path";
|
|
2231
2620
|
function currentRunnerMode() {
|
|
@@ -2392,7 +2781,7 @@ function runnerDaemonKey(input) {
|
|
|
2392
2781
|
}
|
|
2393
2782
|
async function readRunnerDaemonMetadataFile(filePath) {
|
|
2394
2783
|
try {
|
|
2395
|
-
const parsed = JSON.parse(await
|
|
2784
|
+
const parsed = JSON.parse(await readFile4(filePath, "utf8"));
|
|
2396
2785
|
if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
|
|
2397
2786
|
return void 0;
|
|
2398
2787
|
}
|
|
@@ -2405,7 +2794,7 @@ async function readRunnerDaemonMetadataFile(filePath) {
|
|
|
2405
2794
|
// src/runner-service.ts
|
|
2406
2795
|
import { spawn as spawn3 } from "node:child_process";
|
|
2407
2796
|
import { createHash as createHash3 } from "node:crypto";
|
|
2408
|
-
import { mkdir as mkdir6, readFile as
|
|
2797
|
+
import { mkdir as mkdir6, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2409
2798
|
import os4 from "node:os";
|
|
2410
2799
|
import path7 from "node:path";
|
|
2411
2800
|
function detectRunnerServicePlatform(platform = process.platform) {
|
|
@@ -2471,7 +2860,7 @@ async function removeRunnerService(input) {
|
|
|
2471
2860
|
}
|
|
2472
2861
|
async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
2473
2862
|
try {
|
|
2474
|
-
const parsed = JSON.parse(await
|
|
2863
|
+
const parsed = JSON.parse(await readFile5(runnerServiceMetadataPath(input, metadataDir), "utf8"));
|
|
2475
2864
|
if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
|
|
2476
2865
|
return void 0;
|
|
2477
2866
|
}
|
|
@@ -2751,10 +3140,28 @@ function tokens(value) {
|
|
|
2751
3140
|
}
|
|
2752
3141
|
|
|
2753
3142
|
// src/sync.ts
|
|
2754
|
-
import {
|
|
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";
|
|
2755
3146
|
import path8 from "node:path";
|
|
3147
|
+
import { promisify as promisify3 } from "node:util";
|
|
3148
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2756
3149
|
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2757
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;
|
|
2758
3165
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
2759
3166
|
const localDocuments = await readLocalSyncedDocuments(rootDir);
|
|
2760
3167
|
const normalizedWebDocuments = webDocuments.map((document) => ({ ...document, repoPath: canonicalControlPlaneRepoPath(document.repoPath) }));
|
|
@@ -2831,7 +3238,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2831
3238
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
2832
3239
|
const documents = [];
|
|
2833
3240
|
for (const fullPath of markdownFiles) {
|
|
2834
|
-
const raw = await
|
|
3241
|
+
const raw = await readFile6(fullPath, "utf8");
|
|
2835
3242
|
const parsed = parseSyncedMarkdown(raw);
|
|
2836
3243
|
if (!parsed) {
|
|
2837
3244
|
continue;
|
|
@@ -2914,6 +3321,29 @@ async function collectDirtyDocumentsForPush(rootDir, metadata) {
|
|
|
2914
3321
|
});
|
|
2915
3322
|
});
|
|
2916
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
|
+
}
|
|
2917
3347
|
function createSyncedDocumentMarkdown(document) {
|
|
2918
3348
|
return [
|
|
2919
3349
|
"---",
|
|
@@ -2943,7 +3373,7 @@ function parseSyncedMarkdown(content) {
|
|
|
2943
3373
|
}
|
|
2944
3374
|
async function readExistingSyncedDocument(fullPath) {
|
|
2945
3375
|
try {
|
|
2946
|
-
const raw = await
|
|
3376
|
+
const raw = await readFile6(fullPath, "utf8");
|
|
2947
3377
|
const parsed = parseSyncedMarkdown(raw);
|
|
2948
3378
|
if (!parsed) {
|
|
2949
3379
|
return { exists: true };
|
|
@@ -3020,6 +3450,145 @@ function inferTitle(content, repoPath) {
|
|
|
3020
3450
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
3021
3451
|
return heading || path8.basename(repoPath, path8.extname(repoPath));
|
|
3022
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
|
+
}
|
|
3023
3592
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
3024
3593
|
return {
|
|
3025
3594
|
amistioDocumentId: frontmatter.amistioDocumentId,
|
|
@@ -3039,7 +3608,7 @@ async function exists2(filePath) {
|
|
|
3039
3608
|
}
|
|
3040
3609
|
|
|
3041
3610
|
// src/tool-session-store.ts
|
|
3042
|
-
import { mkdir as mkdir8, readFile as
|
|
3611
|
+
import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
|
|
3043
3612
|
import os5 from "node:os";
|
|
3044
3613
|
import path9 from "node:path";
|
|
3045
3614
|
var LocalToolSessionStore = class {
|
|
@@ -3060,7 +3629,7 @@ var LocalToolSessionStore = class {
|
|
|
3060
3629
|
}
|
|
3061
3630
|
async read() {
|
|
3062
3631
|
try {
|
|
3063
|
-
return JSON.parse(await
|
|
3632
|
+
return JSON.parse(await readFile7(this.filePath, "utf8"));
|
|
3064
3633
|
} catch {
|
|
3065
3634
|
return {};
|
|
3066
3635
|
}
|
|
@@ -3360,7 +3929,7 @@ function stripJsonFence(value) {
|
|
|
3360
3929
|
}
|
|
3361
3930
|
|
|
3362
3931
|
// src/runner-status.ts
|
|
3363
|
-
import { createHash as
|
|
3932
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3364
3933
|
var watchStateReminderMs = 60 * 1e3;
|
|
3365
3934
|
function formatWatchStartupContext(input) {
|
|
3366
3935
|
return [
|
|
@@ -3381,7 +3950,7 @@ function watchStateKey(action) {
|
|
|
3381
3950
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
3382
3951
|
}
|
|
3383
3952
|
function stableRunnerId(input) {
|
|
3384
|
-
const digest =
|
|
3953
|
+
const digest = createHash5("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
|
|
3385
3954
|
return `runner_${digest}`;
|
|
3386
3955
|
}
|
|
3387
3956
|
|
|
@@ -3505,12 +4074,12 @@ function roundNumber(value, digits) {
|
|
|
3505
4074
|
}
|
|
3506
4075
|
|
|
3507
4076
|
// src/importer.ts
|
|
3508
|
-
import { execFile as
|
|
3509
|
-
import { createHash as
|
|
3510
|
-
import { readdir as readdir4, readFile as
|
|
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";
|
|
3511
4080
|
import path10 from "node:path";
|
|
3512
|
-
import { promisify as
|
|
3513
|
-
var
|
|
4081
|
+
import { promisify as promisify4 } from "node:util";
|
|
4082
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
3514
4083
|
var defaultMaxFileKb = 256;
|
|
3515
4084
|
var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
3516
4085
|
var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
|
|
@@ -3573,7 +4142,7 @@ async function scanLegacyDocuments(options) {
|
|
|
3573
4142
|
skipped.push({ repoPath, reason: "tooLarge" });
|
|
3574
4143
|
continue;
|
|
3575
4144
|
}
|
|
3576
|
-
const content = await
|
|
4145
|
+
const content = await readFile8(fullPath, "utf8").catch(() => void 0);
|
|
3577
4146
|
if (content === void 0) {
|
|
3578
4147
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
3579
4148
|
continue;
|
|
@@ -3654,7 +4223,7 @@ function parseOptionalOriginCloneUrl(originUrl) {
|
|
|
3654
4223
|
async function listRepositoryPaths(rootDir) {
|
|
3655
4224
|
const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
|
|
3656
4225
|
if (gitFiles !== void 0) {
|
|
3657
|
-
return gitFiles.split("\n").map((line) =>
|
|
4226
|
+
return gitFiles.split("\n").map((line) => normalizeRepoPath4(line)).filter((line) => line.length > 0);
|
|
3658
4227
|
}
|
|
3659
4228
|
const files = [];
|
|
3660
4229
|
await walkRepository(rootDir, rootDir, files);
|
|
@@ -3664,7 +4233,7 @@ async function walkRepository(rootDir, directory, files) {
|
|
|
3664
4233
|
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
3665
4234
|
for (const entry of entries) {
|
|
3666
4235
|
const fullPath = path10.join(directory, entry.name);
|
|
3667
|
-
const repoPath =
|
|
4236
|
+
const repoPath = normalizeRepoPath4(path10.relative(rootDir, fullPath));
|
|
3668
4237
|
if (entry.isDirectory()) {
|
|
3669
4238
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
3670
4239
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -3684,7 +4253,7 @@ function matchesIncludeExclude(repoPath, include, exclude) {
|
|
|
3684
4253
|
return true;
|
|
3685
4254
|
}
|
|
3686
4255
|
function wildcardMatch(pattern, repoPath) {
|
|
3687
|
-
const normalizedPattern =
|
|
4256
|
+
const normalizedPattern = normalizeRepoPath4(pattern);
|
|
3688
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, ".*");
|
|
3689
4258
|
return new RegExp(`^${escaped}$`).test(repoPath);
|
|
3690
4259
|
}
|
|
@@ -3737,7 +4306,7 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
3737
4306
|
return uniquePath;
|
|
3738
4307
|
}
|
|
3739
4308
|
function isCanonicalControlPlanePath(repoPath) {
|
|
3740
|
-
const [firstSegment, secondSegment] =
|
|
4309
|
+
const [firstSegment, secondSegment] = normalizeRepoPath4(repoPath).split("/");
|
|
3741
4310
|
return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
|
|
3742
4311
|
}
|
|
3743
4312
|
function isLegacyControlPlanePath(repoPath) {
|
|
@@ -3770,13 +4339,13 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
|
|
|
3770
4339
|
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
3771
4340
|
}
|
|
3772
4341
|
function hashText(value, length) {
|
|
3773
|
-
return
|
|
4342
|
+
return createHash6("sha256").update(value).digest("hex").slice(0, length);
|
|
3774
4343
|
}
|
|
3775
|
-
function
|
|
4344
|
+
function normalizeRepoPath4(value) {
|
|
3776
4345
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
3777
4346
|
}
|
|
3778
4347
|
async function runGit2(args) {
|
|
3779
|
-
const { stdout } = await
|
|
4348
|
+
const { stdout } = await execFileAsync4("git", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
3780
4349
|
return stdout.trim();
|
|
3781
4350
|
}
|
|
3782
4351
|
|
|
@@ -3810,9 +4379,24 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
3810
4379
|
if (options.model) {
|
|
3811
4380
|
args.push("--model", options.model);
|
|
3812
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
|
+
}
|
|
3813
4395
|
if (options.maxIterations !== void 0) {
|
|
3814
4396
|
args.push("--max-iterations", String(options.maxIterations));
|
|
3815
4397
|
}
|
|
4398
|
+
args.push("--max-preflight-attempts", String(options.maxPreflightAttempts));
|
|
4399
|
+
args.push("--tool-timeout-seconds", String(options.toolTimeoutSeconds));
|
|
3816
4400
|
if (!options.stream) {
|
|
3817
4401
|
args.push("--no-stream");
|
|
3818
4402
|
}
|
|
@@ -3858,11 +4442,11 @@ function truncateProcessOutput(value) {
|
|
|
3858
4442
|
}
|
|
3859
4443
|
|
|
3860
4444
|
// src/git-worktree.ts
|
|
3861
|
-
import { execFile as
|
|
4445
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
3862
4446
|
import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
|
|
3863
4447
|
import path12 from "node:path";
|
|
3864
|
-
import { promisify as
|
|
3865
|
-
var
|
|
4448
|
+
import { promisify as promisify5 } from "node:util";
|
|
4449
|
+
var execFileAsync5 = promisify5(execFile5);
|
|
3866
4450
|
function needsGitWorktreeIsolation(workItem) {
|
|
3867
4451
|
return (workItem.workKind ?? "implementation") === "implementation";
|
|
3868
4452
|
}
|
|
@@ -3923,11 +4507,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
|
|
|
3923
4507
|
}
|
|
3924
4508
|
}
|
|
3925
4509
|
async function gitOutput(cwd, args) {
|
|
3926
|
-
const { stdout } = await
|
|
4510
|
+
const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
3927
4511
|
return stdout.trim();
|
|
3928
4512
|
}
|
|
3929
4513
|
async function gitCommandSucceeds(cwd, args) {
|
|
3930
|
-
return
|
|
4514
|
+
return execFileAsync5("git", args, { cwd }).then(() => true, () => false);
|
|
3931
4515
|
}
|
|
3932
4516
|
async function pathExists(value) {
|
|
3933
4517
|
return stat5(value).then(() => true, () => false);
|
|
@@ -3960,6 +4544,10 @@ var CLI_VERSION = readCliPackageVersion();
|
|
|
3960
4544
|
var program = new Command();
|
|
3961
4545
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
3962
4546
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
4547
|
+
var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
|
|
4548
|
+
var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
|
|
4549
|
+
var RUNNER_WORK_LEASE_SECONDS = 300;
|
|
4550
|
+
var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
|
|
3963
4551
|
program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
|
|
3964
4552
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
3965
4553
|
const created = await initControlPlane(options.root);
|
|
@@ -3986,7 +4574,8 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
|
|
|
3986
4574
|
repositoryLinkId: options.repositoryLink,
|
|
3987
4575
|
repoName: parsedRepoUrl.repoName,
|
|
3988
4576
|
repoFingerprint: createRepoFingerprint(options.account, options.project, options.repositoryLink),
|
|
3989
|
-
defaultBranch: options.defaultBranch
|
|
4577
|
+
defaultBranch: options.defaultBranch,
|
|
4578
|
+
machineId: runnerMachineId()
|
|
3990
4579
|
});
|
|
3991
4580
|
const filePath = await writeProjectLink(checkout.targetDir, {
|
|
3992
4581
|
amistioAccountId: options.account,
|
|
@@ -4038,6 +4627,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
4038
4627
|
repoName: repository.repoName,
|
|
4039
4628
|
repoFingerprint: repository.repoFingerprint,
|
|
4040
4629
|
defaultBranch: repository.defaultBranch,
|
|
4630
|
+
machineId: runnerMachineId(),
|
|
4041
4631
|
...parsedCloneUrl ? { cloneUrl: parsedCloneUrl.cloneUrl } : {},
|
|
4042
4632
|
...parsedCloneUrl?.provider ? { provider: parsedCloneUrl.provider } : {},
|
|
4043
4633
|
...parsedCloneUrl?.repoOwner ? { repoOwner: parsedCloneUrl.repoOwner } : {},
|
|
@@ -4084,7 +4674,8 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
4084
4674
|
repositoryLinkId,
|
|
4085
4675
|
repoName: inferRepoName(pairingRoot),
|
|
4086
4676
|
repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
|
|
4087
|
-
defaultBranch: options.defaultBranch
|
|
4677
|
+
defaultBranch: options.defaultBranch,
|
|
4678
|
+
machineId: runnerMachineId()
|
|
4088
4679
|
});
|
|
4089
4680
|
repositoryLinkId = pairing.repositoryLink.repositoryLinkId;
|
|
4090
4681
|
credential = credential ?? pairing.token;
|
|
@@ -4175,6 +4766,45 @@ sync.command("push").description("Push local brain changes to Amistio for review
|
|
|
4175
4766
|
await materializeBrainDocuments(options.root, documents, { allowDirtyOverwrite: true });
|
|
4176
4767
|
console.log(`Pushed ${documents.length} local document${documents.length === 1 ? "" : "s"} for web review.`);
|
|
4177
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
|
+
});
|
|
4178
4808
|
var work = program.command("work").description("Inspect approved work items");
|
|
4179
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) => {
|
|
4180
4810
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
@@ -4231,7 +4861,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
|
|
|
4231
4861
|
}
|
|
4232
4862
|
console.log("custom - pass --tool-command to use any other local runner command.");
|
|
4233
4863
|
});
|
|
4234
|
-
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) => {
|
|
4235
4865
|
const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
|
|
4236
4866
|
const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
|
|
4237
4867
|
if (options.promptOut) {
|
|
@@ -4243,7 +4873,8 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
4243
4873
|
return;
|
|
4244
4874
|
}
|
|
4245
4875
|
const sessionPolicy = normalizeSessionPolicy(options.session);
|
|
4246
|
-
const
|
|
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 });
|
|
4247
4878
|
console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
4248
4879
|
const result = await runLocalTool({
|
|
4249
4880
|
rootDir: options.root,
|
|
@@ -4251,7 +4882,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
4251
4882
|
tool: options.tool,
|
|
4252
4883
|
invocationChannel: options.invocationChannel,
|
|
4253
4884
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
4254
|
-
...
|
|
4885
|
+
...localModelConfig,
|
|
4255
4886
|
streamOutput: options.stream,
|
|
4256
4887
|
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
|
|
4257
4888
|
});
|
|
@@ -4265,7 +4896,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
4265
4896
|
process.exitCode = result.exitCode;
|
|
4266
4897
|
}
|
|
4267
4898
|
});
|
|
4268
|
-
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("--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) => {
|
|
4269
4900
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4270
4901
|
if (!context) {
|
|
4271
4902
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -4437,7 +5068,7 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
4437
5068
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
4438
5069
|
});
|
|
4439
5070
|
var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
|
|
4440
|
-
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("--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) => {
|
|
4441
5072
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4442
5073
|
if (!context) {
|
|
4443
5074
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -4527,6 +5158,16 @@ runnerService.command("remove").description("Remove the startup service for this
|
|
|
4527
5158
|
});
|
|
4528
5159
|
async function runWatchIteration({ command, context, options, runnerId }) {
|
|
4529
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
|
+
}
|
|
4530
5171
|
return await runNextWorkItem({
|
|
4531
5172
|
apiClient: context.client,
|
|
4532
5173
|
projectId: context.metadata.amistioProjectId,
|
|
@@ -4537,6 +5178,10 @@ async function runWatchIteration({ command, context, options, runnerId }) {
|
|
|
4537
5178
|
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
4538
5179
|
...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
|
|
4539
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 } : {},
|
|
4540
5185
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
4541
5186
|
dryRun: Boolean(options.dryRun),
|
|
4542
5187
|
stream: options.stream,
|
|
@@ -4550,6 +5195,8 @@ async function runWatchIteration({ command, context, options, runnerId }) {
|
|
|
4550
5195
|
runnerId
|
|
4551
5196
|
},
|
|
4552
5197
|
suppressIdleOutput: Boolean(options.watch),
|
|
5198
|
+
maxPreflightAttempts: options.maxPreflightAttempts,
|
|
5199
|
+
toolTimeoutMs: options.toolTimeoutSeconds * 1e3,
|
|
4553
5200
|
verbose: Boolean(options.verbose)
|
|
4554
5201
|
});
|
|
4555
5202
|
} catch (error) {
|
|
@@ -4564,7 +5211,7 @@ ${detail}`);
|
|
|
4564
5211
|
} else {
|
|
4565
5212
|
console.error(`${message} Run with --verbose for details.`);
|
|
4566
5213
|
}
|
|
4567
|
-
await Promise.allSettled([
|
|
5214
|
+
const settlements = await Promise.allSettled([
|
|
4568
5215
|
context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
|
|
4569
5216
|
context.client.recordRunnerLog(context.metadata.amistioProjectId, {
|
|
4570
5217
|
runnerId,
|
|
@@ -4575,9 +5222,68 @@ ${detail}`);
|
|
|
4575
5222
|
machineId: runnerMachineId()
|
|
4576
5223
|
})
|
|
4577
5224
|
]);
|
|
5225
|
+
logRejectedSettlements("record watch error", settlements);
|
|
4578
5226
|
return { status: "failed", exitCode: 1, message };
|
|
4579
5227
|
}
|
|
4580
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
|
+
}
|
|
4581
5287
|
async function runNextWorkItem({
|
|
4582
5288
|
apiClient,
|
|
4583
5289
|
dryRun,
|
|
@@ -4588,11 +5294,17 @@ async function runNextWorkItem({
|
|
|
4588
5294
|
sessionPolicy,
|
|
4589
5295
|
stream,
|
|
4590
5296
|
explicitModel,
|
|
5297
|
+
explicitModelId,
|
|
5298
|
+
explicitModelVariant,
|
|
5299
|
+
explicitProviderId,
|
|
5300
|
+
explicitReasoningEffort,
|
|
4591
5301
|
explicitInvocationChannel,
|
|
4592
5302
|
explicitTool,
|
|
5303
|
+
maxPreflightAttempts,
|
|
4593
5304
|
toolCommand,
|
|
4594
5305
|
commandContext,
|
|
4595
5306
|
suppressIdleOutput,
|
|
5307
|
+
toolTimeoutMs,
|
|
4596
5308
|
verbose
|
|
4597
5309
|
}) {
|
|
4598
5310
|
const toolConfig = await resolveRunnerToolConfig({
|
|
@@ -4600,6 +5312,10 @@ async function runNextWorkItem({
|
|
|
4600
5312
|
projectId,
|
|
4601
5313
|
...explicitInvocationChannel ? { explicitInvocationChannel } : {},
|
|
4602
5314
|
...explicitModel ? { explicitModel } : {},
|
|
5315
|
+
...explicitProviderId ? { explicitProviderId } : {},
|
|
5316
|
+
...explicitModelId ? { explicitModelId } : {},
|
|
5317
|
+
...explicitModelVariant ? { explicitModelVariant } : {},
|
|
5318
|
+
...explicitReasoningEffort ? { explicitReasoningEffort } : {},
|
|
4603
5319
|
...explicitTool ? { explicitTool } : {},
|
|
4604
5320
|
...toolCommand ? { toolCommand } : {}
|
|
4605
5321
|
});
|
|
@@ -4615,7 +5331,7 @@ async function runNextWorkItem({
|
|
|
4615
5331
|
console.log(toolConfig.message);
|
|
4616
5332
|
return { status: "blocked", exitCode: 1 };
|
|
4617
5333
|
}
|
|
4618
|
-
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId,
|
|
5334
|
+
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, RUNNER_WORK_LEASE_SECONDS, runnerIsolationCapabilityMetadata());
|
|
4619
5335
|
if (!result.workItem) {
|
|
4620
5336
|
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
4621
5337
|
const message = formatProjectNextAction(nextAction);
|
|
@@ -4625,14 +5341,20 @@ async function runNextWorkItem({
|
|
|
4625
5341
|
return { status: "idle", exitCode: 0, nextAction, message };
|
|
4626
5342
|
}
|
|
4627
5343
|
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
5344
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
5345
|
+
status: "running",
|
|
5346
|
+
summary: "Prepared local runner execution prompt.",
|
|
5347
|
+
idempotencyKey: `runner_milestone_prompt_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
5348
|
+
metadata: { workKind: result.workItem.workKind ?? "implementation", attempt: result.workItem.attempt }
|
|
5349
|
+
});
|
|
4628
5350
|
if (dryRun || toolConfig.tool === "none") {
|
|
4629
5351
|
console.log(prompt);
|
|
4630
5352
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4631
5353
|
return { status: "preview", exitCode: 0 };
|
|
4632
5354
|
}
|
|
4633
|
-
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
4634
|
-
if (worktreeIsolation.status
|
|
4635
|
-
return { status: "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
5355
|
+
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
5356
|
+
if (worktreeIsolation.status !== "ready") {
|
|
5357
|
+
return { status: worktreeIsolation.status === "failed" ? "failed" : "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
4636
5358
|
}
|
|
4637
5359
|
const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
|
|
4638
5360
|
const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
|
|
@@ -4643,7 +5365,8 @@ async function runNextWorkItem({
|
|
|
4643
5365
|
...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
4644
5366
|
...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
|
|
4645
5367
|
});
|
|
4646
|
-
const
|
|
5368
|
+
const resolvedModelConfig = toolConfigModelOptions(toolConfig);
|
|
5369
|
+
const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...resolvedModelConfig });
|
|
4647
5370
|
const sessionContext = await prepareToolSession({
|
|
4648
5371
|
apiClient,
|
|
4649
5372
|
projectId,
|
|
@@ -4669,6 +5392,7 @@ async function runNextWorkItem({
|
|
|
4669
5392
|
const providerSessionStore = new LocalToolSessionStore();
|
|
4670
5393
|
const providerSessionId = sessionContext.toolSession ? await providerSessionStore.getProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName) : void 0;
|
|
4671
5394
|
let toolResult;
|
|
5395
|
+
const stopLeaseRenewal = startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem: result.workItem, telemetry: isolationTelemetry });
|
|
4672
5396
|
try {
|
|
4673
5397
|
toolResult = await runLocalTool({
|
|
4674
5398
|
rootDir: executionRoot,
|
|
@@ -4676,8 +5400,9 @@ async function runNextWorkItem({
|
|
|
4676
5400
|
tool: toolConfig.tool,
|
|
4677
5401
|
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
4678
5402
|
...toolCommand ? { toolCommand } : {},
|
|
4679
|
-
...
|
|
5403
|
+
...resolvedModelConfig,
|
|
4680
5404
|
streamOutput: stream,
|
|
5405
|
+
timeoutMs: toolTimeoutMs,
|
|
4681
5406
|
...sessionContext.toolSession ? {
|
|
4682
5407
|
session: {
|
|
4683
5408
|
toolSessionId: sessionContext.toolSession.toolSessionId,
|
|
@@ -4688,10 +5413,11 @@ async function runNextWorkItem({
|
|
|
4688
5413
|
} : {}
|
|
4689
5414
|
});
|
|
4690
5415
|
} catch (error) {
|
|
5416
|
+
stopLeaseRenewal();
|
|
4691
5417
|
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4692
5418
|
const durationMs2 = Date.now() - startedAt;
|
|
4693
5419
|
const message = `${preview.toolName} failed before returning a result.`;
|
|
4694
|
-
await Promise.allSettled([
|
|
5420
|
+
const settlements = await Promise.allSettled([
|
|
4695
5421
|
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
4696
5422
|
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
4697
5423
|
apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
|
|
@@ -4713,11 +5439,13 @@ async function runNextWorkItem({
|
|
|
4713
5439
|
metadata: { tool: preview.toolName, error: detail }
|
|
4714
5440
|
})
|
|
4715
5441
|
]);
|
|
5442
|
+
logRejectedSettlements("record local tool failure", settlements);
|
|
4716
5443
|
if (verbose || !stream) {
|
|
4717
5444
|
console.error(detail);
|
|
4718
5445
|
}
|
|
4719
5446
|
return { status: "failed", exitCode: 1, message };
|
|
4720
5447
|
}
|
|
5448
|
+
stopLeaseRenewal();
|
|
4721
5449
|
if (sessionContext.toolSession && toolResult.providerSessionId) {
|
|
4722
5450
|
await providerSessionStore.setProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName, toolResult.providerSessionId);
|
|
4723
5451
|
}
|
|
@@ -4728,47 +5456,59 @@ async function runNextWorkItem({
|
|
|
4728
5456
|
console.error(toolResult.stderr.trim());
|
|
4729
5457
|
}
|
|
4730
5458
|
if (result.workItem.workKind === "brainGeneration" || result.workItem.workKind === "planRevision") {
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
5459
|
+
try {
|
|
5460
|
+
return await finalizeBrainGenerationWork({
|
|
5461
|
+
apiClient,
|
|
5462
|
+
durationMs: Date.now() - startedAt,
|
|
5463
|
+
projectId,
|
|
5464
|
+
repositoryLinkId,
|
|
5465
|
+
runnerId,
|
|
5466
|
+
sessionContext,
|
|
5467
|
+
toolConfig,
|
|
5468
|
+
toolName: preview.toolName,
|
|
5469
|
+
toolResult,
|
|
5470
|
+
workItem: result.workItem
|
|
5471
|
+
});
|
|
5472
|
+
} catch (error) {
|
|
5473
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
5474
|
+
}
|
|
4743
5475
|
}
|
|
4744
5476
|
if (result.workItem.workKind === "assistantQuestion") {
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5477
|
+
try {
|
|
5478
|
+
return await finalizeAssistantQuestionWork({
|
|
5479
|
+
apiClient,
|
|
5480
|
+
durationMs: Date.now() - startedAt,
|
|
5481
|
+
projectId,
|
|
5482
|
+
repositoryLinkId,
|
|
5483
|
+
runnerId,
|
|
5484
|
+
sessionContext,
|
|
5485
|
+
toolConfig,
|
|
5486
|
+
toolName: preview.toolName,
|
|
5487
|
+
toolResult,
|
|
5488
|
+
workItem: result.workItem
|
|
5489
|
+
});
|
|
5490
|
+
} catch (error) {
|
|
5491
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
5492
|
+
}
|
|
4757
5493
|
}
|
|
4758
5494
|
if (result.workItem.workKind === "impactPreview") {
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
5495
|
+
try {
|
|
5496
|
+
return await finalizeImpactPreviewWork({
|
|
5497
|
+
apiClient,
|
|
5498
|
+
durationMs: Date.now() - startedAt,
|
|
5499
|
+
projectId,
|
|
5500
|
+
repositoryLinkId,
|
|
5501
|
+
root,
|
|
5502
|
+
runnerId,
|
|
5503
|
+
sessionContext,
|
|
5504
|
+
toolConfig,
|
|
5505
|
+
toolName: preview.toolName,
|
|
5506
|
+
toolResult,
|
|
5507
|
+
workItem: result.workItem
|
|
5508
|
+
});
|
|
5509
|
+
} catch (error) {
|
|
5510
|
+
return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
|
|
5511
|
+
}
|
|
4772
5512
|
}
|
|
4773
5513
|
const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
|
|
4774
5514
|
const durationMs = Date.now() - startedAt;
|
|
@@ -4820,11 +5560,17 @@ async function runNextWorkItem({
|
|
|
4820
5560
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
4821
5561
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
4822
5562
|
}
|
|
4823
|
-
async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
5563
|
+
async function prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
4824
5564
|
if (!needsGitWorktreeIsolation(workItem)) {
|
|
4825
5565
|
return { status: "ready" };
|
|
4826
5566
|
}
|
|
4827
5567
|
const identity = resolveWorktreeIdentity(workItem);
|
|
5568
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5569
|
+
status: "running",
|
|
5570
|
+
summary: `Checking Git worktree isolation for attempt ${workItem.attempt}/${maxPreflightAttempts}.`,
|
|
5571
|
+
idempotencyKey: `runner_milestone_worktree_preflight_${workItem.workItemId}_${workItem.attempt}`,
|
|
5572
|
+
metadata: { executionWorktreeKey: identity.worktreeKey, executionBranch: identity.branch, implementationScopeId: identity.implementationScopeId, attempt: workItem.attempt, maxAttempts: maxPreflightAttempts }
|
|
5573
|
+
});
|
|
4828
5574
|
try {
|
|
4829
5575
|
const isolation = await prepareGitWorktreeIsolation(root, workItem);
|
|
4830
5576
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
@@ -4837,29 +5583,111 @@ async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryL
|
|
|
4837
5583
|
} catch (error) {
|
|
4838
5584
|
const message = errorMessage3(error);
|
|
4839
5585
|
const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
|
|
4840
|
-
const
|
|
5586
|
+
const finalAttempt = workItem.attempt >= maxPreflightAttempts;
|
|
5587
|
+
const statusMessage = finalAttempt ? `Git worktree preflight failed after ${workItem.attempt}/${maxPreflightAttempts} attempts. ${message}` : `Git worktree preflight attempt ${workItem.attempt}/${maxPreflightAttempts} failed. Requeueing for retry. ${message}`;
|
|
5588
|
+
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
|
|
4841
5589
|
...telemetry,
|
|
4842
|
-
message,
|
|
4843
|
-
blockerReason: message,
|
|
5590
|
+
message: statusMessage,
|
|
5591
|
+
...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
|
|
4844
5592
|
error: message
|
|
4845
5593
|
});
|
|
4846
5594
|
await recordRunnerMilestone(apiClient, projectId, statusResult.workItem, runnerId, repositoryLinkId, {
|
|
4847
|
-
status: "
|
|
4848
|
-
summary:
|
|
4849
|
-
idempotencyKey: `
|
|
4850
|
-
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "" }
|
|
5595
|
+
status: finalAttempt ? "failed" : "warning",
|
|
5596
|
+
summary: statusMessage,
|
|
5597
|
+
idempotencyKey: `runner_milestone_worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
5598
|
+
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "", attempt: workItem.attempt, maxAttempts: maxPreflightAttempts, error: message }
|
|
4851
5599
|
});
|
|
4852
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "
|
|
5600
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", {
|
|
4853
5601
|
...runnerHeartbeatMetadata(toolConfig),
|
|
4854
|
-
|
|
4855
|
-
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
4856
|
-
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
4857
|
-
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
5602
|
+
preferenceMessage: statusMessage
|
|
4858
5603
|
});
|
|
4859
|
-
console.error(
|
|
4860
|
-
return { status: "
|
|
5604
|
+
console.error(statusMessage);
|
|
5605
|
+
return { status: finalAttempt ? "failed" : "retrying", message: statusMessage };
|
|
4861
5606
|
}
|
|
4862
5607
|
}
|
|
5608
|
+
function startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem, telemetry }) {
|
|
5609
|
+
let stopped = false;
|
|
5610
|
+
const renew = async () => {
|
|
5611
|
+
if (stopped) return;
|
|
5612
|
+
const leaseExpiresAt = new Date(Date.now() + RUNNER_WORK_LEASE_SECONDS * 1e3).toISOString();
|
|
5613
|
+
try {
|
|
5614
|
+
await apiClient.updateWorkStatus(projectId, workItem.workItemId, "running", `lease_renewal_${workItem.workItemId}_${workItem.attempt}_${Date.now()}`, runnerId, {
|
|
5615
|
+
...telemetry,
|
|
5616
|
+
leaseExpiresAt
|
|
5617
|
+
});
|
|
5618
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
|
|
5619
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
5620
|
+
currentWorkItemId: workItem.workItemId,
|
|
5621
|
+
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
5622
|
+
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
5623
|
+
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
5624
|
+
});
|
|
5625
|
+
} catch (error) {
|
|
5626
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
5627
|
+
console.error(`Could not renew Amistio work lease for ${workItem.workItemId}: ${detail}`);
|
|
5628
|
+
await apiClient.recordRunnerLog(projectId, {
|
|
5629
|
+
runnerId,
|
|
5630
|
+
repositoryLinkId,
|
|
5631
|
+
status: "failed",
|
|
5632
|
+
workItemId: workItem.workItemId,
|
|
5633
|
+
workTitle: workItem.title,
|
|
5634
|
+
...workItem.workKind ? { workKind: workItem.workKind } : {},
|
|
5635
|
+
message: "Runner could not renew the active work lease.",
|
|
5636
|
+
error: detail,
|
|
5637
|
+
machineId: runnerMachineId()
|
|
5638
|
+
}).catch(() => void 0);
|
|
5639
|
+
}
|
|
5640
|
+
};
|
|
5641
|
+
const timer = setInterval(() => {
|
|
5642
|
+
void renew();
|
|
5643
|
+
}, RUNNER_WORK_LEASE_RENEWAL_MS);
|
|
5644
|
+
timer.unref?.();
|
|
5645
|
+
return () => {
|
|
5646
|
+
stopped = true;
|
|
5647
|
+
clearInterval(timer);
|
|
5648
|
+
};
|
|
5649
|
+
}
|
|
5650
|
+
async function recordFinalizationFailure({ apiClient, durationMs, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName, workItem }) {
|
|
5651
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
5652
|
+
const message = `${toolName} completed, but Amistio could not finalize the result.`;
|
|
5653
|
+
const settlements = await Promise.allSettled([
|
|
5654
|
+
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
5655
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
5656
|
+
apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID()}`, runnerId, {
|
|
5657
|
+
...isolationTelemetry,
|
|
5658
|
+
tool: toolName,
|
|
5659
|
+
durationMs,
|
|
5660
|
+
message,
|
|
5661
|
+
error: detail,
|
|
5662
|
+
...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
|
|
5663
|
+
sessionPolicy: sessionContext.policy,
|
|
5664
|
+
sessionDecision: sessionContext.decision,
|
|
5665
|
+
sessionDecisionReason: sessionContext.reason
|
|
5666
|
+
}),
|
|
5667
|
+
apiClient.recordRunnerLog(projectId, {
|
|
5668
|
+
runnerId,
|
|
5669
|
+
repositoryLinkId,
|
|
5670
|
+
status: "failed",
|
|
5671
|
+
workItemId: workItem.workItemId,
|
|
5672
|
+
workTitle: workItem.title,
|
|
5673
|
+
...workItem.workKind ? { workKind: workItem.workKind } : {},
|
|
5674
|
+
tool: toolName,
|
|
5675
|
+
durationMs,
|
|
5676
|
+
message,
|
|
5677
|
+
error: detail,
|
|
5678
|
+
machineId: runnerMachineId()
|
|
5679
|
+
}),
|
|
5680
|
+
recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5681
|
+
status: "failed",
|
|
5682
|
+
summary: message,
|
|
5683
|
+
idempotencyKey: `runner_milestone_finalization_failed_${workItem.workItemId}_${workItem.attempt}`,
|
|
5684
|
+
metadata: { tool: toolName, durationMs, error: detail }
|
|
5685
|
+
})
|
|
5686
|
+
]);
|
|
5687
|
+
logRejectedSettlements("record finalization failure", settlements);
|
|
5688
|
+
console.error(detail);
|
|
5689
|
+
return { status: "failed", exitCode: 1, message };
|
|
5690
|
+
}
|
|
4863
5691
|
function workItemIsolationTelemetry(workItem, isolation) {
|
|
4864
5692
|
const implementationScopeId = isolation?.implementationScopeId ?? workItem.implementationScopeId;
|
|
4865
5693
|
const executionBranch = isolation?.branch ?? workItem.executionBranch;
|
|
@@ -4888,6 +5716,13 @@ async function recordRunnerMilestone(apiClient, projectId, workItem, runnerId, r
|
|
|
4888
5716
|
...input
|
|
4889
5717
|
}).catch(() => void 0);
|
|
4890
5718
|
}
|
|
5719
|
+
function logRejectedSettlements(action, settlements) {
|
|
5720
|
+
for (const settlement of settlements) {
|
|
5721
|
+
if (settlement.status === "rejected") {
|
|
5722
|
+
console.error(`${action} failed: ${errorMessage3(settlement.reason)}`);
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
4891
5726
|
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
4892
5727
|
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
4893
5728
|
const command = commands.filter((item) => item.status === "pending" || item.status === "acknowledged" || item.status === "running").sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
|
|
@@ -5453,11 +6288,17 @@ function parseInvocationChannel(value) {
|
|
|
5453
6288
|
}
|
|
5454
6289
|
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
5455
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
|
+
}
|
|
5456
6297
|
function inferRepoName(root) {
|
|
5457
6298
|
return path13.basename(path13.resolve(root)) || "repository";
|
|
5458
6299
|
}
|
|
5459
6300
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
5460
|
-
return
|
|
6301
|
+
return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
5461
6302
|
}
|
|
5462
6303
|
function defaultApiUrl() {
|
|
5463
6304
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -5472,9 +6313,10 @@ function formatApiUrlFlag(apiUrl) {
|
|
|
5472
6313
|
function formatShellArg(value) {
|
|
5473
6314
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
5474
6315
|
}
|
|
5475
|
-
async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
|
|
6316
|
+
async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitModelId, explicitModelVariant, explicitProviderId, explicitReasoningEffort, explicitTool, projectId, toolCommand }) {
|
|
5476
6317
|
const capabilities = toRunnerToolCapabilities(await detectLocalTools());
|
|
5477
6318
|
if (toolCommand) {
|
|
6319
|
+
const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
|
|
5478
6320
|
return {
|
|
5479
6321
|
ready: true,
|
|
5480
6322
|
tool: explicitTool ?? "auto",
|
|
@@ -5485,33 +6327,42 @@ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, e
|
|
|
5485
6327
|
requestedInvocationChannel: explicitInvocationChannel ?? "command",
|
|
5486
6328
|
effectiveInvocationChannel: "command",
|
|
5487
6329
|
...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
|
|
5488
|
-
...
|
|
6330
|
+
...modelConfig2,
|
|
5489
6331
|
message: "Using local custom tool command."
|
|
5490
6332
|
};
|
|
5491
6333
|
}
|
|
5492
6334
|
if (explicitTool === "none") {
|
|
5493
|
-
|
|
5494
|
-
|
|
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 });
|
|
5495
6338
|
}
|
|
5496
6339
|
return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
|
|
5497
6340
|
}
|
|
5498
6341
|
if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
|
|
5499
|
-
|
|
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 });
|
|
5500
6344
|
}
|
|
5501
6345
|
const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
|
|
5502
6346
|
const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
|
|
5503
6347
|
const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
|
|
5504
|
-
const
|
|
5505
|
-
|
|
5506
|
-
|
|
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 });
|
|
5507
6357
|
}
|
|
5508
|
-
function resolveRequestedTool({ capabilities,
|
|
6358
|
+
function resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source }) {
|
|
6359
|
+
const needsModelSelection = hasModelConfig(modelConfig);
|
|
5509
6360
|
if (requestedTool === "auto") {
|
|
5510
|
-
const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!
|
|
6361
|
+
const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!needsModelSelection || capability2.supportsModelSelection));
|
|
5511
6362
|
if (!candidate) {
|
|
5512
6363
|
const anyAvailable = capabilities.some((capability2) => capability2.available);
|
|
5513
6364
|
const anyChannelAvailable = capabilities.some((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel));
|
|
5514
|
-
const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" :
|
|
6365
|
+
const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : needsModelSelection ? "modelUnsupported" : "unavailable";
|
|
5515
6366
|
return unavailableToolConfig({
|
|
5516
6367
|
capabilities,
|
|
5517
6368
|
source,
|
|
@@ -5519,23 +6370,31 @@ function resolveRequestedTool({ capabilities, model, requestedInvocationChannel,
|
|
|
5519
6370
|
requestedTool,
|
|
5520
6371
|
requestedInvocationChannel,
|
|
5521
6372
|
tool: "auto",
|
|
5522
|
-
...
|
|
5523
|
-
message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` :
|
|
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."
|
|
5524
6375
|
});
|
|
5525
6376
|
}
|
|
5526
|
-
|
|
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 };
|
|
5527
6382
|
}
|
|
5528
6383
|
const capability = capabilities.find((candidate) => candidate.name === requestedTool);
|
|
5529
6384
|
if (!capability?.available) {
|
|
5530
|
-
return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...
|
|
6385
|
+
return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...modelConfig, message: `${requestedTool} is selected but is not available on this runner.` });
|
|
5531
6386
|
}
|
|
5532
6387
|
if (!capabilitySupportsInvocationChannel(capability, requestedInvocationChannel)) {
|
|
5533
|
-
return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...
|
|
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.` });
|
|
5534
6389
|
}
|
|
5535
|
-
if (
|
|
5536
|
-
return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool,
|
|
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.` });
|
|
5537
6392
|
}
|
|
5538
|
-
|
|
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 });
|
|
6396
|
+
}
|
|
6397
|
+
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...modelResolution.config };
|
|
5539
6398
|
}
|
|
5540
6399
|
function unavailableToolConfig(input) {
|
|
5541
6400
|
return {
|
|
@@ -5549,9 +6408,116 @@ function unavailableToolConfig(input) {
|
|
|
5549
6408
|
...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
|
|
5550
6409
|
...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
|
|
5551
6410
|
...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
|
|
5552
|
-
...input
|
|
6411
|
+
...normalizeModelConfig(input)
|
|
5553
6412
|
};
|
|
5554
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
|
+
}
|
|
5555
6521
|
function capabilitySupportsInvocationChannel(capability, channel) {
|
|
5556
6522
|
if (channel === "auto") return capability.available;
|
|
5557
6523
|
if (channel === "sdk") return capability.sdkAvailable;
|
|
@@ -5584,6 +6550,8 @@ function runnerIsolationCapabilityMetadata() {
|
|
|
5584
6550
|
};
|
|
5585
6551
|
}
|
|
5586
6552
|
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
6553
|
+
const modelConfig = toolConfig ? toolConfigModelOptions(toolConfig) : {};
|
|
6554
|
+
const effectiveModelConfig = toolConfig?.ready ? modelConfig : {};
|
|
5587
6555
|
return {
|
|
5588
6556
|
version: CLI_VERSION,
|
|
5589
6557
|
mode,
|
|
@@ -5593,16 +6561,24 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
5593
6561
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
5594
6562
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
5595
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 } : {},
|
|
5596
6568
|
...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
|
|
5597
6569
|
...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
|
|
5598
|
-
...
|
|
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 } : {},
|
|
5599
6575
|
...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
|
|
5600
6576
|
...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
|
|
5601
6577
|
...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
|
|
5602
6578
|
};
|
|
5603
6579
|
}
|
|
5604
6580
|
function runnerMachineId() {
|
|
5605
|
-
return
|
|
6581
|
+
return createHash7("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
|
|
5606
6582
|
}
|
|
5607
6583
|
async function delay(milliseconds) {
|
|
5608
6584
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|