@amistio/cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +989 -117
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
6
5
|
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import os6 from "node:os";
|
|
7
|
+
import path12 from "node:path";
|
|
9
8
|
import { Command } from "commander";
|
|
10
9
|
|
|
11
10
|
// ../shared/src/schemas.ts
|
|
@@ -86,9 +85,12 @@ var runnerToolNameSchema = z.enum(runnerToolNames);
|
|
|
86
85
|
var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
|
|
87
86
|
var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
|
|
88
87
|
var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
|
|
89
|
-
var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "custom", "none"]);
|
|
88
|
+
var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "channelUnsupported", "custom", "none"]);
|
|
89
|
+
var runnerInvocationChannelSchema = z.enum(["auto", "sdk", "command"]);
|
|
90
|
+
var runnerEffectiveInvocationChannelSchema = z.enum(["sdk", "command"]);
|
|
90
91
|
var runnerToolModelPreferenceSchema = z.object({
|
|
91
92
|
tool: runnerToolSelectionSchema.optional(),
|
|
93
|
+
invocationChannel: runnerInvocationChannelSchema.optional(),
|
|
92
94
|
model: z.string().trim().min(1).max(160).optional()
|
|
93
95
|
});
|
|
94
96
|
var runnerToolCapabilitySchema = z.object({
|
|
@@ -104,6 +106,7 @@ var runnerToolCapabilitySchema = z.object({
|
|
|
104
106
|
});
|
|
105
107
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
106
108
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
109
|
+
var projectStatusSchema = z.enum(["active", "archived"]);
|
|
107
110
|
var baseItemSchema = z.object({
|
|
108
111
|
id: z.string().min(1),
|
|
109
112
|
type: itemTypeSchema,
|
|
@@ -130,7 +133,11 @@ var projectItemSchema = baseItemSchema.extend({
|
|
|
130
133
|
projectId: z.string().min(1),
|
|
131
134
|
name: z.string().min(1),
|
|
132
135
|
slug: z.string().min(1),
|
|
133
|
-
description: z.string().optional()
|
|
136
|
+
description: z.string().optional(),
|
|
137
|
+
status: projectStatusSchema.default("active"),
|
|
138
|
+
archivedAt: isoDateTimeSchema.optional(),
|
|
139
|
+
archivedByUserId: z.string().min(1).optional(),
|
|
140
|
+
archiveReason: z.string().min(1).optional()
|
|
134
141
|
});
|
|
135
142
|
var repositoryLinkItemSchema = baseItemSchema.extend({
|
|
136
143
|
type: z.literal("repositoryLink"),
|
|
@@ -229,14 +236,16 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
229
236
|
projectId: z.string().min(1),
|
|
230
237
|
runnerId: z.string().min(1),
|
|
231
238
|
repositoryLinkId: z.string().min(1),
|
|
232
|
-
status: z.enum(["online", "offline", "running", "blocked"]),
|
|
239
|
+
status: z.enum(["online", "offline", "running", "blocked", "removed"]),
|
|
233
240
|
version: z.string().optional(),
|
|
234
241
|
mode: z.enum(["foreground", "background"]).optional(),
|
|
235
242
|
hostname: z.string().min(1).optional(),
|
|
236
243
|
runnerName: z.string().min(1).optional(),
|
|
237
244
|
capabilities: z.array(runnerToolCapabilitySchema).optional(),
|
|
238
245
|
requestedTool: runnerToolSelectionSchema.optional(),
|
|
246
|
+
requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
|
|
239
247
|
effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
|
|
248
|
+
effectiveInvocationChannel: runnerEffectiveInvocationChannelSchema.optional(),
|
|
240
249
|
effectiveModel: z.string().min(1).optional(),
|
|
241
250
|
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
242
251
|
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
@@ -717,7 +726,7 @@ function runnerWaitAction(readiness, fallbackTitle) {
|
|
|
717
726
|
}
|
|
718
727
|
function getSharedRunnerReadiness(repositoryLinks, runnerHeartbeats, nowMs) {
|
|
719
728
|
const repositoryLinkIds = new Set(repositoryLinks.map((link) => link.repositoryLinkId));
|
|
720
|
-
const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId)).sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
|
|
729
|
+
const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId) && heartbeat.status !== "removed").sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
|
|
721
730
|
const repositoryLink = latestHeartbeat ? repositoryLinks.find((link) => link.repositoryLinkId === latestHeartbeat.repositoryLinkId) ?? repositoryLinks[0] : repositoryLinks[0];
|
|
722
731
|
if (!latestHeartbeat) {
|
|
723
732
|
return { ready: false, reason: "runnerMissing", ...repositoryLink ? { repositoryLink } : {} };
|
|
@@ -992,6 +1001,22 @@ var ApiClient = class {
|
|
|
992
1001
|
}
|
|
993
1002
|
);
|
|
994
1003
|
}
|
|
1004
|
+
async importPairingSession(input) {
|
|
1005
|
+
return this.request(
|
|
1006
|
+
"pairing-sessions",
|
|
1007
|
+
z3.object({
|
|
1008
|
+
accountId: z3.string().min(1),
|
|
1009
|
+
projectId: z3.string().min(1),
|
|
1010
|
+
repositoryLink: repositoryLinkItemSchema,
|
|
1011
|
+
repositoryLinkAction: z3.enum(["created", "reused"]),
|
|
1012
|
+
token: z3.string().min(1)
|
|
1013
|
+
}),
|
|
1014
|
+
{
|
|
1015
|
+
method: "PATCH",
|
|
1016
|
+
body: JSON.stringify(input)
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
995
1020
|
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300) {
|
|
996
1021
|
return this.request(
|
|
997
1022
|
`/projects/${projectId}/work-items/claim`,
|
|
@@ -1066,6 +1091,7 @@ var ApiClient = class {
|
|
|
1066
1091
|
project: runnerSettingsItemSchema.optional(),
|
|
1067
1092
|
effective: z3.object({
|
|
1068
1093
|
tool: runnerToolSelectionSchema,
|
|
1094
|
+
invocationChannel: runnerInvocationChannelSchema,
|
|
1069
1095
|
model: z3.string().optional(),
|
|
1070
1096
|
source: runnerPreferenceSourceSchema
|
|
1071
1097
|
})
|
|
@@ -1141,7 +1167,7 @@ var ApiClient = class {
|
|
|
1141
1167
|
...init,
|
|
1142
1168
|
headers: {
|
|
1143
1169
|
"content-type": "application/json",
|
|
1144
|
-
"x-amistio-account-id": this.options.accountId,
|
|
1170
|
+
...this.options.accountId ? { "x-amistio-account-id": this.options.accountId } : {},
|
|
1145
1171
|
...this.options.token ? { authorization: `Bearer ${this.options.token}` } : {}
|
|
1146
1172
|
}
|
|
1147
1173
|
});
|
|
@@ -1178,8 +1204,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
1178
1204
|
});
|
|
1179
1205
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
1180
1206
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
1181
|
-
const
|
|
1182
|
-
return new URL(`${base}${
|
|
1207
|
+
const path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1208
|
+
return new URL(`${base}${path13}`);
|
|
1183
1209
|
}
|
|
1184
1210
|
|
|
1185
1211
|
// src/orchestrator.ts
|
|
@@ -1375,6 +1401,7 @@ async function runLocalTool(options) {
|
|
|
1375
1401
|
prompt: options.prompt,
|
|
1376
1402
|
promptFilePath,
|
|
1377
1403
|
tool: options.tool ?? "auto",
|
|
1404
|
+
invocationChannel: options.invocationChannel ?? "auto",
|
|
1378
1405
|
...options.model ? { model: options.model } : {}
|
|
1379
1406
|
};
|
|
1380
1407
|
if (options.toolCommand) {
|
|
@@ -1407,6 +1434,7 @@ async function createToolRunPreview(options) {
|
|
|
1407
1434
|
prompt: options.prompt,
|
|
1408
1435
|
promptFilePath,
|
|
1409
1436
|
tool: options.tool ?? "auto",
|
|
1437
|
+
invocationChannel: options.invocationChannel ?? "auto",
|
|
1410
1438
|
...options.model ? { model: options.model } : {}
|
|
1411
1439
|
};
|
|
1412
1440
|
if (options.toolCommand) {
|
|
@@ -1443,25 +1471,32 @@ async function createToolRunner(options) {
|
|
|
1443
1471
|
if (tool === "none") {
|
|
1444
1472
|
throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
|
|
1445
1473
|
}
|
|
1446
|
-
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model)) : await selectRequestedAdapter(tool);
|
|
1474
|
+
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model), options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
|
|
1447
1475
|
if (options.model && !adapter.supportsModelSelection) {
|
|
1448
1476
|
throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
|
|
1449
1477
|
}
|
|
1450
|
-
if (adapter.runWithSdk && await isSdkAvailable(adapter)) {
|
|
1478
|
+
if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
|
|
1451
1479
|
return {
|
|
1452
1480
|
toolName: adapter.name,
|
|
1453
1481
|
kind: "sdk",
|
|
1454
1482
|
displayCommand: adapter.sdkDisplayCommand ?? `${adapter.name} SDK`,
|
|
1455
|
-
adapter
|
|
1483
|
+
adapter,
|
|
1484
|
+
allowCommandFallback: options.invocationChannel === "auto"
|
|
1456
1485
|
};
|
|
1457
1486
|
}
|
|
1458
|
-
if (adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
|
|
1487
|
+
if (options.invocationChannel !== "sdk" && adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
|
|
1459
1488
|
return {
|
|
1460
1489
|
toolName: adapter.name,
|
|
1461
1490
|
kind: "command",
|
|
1462
1491
|
invocation: adapter.buildInvocation(options)
|
|
1463
1492
|
};
|
|
1464
1493
|
}
|
|
1494
|
+
if (options.invocationChannel === "sdk") {
|
|
1495
|
+
throw new Error(`The ${adapter.name} SDK was not found. Select Auto or Command invocation, install the SDK/runtime, or pass --tool-command locally.`);
|
|
1496
|
+
}
|
|
1497
|
+
if (options.invocationChannel === "command") {
|
|
1498
|
+
throw new Error(`The ${adapter.name} executable was not found. Select Auto or SDK invocation, install the command, or pass --tool-command locally.`);
|
|
1499
|
+
}
|
|
1465
1500
|
throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
|
|
1466
1501
|
}
|
|
1467
1502
|
async function executeToolRunner(runner2, input) {
|
|
@@ -1471,7 +1506,7 @@ async function executeToolRunner(runner2, input) {
|
|
|
1471
1506
|
try {
|
|
1472
1507
|
return await runner2.adapter.runWithSdk(input);
|
|
1473
1508
|
} catch (error) {
|
|
1474
|
-
if (runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1509
|
+
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1475
1510
|
const fallback = runner2.adapter.buildInvocation(input);
|
|
1476
1511
|
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
|
|
1477
1512
|
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
|
|
@@ -1490,34 +1525,53 @@ function normalizeToolRequest(value) {
|
|
|
1490
1525
|
}
|
|
1491
1526
|
throw new Error(`Unsupported local tool: ${value}. Supported tools: auto, none, ${localToolNames.join(", ")}.`);
|
|
1492
1527
|
}
|
|
1493
|
-
async function selectFirstAvailableAdapter(requiresModelSelection = false) {
|
|
1528
|
+
async function selectFirstAvailableAdapter(requiresModelSelection = false, invocationChannel = "auto") {
|
|
1494
1529
|
for (const adapter of localToolAdapters) {
|
|
1495
1530
|
if (requiresModelSelection && !adapter.supportsModelSelection) {
|
|
1496
1531
|
continue;
|
|
1497
1532
|
}
|
|
1498
1533
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
1499
1534
|
const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
|
|
1500
|
-
if (sdkAvailable
|
|
1535
|
+
if (supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
|
|
1501
1536
|
return adapter;
|
|
1502
1537
|
}
|
|
1503
1538
|
}
|
|
1539
|
+
if (invocationChannel !== "auto") {
|
|
1540
|
+
throw new Error(`No installed local AI tool supports ${invocationChannel} invocation${requiresModelSelection ? " with model selection" : ""}. Select Auto or install a compatible tool.`);
|
|
1541
|
+
}
|
|
1504
1542
|
throw new Error(
|
|
1505
1543
|
requiresModelSelection ? "No installed local AI tool supports model selection. Remove --model or choose a model-aware adapter." : `No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
|
|
1506
1544
|
);
|
|
1507
1545
|
}
|
|
1508
|
-
async function selectRequestedAdapter(tool) {
|
|
1546
|
+
async function selectRequestedAdapter(tool, invocationChannel = "auto") {
|
|
1509
1547
|
const adapter = localToolAdapters.find((candidate) => candidate.name === tool);
|
|
1510
1548
|
if (!adapter) {
|
|
1511
1549
|
throw new Error(`Unsupported local tool: ${tool}.`);
|
|
1512
1550
|
}
|
|
1513
|
-
|
|
1551
|
+
const sdkAvailable = await isSdkAvailable(adapter);
|
|
1552
|
+
const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
|
|
1553
|
+
if (!supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
|
|
1554
|
+
if (invocationChannel === "sdk") {
|
|
1555
|
+
throw new Error(`The ${tool} SDK was not found. Select Auto or Command invocation, install it, or pass --tool-command locally.`);
|
|
1556
|
+
}
|
|
1557
|
+
if (invocationChannel === "command") {
|
|
1558
|
+
throw new Error(`The ${tool} executable was not found. Select Auto or SDK invocation, install it, or pass --tool-command locally.`);
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
|
|
1561
|
+
}
|
|
1562
|
+
if (invocationChannel !== "command" && sdkAvailable) {
|
|
1514
1563
|
return adapter;
|
|
1515
1564
|
}
|
|
1516
|
-
if (
|
|
1565
|
+
if (invocationChannel !== "sdk" && commandAvailable) {
|
|
1517
1566
|
return adapter;
|
|
1518
1567
|
}
|
|
1519
1568
|
throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
|
|
1520
1569
|
}
|
|
1570
|
+
function supportsRequestedInvocationChannel(capability, invocationChannel) {
|
|
1571
|
+
if (invocationChannel === "auto") return capability.sdkAvailable || capability.commandAvailable;
|
|
1572
|
+
if (invocationChannel === "sdk") return capability.sdkAvailable;
|
|
1573
|
+
return capability.commandAvailable;
|
|
1574
|
+
}
|
|
1521
1575
|
async function isSdkAvailable(adapter) {
|
|
1522
1576
|
if (!adapter.sdkPackageName || !adapter.runWithSdk) {
|
|
1523
1577
|
return false;
|
|
@@ -2484,12 +2538,782 @@ function watchStateKey(action) {
|
|
|
2484
2538
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
2485
2539
|
}
|
|
2486
2540
|
|
|
2541
|
+
// src/importer.ts
|
|
2542
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2543
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2544
|
+
import { readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
2545
|
+
import path9 from "node:path";
|
|
2546
|
+
import { promisify as promisify2 } from "node:util";
|
|
2547
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
2548
|
+
var defaultMaxFileKb = 256;
|
|
2549
|
+
var controlPlaneRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2550
|
+
var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
|
|
2551
|
+
var excludedFileNames = /* @__PURE__ */ new Set(["context/amistio-project.md"]);
|
|
2552
|
+
var generatedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
|
|
2553
|
+
var documentFolderByType = {
|
|
2554
|
+
architecture: "architecture",
|
|
2555
|
+
context: "context",
|
|
2556
|
+
decision: "decisions",
|
|
2557
|
+
feature: "features",
|
|
2558
|
+
memory: "memory",
|
|
2559
|
+
plan: "plans",
|
|
2560
|
+
prompt: "prompts/shared",
|
|
2561
|
+
workflow: "workflows"
|
|
2562
|
+
};
|
|
2563
|
+
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
2564
|
+
const requestedRoot = path9.resolve(rootDir);
|
|
2565
|
+
const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
|
|
2566
|
+
const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
|
|
2567
|
+
const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
|
|
2568
|
+
const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
|
|
2569
|
+
const repoName = (parsedCloneUrl?.repoName ?? path9.basename(root)) || "repository";
|
|
2570
|
+
const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
|
|
2571
|
+
return {
|
|
2572
|
+
rootDir: root,
|
|
2573
|
+
repoName,
|
|
2574
|
+
defaultBranch: detectedBranch || defaultBranch,
|
|
2575
|
+
repoFingerprint: `import_${hashText(fingerprintSeed, 24)}`,
|
|
2576
|
+
...parsedCloneUrl ? { parsedCloneUrl } : {},
|
|
2577
|
+
...!parsedCloneUrl && originUrl ? { originRemoteWarning: "Origin remote is not a supported hosted HTTPS or SSH clone URL, so no clone URL will be stored." } : {}
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
async function scanLegacyDocuments(options) {
|
|
2581
|
+
const rootDir = path9.resolve(options.rootDir);
|
|
2582
|
+
const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
|
|
2583
|
+
const skipped = [];
|
|
2584
|
+
const candidates = [];
|
|
2585
|
+
const usedDestinationPaths = /* @__PURE__ */ new Set();
|
|
2586
|
+
const repoPaths = (await listRepositoryPaths(rootDir)).sort((first, second) => first.localeCompare(second));
|
|
2587
|
+
for (const repoPath of repoPaths) {
|
|
2588
|
+
if (!matchesIncludeExclude(repoPath, options.include, options.exclude)) {
|
|
2589
|
+
skipped.push({ repoPath, reason: "excluded" });
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (!isMarkdownDocument(repoPath)) {
|
|
2593
|
+
skipped.push({ repoPath, reason: "notMarkdown" });
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
if (isExcludedRepoPath(repoPath)) {
|
|
2597
|
+
skipped.push({ repoPath, reason: "excluded" });
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const fullPath = path9.join(rootDir, ...repoPath.split("/"));
|
|
2601
|
+
const fileStat = await stat4(fullPath).catch(() => void 0);
|
|
2602
|
+
if (!fileStat?.isFile()) {
|
|
2603
|
+
skipped.push({ repoPath, reason: "unreadable" });
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
if (fileStat.size > maxBytes) {
|
|
2607
|
+
skipped.push({ repoPath, reason: "tooLarge" });
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
const content = await readFile6(fullPath, "utf8").catch(() => void 0);
|
|
2611
|
+
if (content === void 0) {
|
|
2612
|
+
skipped.push({ repoPath, reason: "unreadable" });
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
if (isAmistioManagedMarkdown(content)) {
|
|
2616
|
+
skipped.push({ repoPath, reason: "alreadyManaged" });
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const documentType = classifyLegacyDocument(repoPath, content);
|
|
2620
|
+
const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType), repoPath, usedDestinationPaths);
|
|
2621
|
+
candidates.push({
|
|
2622
|
+
sourcePath: repoPath,
|
|
2623
|
+
repoPath: destinationPath,
|
|
2624
|
+
documentType,
|
|
2625
|
+
title: inferTitle2(content, repoPath),
|
|
2626
|
+
content,
|
|
2627
|
+
contentHash: sha256ContentHash(content)
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
return { candidates, skipped };
|
|
2631
|
+
}
|
|
2632
|
+
function buildImportedBrainDocuments(options) {
|
|
2633
|
+
const importedAt = options.importedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2634
|
+
const existingById = new Map((options.existingDocuments ?? []).map((document) => [document.documentId, document]));
|
|
2635
|
+
return options.candidates.map((candidate) => {
|
|
2636
|
+
const documentId = stableImportDocumentId(options.accountId, options.projectId, options.repositoryLinkId, candidate.sourcePath);
|
|
2637
|
+
const existing = existingById.get(documentId);
|
|
2638
|
+
const revision = existing ? existing.contentHash === candidate.contentHash ? existing.revision : existing.revision + 1 : 0;
|
|
2639
|
+
return {
|
|
2640
|
+
id: documentId,
|
|
2641
|
+
type: "brainDocument",
|
|
2642
|
+
schemaVersion: 1,
|
|
2643
|
+
accountId: options.accountId,
|
|
2644
|
+
projectId: options.projectId,
|
|
2645
|
+
documentId,
|
|
2646
|
+
documentType: candidate.documentType,
|
|
2647
|
+
title: candidate.title,
|
|
2648
|
+
status: "reviewing",
|
|
2649
|
+
repoPath: candidate.repoPath,
|
|
2650
|
+
content: candidate.content,
|
|
2651
|
+
contentHash: candidate.contentHash,
|
|
2652
|
+
frontmatter: {
|
|
2653
|
+
...existing?.frontmatter ?? {},
|
|
2654
|
+
legacySourcePath: candidate.sourcePath,
|
|
2655
|
+
importedByCommand: "amistio import",
|
|
2656
|
+
importedAt,
|
|
2657
|
+
importedSourceHash: candidate.contentHash
|
|
2658
|
+
},
|
|
2659
|
+
revision,
|
|
2660
|
+
source: "repo",
|
|
2661
|
+
syncState: "dirtyInRepo",
|
|
2662
|
+
createdAt: existing?.createdAt ?? importedAt,
|
|
2663
|
+
updatedAt: importedAt,
|
|
2664
|
+
...existing?.approvedRevision !== void 0 ? { approvedRevision: existing.approvedRevision } : {}
|
|
2665
|
+
};
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
function importSkipCounts(skipped) {
|
|
2669
|
+
return {
|
|
2670
|
+
notMarkdown: skipped.filter((item) => item.reason === "notMarkdown").length,
|
|
2671
|
+
excluded: skipped.filter((item) => item.reason === "excluded").length,
|
|
2672
|
+
tooLarge: skipped.filter((item) => item.reason === "tooLarge").length,
|
|
2673
|
+
alreadyManaged: skipped.filter((item) => item.reason === "alreadyManaged").length,
|
|
2674
|
+
unreadable: skipped.filter((item) => item.reason === "unreadable").length
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function parseOptionalOriginCloneUrl(originUrl) {
|
|
2678
|
+
try {
|
|
2679
|
+
return parseRepositoryCloneUrl(originUrl);
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2682
|
+
if (message.toLowerCase().includes("credential") || message.toLowerCase().includes("password")) {
|
|
2683
|
+
throw new Error("Repository origin remote contains embedded credentials. Remove credentials from the remote URL before importing.");
|
|
2684
|
+
}
|
|
2685
|
+
return void 0;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
async function listRepositoryPaths(rootDir) {
|
|
2689
|
+
const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
|
|
2690
|
+
if (gitFiles !== void 0) {
|
|
2691
|
+
return gitFiles.split("\n").map((line) => normalizeRepoPath2(line)).filter((line) => line.length > 0);
|
|
2692
|
+
}
|
|
2693
|
+
const files = [];
|
|
2694
|
+
await walkRepository(rootDir, rootDir, files);
|
|
2695
|
+
return files;
|
|
2696
|
+
}
|
|
2697
|
+
async function walkRepository(rootDir, directory, files) {
|
|
2698
|
+
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
2699
|
+
for (const entry of entries) {
|
|
2700
|
+
const fullPath = path9.join(directory, entry.name);
|
|
2701
|
+
const repoPath = normalizeRepoPath2(path9.relative(rootDir, fullPath));
|
|
2702
|
+
if (entry.isDirectory()) {
|
|
2703
|
+
if (!excludedDirectoryNames.has(entry.name)) {
|
|
2704
|
+
await walkRepository(rootDir, fullPath, files);
|
|
2705
|
+
}
|
|
2706
|
+
} else if (entry.isFile() && !isExcludedRepoPath(repoPath)) {
|
|
2707
|
+
files.push(repoPath);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function matchesIncludeExclude(repoPath, include, exclude) {
|
|
2712
|
+
if (include?.length && !include.some((pattern) => wildcardMatch(pattern, repoPath))) {
|
|
2713
|
+
return false;
|
|
2714
|
+
}
|
|
2715
|
+
if (exclude?.some((pattern) => wildcardMatch(pattern, repoPath))) {
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
return true;
|
|
2719
|
+
}
|
|
2720
|
+
function wildcardMatch(pattern, repoPath) {
|
|
2721
|
+
const normalizedPattern = normalizeRepoPath2(pattern);
|
|
2722
|
+
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "::DOUBLE_STAR_SLASH::").replace(/\*\*/g, "::DOUBLE_STAR::").replace(/\?/g, "[^/]").replace(/\*/g, "[^/]*").replace(/::DOUBLE_STAR_SLASH::/g, "(?:.*/)?").replace(/::DOUBLE_STAR::/g, ".*");
|
|
2723
|
+
return new RegExp(`^${escaped}$`).test(repoPath);
|
|
2724
|
+
}
|
|
2725
|
+
function isMarkdownDocument(repoPath) {
|
|
2726
|
+
return /\.(md|mdx)$/i.test(repoPath);
|
|
2727
|
+
}
|
|
2728
|
+
function isExcludedRepoPath(repoPath) {
|
|
2729
|
+
if (excludedFileNames.has(repoPath)) return true;
|
|
2730
|
+
const segments = repoPath.split("/");
|
|
2731
|
+
if (segments.some((segment) => excludedDirectoryNames.has(segment) || generatedPathSegments.has(segment))) return true;
|
|
2732
|
+
const basename = segments[segments.length - 1]?.toLowerCase() ?? "";
|
|
2733
|
+
return basename.startsWith(".env") || basename.includes("secret") || basename.includes("token") || basename.includes("credential") || basename.endsWith(".lock");
|
|
2734
|
+
}
|
|
2735
|
+
function isAmistioManagedMarkdown(content) {
|
|
2736
|
+
const frontmatter = parseFrontmatter(content);
|
|
2737
|
+
return typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0;
|
|
2738
|
+
}
|
|
2739
|
+
function classifyLegacyDocument(repoPath, content) {
|
|
2740
|
+
const haystack = `${repoPath} ${inferTitle2(content, repoPath)}`.toLowerCase();
|
|
2741
|
+
if (/\b(prompt|prompts|instruction|instructions|copilot|agent|skill)\b/.test(haystack)) return "prompt";
|
|
2742
|
+
if (/\b(adr|decision|decisions|rfc)\b/.test(haystack)) return "decision";
|
|
2743
|
+
if (/\b(architecture|architectural|design|system|technical|tech-spec)\b/.test(haystack)) return "architecture";
|
|
2744
|
+
if (/\b(feature|features|spec|requirements|prd|story|stories)\b/.test(haystack)) return "feature";
|
|
2745
|
+
if (/\b(memory|memories|lesson|lessons|mistake|mistakes|learning|retro|retrospective)\b/.test(haystack)) return "memory";
|
|
2746
|
+
if (/\b(plan|plans|roadmap|milestone|todo|task|tasks)\b/.test(haystack)) return "plan";
|
|
2747
|
+
if (/\b(workflow|workflows|runbook|playbook|process|procedure|ops)\b/.test(haystack)) return "workflow";
|
|
2748
|
+
return "context";
|
|
2749
|
+
}
|
|
2750
|
+
function canonicalImportPath(sourcePath, documentType) {
|
|
2751
|
+
if (isControlPlanePath2(sourcePath)) {
|
|
2752
|
+
return sourcePath;
|
|
2753
|
+
}
|
|
2754
|
+
const baseSlug = slugFromPath(sourcePath);
|
|
2755
|
+
return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
|
|
2756
|
+
}
|
|
2757
|
+
function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
2758
|
+
if (!usedPaths.has(basePath)) {
|
|
2759
|
+
usedPaths.add(basePath);
|
|
2760
|
+
return basePath;
|
|
2761
|
+
}
|
|
2762
|
+
const extension = path9.posix.extname(basePath) || ".md";
|
|
2763
|
+
const directory = path9.posix.dirname(basePath);
|
|
2764
|
+
const basename = path9.posix.basename(basePath, extension);
|
|
2765
|
+
const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
|
|
2766
|
+
usedPaths.add(uniquePath);
|
|
2767
|
+
return uniquePath;
|
|
2768
|
+
}
|
|
2769
|
+
function isControlPlanePath2(repoPath) {
|
|
2770
|
+
const [firstSegment] = repoPath.split("/");
|
|
2771
|
+
return Boolean(firstSegment && controlPlaneRoots.includes(firstSegment));
|
|
2772
|
+
}
|
|
2773
|
+
function inferTitle2(content, repoPath) {
|
|
2774
|
+
const body = stripFrontmatter(content);
|
|
2775
|
+
const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
|
|
2776
|
+
if (heading) return heading;
|
|
2777
|
+
const basename = path9.posix.basename(repoPath, path9.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
|
|
2778
|
+
return titleCase(basename || "Imported Document");
|
|
2779
|
+
}
|
|
2780
|
+
function stripFrontmatter(content) {
|
|
2781
|
+
if (!content.startsWith("---\n")) return content;
|
|
2782
|
+
const end = content.indexOf("\n---", 4);
|
|
2783
|
+
if (end === -1) return content;
|
|
2784
|
+
const closingLineEnd = content.indexOf("\n", end + 4);
|
|
2785
|
+
return closingLineEnd === -1 ? "" : content.slice(closingLineEnd + 1);
|
|
2786
|
+
}
|
|
2787
|
+
function slugFromPath(repoPath) {
|
|
2788
|
+
const withoutExtension = repoPath.replace(/\.(md|mdx)$/i, "");
|
|
2789
|
+
const slug = withoutExtension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 90);
|
|
2790
|
+
return slug || "imported-document";
|
|
2791
|
+
}
|
|
2792
|
+
function titleCase(value) {
|
|
2793
|
+
return value.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
2794
|
+
}
|
|
2795
|
+
function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePath) {
|
|
2796
|
+
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
2797
|
+
}
|
|
2798
|
+
function hashText(value, length) {
|
|
2799
|
+
return createHash3("sha256").update(value).digest("hex").slice(0, length);
|
|
2800
|
+
}
|
|
2801
|
+
function normalizeRepoPath2(value) {
|
|
2802
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
2803
|
+
}
|
|
2804
|
+
async function runGit2(args) {
|
|
2805
|
+
const { stdout } = await execFileAsync2("git", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
2806
|
+
return stdout.trim();
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/runner-actions.ts
|
|
2810
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2811
|
+
import path10 from "node:path";
|
|
2812
|
+
function buildBackgroundRunnerArgs(options) {
|
|
2813
|
+
const args = [
|
|
2814
|
+
"run",
|
|
2815
|
+
"--watch",
|
|
2816
|
+
"--api-url",
|
|
2817
|
+
options.apiUrl,
|
|
2818
|
+
"--runner-id",
|
|
2819
|
+
options.runnerId,
|
|
2820
|
+
"--root",
|
|
2821
|
+
path10.resolve(options.root),
|
|
2822
|
+
"--session",
|
|
2823
|
+
options.session,
|
|
2824
|
+
"--interval-seconds",
|
|
2825
|
+
String(options.intervalSeconds)
|
|
2826
|
+
];
|
|
2827
|
+
if (options.tool) {
|
|
2828
|
+
args.push("--tool", options.tool);
|
|
2829
|
+
}
|
|
2830
|
+
if (options.invocationChannel) {
|
|
2831
|
+
args.push("--invocation-channel", options.invocationChannel);
|
|
2832
|
+
}
|
|
2833
|
+
if (options.toolCommand) {
|
|
2834
|
+
args.push("--tool-command", options.toolCommand);
|
|
2835
|
+
}
|
|
2836
|
+
if (options.model) {
|
|
2837
|
+
args.push("--model", options.model);
|
|
2838
|
+
}
|
|
2839
|
+
if (options.maxIterations !== void 0) {
|
|
2840
|
+
args.push("--max-iterations", String(options.maxIterations));
|
|
2841
|
+
}
|
|
2842
|
+
if (!options.stream) {
|
|
2843
|
+
args.push("--no-stream");
|
|
2844
|
+
}
|
|
2845
|
+
return args;
|
|
2846
|
+
}
|
|
2847
|
+
async function runOfficialCliUpdate() {
|
|
2848
|
+
const result = await runOfficialUpdateProcess("npm", ["install", "-g", "@amistio/cli"], 12e4);
|
|
2849
|
+
if (result.exitCode === 0) {
|
|
2850
|
+
return { succeeded: true, message: "Official Amistio CLI update command completed." };
|
|
2851
|
+
}
|
|
2852
|
+
return { succeeded: false, message: "Official Amistio CLI update command failed.", error: result.output || `npm exited with code ${result.exitCode}.` };
|
|
2853
|
+
}
|
|
2854
|
+
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
2855
|
+
return new Promise((resolve) => {
|
|
2856
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2857
|
+
let output = "";
|
|
2858
|
+
const updateTimeout = setTimeout(() => {
|
|
2859
|
+
output += "Timed out while running official CLI update.\n";
|
|
2860
|
+
child.kill("SIGTERM");
|
|
2861
|
+
}, timeoutMs);
|
|
2862
|
+
child.stdout?.on("data", (chunk) => {
|
|
2863
|
+
output += chunk.toString("utf8");
|
|
2864
|
+
});
|
|
2865
|
+
child.stderr?.on("data", (chunk) => {
|
|
2866
|
+
output += chunk.toString("utf8");
|
|
2867
|
+
});
|
|
2868
|
+
child.on("error", (error) => {
|
|
2869
|
+
clearTimeout(updateTimeout);
|
|
2870
|
+
resolve({ exitCode: 1, output: error.message });
|
|
2871
|
+
});
|
|
2872
|
+
child.on("close", (code) => {
|
|
2873
|
+
clearTimeout(updateTimeout);
|
|
2874
|
+
resolve({ exitCode: code ?? 1, output: truncateProcessOutput(output) });
|
|
2875
|
+
});
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
function truncateProcessOutput(value) {
|
|
2879
|
+
const trimmed = value.trim();
|
|
2880
|
+
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/runner-tui.ts
|
|
2884
|
+
import { randomUUID } from "node:crypto";
|
|
2885
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2886
|
+
import os5 from "node:os";
|
|
2887
|
+
import path11 from "node:path";
|
|
2888
|
+
import * as readline from "node:readline";
|
|
2889
|
+
async function runRunnerTui(options) {
|
|
2890
|
+
const input = options.input ?? process.stdin;
|
|
2891
|
+
const output = options.output ?? process.stdout;
|
|
2892
|
+
if (!input.isTTY || !output.isTTY) {
|
|
2893
|
+
output.write(`${runnerTuiNonInteractiveMessage()}
|
|
2894
|
+
`);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
let selectedIndex = 0;
|
|
2898
|
+
let message;
|
|
2899
|
+
let state = await loadRunnerTuiState(options);
|
|
2900
|
+
const render = async () => {
|
|
2901
|
+
state = await loadRunnerTuiState(options);
|
|
2902
|
+
selectedIndex = clampSelectedIndex(state, selectedIndex);
|
|
2903
|
+
output.write(`\x1B[?25l\x1B[2J\x1B[H${renderRunnerTuiScreen(state, selectedIndex, { columns: output.columns, rows: output.rows, ...message ? { message } : {} })}`);
|
|
2904
|
+
};
|
|
2905
|
+
readline.emitKeypressEvents(input);
|
|
2906
|
+
input.setRawMode?.(true);
|
|
2907
|
+
input.resume();
|
|
2908
|
+
await render();
|
|
2909
|
+
await new Promise((resolve) => {
|
|
2910
|
+
let busy = false;
|
|
2911
|
+
const keypressHandler = (character, key) => {
|
|
2912
|
+
if (busy) return;
|
|
2913
|
+
busy = true;
|
|
2914
|
+
void handleRunnerTuiKey({ character, input, key, output, options, selectedIndex, state }).then(async (result) => {
|
|
2915
|
+
if (result.quit) {
|
|
2916
|
+
teardownRunnerTui(input, output, keypressHandler);
|
|
2917
|
+
resolve();
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
selectedIndex = result.selectedIndex;
|
|
2921
|
+
message = result.message;
|
|
2922
|
+
await render();
|
|
2923
|
+
}).catch(async (error) => {
|
|
2924
|
+
message = error instanceof Error ? error.message : String(error);
|
|
2925
|
+
await render();
|
|
2926
|
+
}).finally(() => {
|
|
2927
|
+
busy = false;
|
|
2928
|
+
});
|
|
2929
|
+
};
|
|
2930
|
+
input.on("keypress", keypressHandler);
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
async function loadRunnerTuiState(options) {
|
|
2934
|
+
const resolvedRoot = path11.resolve(options.root);
|
|
2935
|
+
const metadata = await readProjectLink(resolvedRoot);
|
|
2936
|
+
const credentialStore = options.credentialStore ?? new LocalCredentialStore();
|
|
2937
|
+
if (!metadata) {
|
|
2938
|
+
return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: options.remote, credentialPresent: false, remoteError: "Repository is not paired. Run `amistio pair` or `amistio import <code>` first." });
|
|
2939
|
+
}
|
|
2940
|
+
const token = await credentialStore.get(credentialKey(metadata.amistioAccountId, metadata.amistioProjectId, metadata.repositoryLinkId));
|
|
2941
|
+
const localRecords = await listRunnerDaemonMetadata({
|
|
2942
|
+
accountId: metadata.amistioAccountId,
|
|
2943
|
+
projectId: metadata.amistioProjectId,
|
|
2944
|
+
repositoryLinkId: metadata.repositoryLinkId,
|
|
2945
|
+
...options.runnerId ? { runnerId: options.runnerId } : {}
|
|
2946
|
+
}, options.metadataDir);
|
|
2947
|
+
if (!options.remote) {
|
|
2948
|
+
return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: false, credentialPresent: Boolean(token), metadata, localRecords });
|
|
2949
|
+
}
|
|
2950
|
+
if (!token) {
|
|
2951
|
+
return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: true, credentialPresent: false, metadata, localRecords, remoteError: "Remote runner state was not loaded because this checkout has no local runner credential." });
|
|
2952
|
+
}
|
|
2953
|
+
const client = new ApiClient({ apiUrl: options.apiUrl, accountId: metadata.amistioAccountId, token });
|
|
2954
|
+
try {
|
|
2955
|
+
const [{ runners }, preferences] = await Promise.all([
|
|
2956
|
+
client.listRunners(metadata.amistioProjectId),
|
|
2957
|
+
client.getRunnerPreferences(metadata.amistioProjectId).catch(() => void 0)
|
|
2958
|
+
]);
|
|
2959
|
+
const projectRunners = runners.filter((runner2) => runner2.repositoryLinkId === metadata.repositoryLinkId).filter((runner2) => !options.runnerId || runner2.runnerId === options.runnerId);
|
|
2960
|
+
const commandGroups = await Promise.all(projectRunners.map((runner2) => client.listRunnerCommands(metadata.amistioProjectId, runner2.runnerId, runner2.repositoryLinkId).then((result) => result.commands).catch(() => [])));
|
|
2961
|
+
return createRunnerTuiState({
|
|
2962
|
+
root: resolvedRoot,
|
|
2963
|
+
apiUrl: options.apiUrl,
|
|
2964
|
+
remoteEnabled: true,
|
|
2965
|
+
credentialPresent: true,
|
|
2966
|
+
metadata,
|
|
2967
|
+
localRecords,
|
|
2968
|
+
remoteRunners: projectRunners,
|
|
2969
|
+
remoteCommands: commandGroups.flat(),
|
|
2970
|
+
...preferences ? { preferences } : {}
|
|
2971
|
+
});
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: true, credentialPresent: true, metadata, localRecords, remoteError: error instanceof Error ? error.message : String(error) });
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function createRunnerTuiState(input) {
|
|
2977
|
+
const localRecords = input.localRecords ?? [];
|
|
2978
|
+
const remoteRunners = input.remoteRunners ?? [];
|
|
2979
|
+
const remoteCommands = input.remoteCommands ?? [];
|
|
2980
|
+
const runnersByKey = /* @__PURE__ */ new Map();
|
|
2981
|
+
for (const record of localRecords) {
|
|
2982
|
+
const runnerKey = runnerKeyFor(record.runnerId, record.repositoryLinkId);
|
|
2983
|
+
runnersByKey.set(runnerKey, {
|
|
2984
|
+
runnerId: record.runnerId,
|
|
2985
|
+
repositoryLinkId: record.repositoryLinkId,
|
|
2986
|
+
source: "local",
|
|
2987
|
+
local: {
|
|
2988
|
+
metadata: record,
|
|
2989
|
+
runtimeStatus: runnerDaemonRuntimeStatus(record),
|
|
2990
|
+
uptime: runnerDaemonRuntimeStatus(record) === "running" ? runnerDaemonUptime(record, input.nowMs) : "not running"
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
for (const heartbeat of remoteRunners) {
|
|
2995
|
+
const runnerKey = runnerKeyFor(heartbeat.runnerId, heartbeat.repositoryLinkId);
|
|
2996
|
+
const existing = runnersByKey.get(runnerKey);
|
|
2997
|
+
const latestCommand = latestRunnerCommand(remoteCommands, heartbeat.runnerId, heartbeat.repositoryLinkId);
|
|
2998
|
+
if (existing) {
|
|
2999
|
+
runnersByKey.set(runnerKey, { ...existing, heartbeat, ...latestCommand ? { latestCommand } : {} });
|
|
3000
|
+
} else {
|
|
3001
|
+
runnersByKey.set(runnerKey, { runnerId: heartbeat.runnerId, repositoryLinkId: heartbeat.repositoryLinkId, source: "remote", heartbeat, ...latestCommand ? { latestCommand } : {} });
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
const runners = [...runnersByKey.values()].sort((first, second) => {
|
|
3005
|
+
if (first.source !== second.source) return first.source === "local" ? -1 : 1;
|
|
3006
|
+
return first.runnerId.localeCompare(second.runnerId);
|
|
3007
|
+
});
|
|
3008
|
+
return {
|
|
3009
|
+
root: path11.resolve(input.root),
|
|
3010
|
+
apiUrl: input.apiUrl,
|
|
3011
|
+
paired: Boolean(input.metadata),
|
|
3012
|
+
credentialPresent: input.credentialPresent,
|
|
3013
|
+
remoteEnabled: input.remoteEnabled,
|
|
3014
|
+
runners,
|
|
3015
|
+
localRunnerCount: localRecords.length,
|
|
3016
|
+
remoteRunnerCount: remoteRunners.length,
|
|
3017
|
+
...input.metadata ? { metadata: input.metadata } : {},
|
|
3018
|
+
...input.preferences ? { preferences: input.preferences } : {},
|
|
3019
|
+
...input.remoteError ? { remoteError: input.remoteError } : {}
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
function renderRunnerTuiScreen(state, selectedIndex = 0, options = {}) {
|
|
3023
|
+
const columns = Math.max(40, options.columns ?? 100);
|
|
3024
|
+
const rows = Math.max(8, options.rows ?? 32);
|
|
3025
|
+
const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
|
|
3026
|
+
const lines = [];
|
|
3027
|
+
lines.push("Amistio Runner UI");
|
|
3028
|
+
lines.push(`Project: ${state.metadata?.amistioProjectId ?? "unpaired"} Repository link: ${state.metadata?.repositoryLinkId ?? "none"}`);
|
|
3029
|
+
lines.push(`API: ${state.apiUrl}`);
|
|
3030
|
+
lines.push(`Root: ${state.root}`);
|
|
3031
|
+
lines.push(`Credential: ${state.credentialPresent ? "available" : "missing"} Remote: ${state.remoteEnabled ? state.remoteError ? "warning" : "enabled" : "local only"}`);
|
|
3032
|
+
if (state.remoteError) lines.push(`Remote: ${state.remoteError}`);
|
|
3033
|
+
lines.push(`Preferences: ${formatPreferenceSummary(state.preferences)}`);
|
|
3034
|
+
lines.push("");
|
|
3035
|
+
lines.push(`Runners (${state.localRunnerCount} local, ${state.remoteRunnerCount} remote heartbeat${state.remoteRunnerCount === 1 ? "" : "s"})`);
|
|
3036
|
+
if (!state.runners.length) {
|
|
3037
|
+
lines.push(" No runner records found. Press s to start a background runner, or run `amistio run --background`.");
|
|
3038
|
+
}
|
|
3039
|
+
state.runners.forEach((runner2, index) => {
|
|
3040
|
+
const marker = index === clampSelectedIndex(state, selectedIndex) ? ">" : " ";
|
|
3041
|
+
lines.push(`${marker} ${formatRunnerListLine(runner2)}`);
|
|
3042
|
+
});
|
|
3043
|
+
lines.push("");
|
|
3044
|
+
lines.push("Details");
|
|
3045
|
+
if (selectedRunner) {
|
|
3046
|
+
lines.push(...formatRunnerDetails(selectedRunner));
|
|
3047
|
+
} else {
|
|
3048
|
+
lines.push(" No runner selected.");
|
|
3049
|
+
}
|
|
3050
|
+
lines.push("");
|
|
3051
|
+
lines.push("Preference Editing");
|
|
3052
|
+
lines.push(" Account/project runner preferences are read-only here until a user-authenticated CLI path exists.");
|
|
3053
|
+
const availability = runnerTuiActionAvailability(state, selectedIndex);
|
|
3054
|
+
lines.push(`Actions: r refresh | s start${availability.start ? "" : " (needs credential)"} | x stop | b restart | u update CLI | d local remove | l logs | q quit`);
|
|
3055
|
+
if (options.message) {
|
|
3056
|
+
lines.push("");
|
|
3057
|
+
lines.push("Status");
|
|
3058
|
+
lines.push(...options.message.split("\n").map((line) => ` ${line}`));
|
|
3059
|
+
}
|
|
3060
|
+
return `${lines.slice(0, rows).map((line) => fitLine(line, columns)).join("\n")}
|
|
3061
|
+
`;
|
|
3062
|
+
}
|
|
3063
|
+
function runnerTuiActionAvailability(state, selectedIndex = 0) {
|
|
3064
|
+
const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
|
|
3065
|
+
return {
|
|
3066
|
+
start: state.paired && state.credentialPresent,
|
|
3067
|
+
stop: Boolean(selectedRunner?.local),
|
|
3068
|
+
restart: Boolean(selectedRunner?.local),
|
|
3069
|
+
update: state.paired,
|
|
3070
|
+
localRemove: state.paired && state.credentialPresent,
|
|
3071
|
+
logs: Boolean(selectedRunner?.local?.metadata.logPath)
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
function runnerTuiNonInteractiveMessage() {
|
|
3075
|
+
return "Terminal UI requires an interactive TTY. Use `amistio runner status` for status or `amistio run --background` to start a background runner.";
|
|
3076
|
+
}
|
|
3077
|
+
async function handleRunnerTuiKey({ character, input, key, options, output, selectedIndex, state }) {
|
|
3078
|
+
if (key.ctrl && key.name === "c" || character === "q") return { quit: true, selectedIndex };
|
|
3079
|
+
if (key.name === "up") return { selectedIndex: Math.max(0, selectedIndex - 1) };
|
|
3080
|
+
if (key.name === "down") return { selectedIndex: Math.min(Math.max(0, state.runners.length - 1), selectedIndex + 1) };
|
|
3081
|
+
if (character === "r") return { selectedIndex, message: "Refreshed runner state." };
|
|
3082
|
+
if (character === "s") return { selectedIndex, message: await promptAndStartRunner(state, options, input, output) };
|
|
3083
|
+
if (character === "x") return { selectedIndex, message: await stopSelectedRunner(state, selectedIndex, options) };
|
|
3084
|
+
if (character === "b") return { selectedIndex, message: await restartSelectedRunner(state, selectedIndex, options) };
|
|
3085
|
+
if (character === "u") return { selectedIndex, message: await confirmAndUpdateCli(input, output) };
|
|
3086
|
+
if (character === "d") return { selectedIndex, message: await confirmAndRemoveLocalCredential(state, options, input, output) };
|
|
3087
|
+
if (character === "l") return { selectedIndex, message: await readSelectedRunnerLog(state, selectedIndex) };
|
|
3088
|
+
return { selectedIndex, message: "Unknown key. Use r, s, x, b, u, d, l, or q." };
|
|
3089
|
+
}
|
|
3090
|
+
async function promptAndStartRunner(state, options, input, output) {
|
|
3091
|
+
const context = await loadRunnerTuiContext(state, options);
|
|
3092
|
+
if (!context.token) {
|
|
3093
|
+
return "Cannot start a background runner because this checkout has no local runner credential.";
|
|
3094
|
+
}
|
|
3095
|
+
const startOptions = await promptForStartRunnerOptions(input, output, options);
|
|
3096
|
+
const metadata = await startRunnerDaemon({
|
|
3097
|
+
accountId: context.metadata.amistioAccountId,
|
|
3098
|
+
projectId: context.metadata.amistioProjectId,
|
|
3099
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3100
|
+
runnerId: startOptions.runnerId,
|
|
3101
|
+
rootDir: path11.resolve(options.root),
|
|
3102
|
+
apiUrl: options.apiUrl,
|
|
3103
|
+
args: buildBackgroundRunnerArgs({
|
|
3104
|
+
apiUrl: options.apiUrl,
|
|
3105
|
+
runnerId: startOptions.runnerId,
|
|
3106
|
+
root: options.root,
|
|
3107
|
+
...startOptions.tool ? { tool: startOptions.tool } : {},
|
|
3108
|
+
...startOptions.invocationChannel ? { invocationChannel: startOptions.invocationChannel } : {},
|
|
3109
|
+
...startOptions.model ? { model: startOptions.model } : {},
|
|
3110
|
+
session: startOptions.session,
|
|
3111
|
+
intervalSeconds: startOptions.intervalSeconds,
|
|
3112
|
+
stream: startOptions.stream
|
|
3113
|
+
}),
|
|
3114
|
+
...options.metadataDir ? { metadataDir: options.metadataDir } : {}
|
|
3115
|
+
});
|
|
3116
|
+
return `Started background runner ${metadata.runnerId} with PID ${metadata.pid}.${metadata.logPath ? `
|
|
3117
|
+
Log: ${metadata.logPath}` : ""}`;
|
|
3118
|
+
}
|
|
3119
|
+
async function promptForStartRunnerOptions(input, output, options) {
|
|
3120
|
+
const runnerId = (await promptLine(input, output, `Runner ID [${options.runnerId ?? "new"}]: `)).trim() || options.runnerId || `runner_${randomUUID()}`;
|
|
3121
|
+
const tool = parseOptionalTool(await promptLine(input, output, "AI tool / SDK [remote preference; auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent]: "));
|
|
3122
|
+
const invocationChannel = parseOptionalInvocationChannel(await promptLine(input, output, "Invocation channel [remote preference; auto, sdk, command]: "));
|
|
3123
|
+
const model = optionalTrim(await promptLine(input, output, "Model [provider default]: "));
|
|
3124
|
+
const sessionInput = optionalTrim(await promptLine(input, output, "Session policy [auto]: ")) ?? "auto";
|
|
3125
|
+
const session = sessionPolicySchema.parse(sessionInput);
|
|
3126
|
+
const intervalInput = optionalTrim(await promptLine(input, output, `Polling interval seconds [${options.intervalSeconds}]: `));
|
|
3127
|
+
const intervalSeconds = intervalInput ? parsePositiveInteger(intervalInput) : options.intervalSeconds;
|
|
3128
|
+
const streamInput = optionalTrim(await promptLine(input, output, "Stream output from background runner? [Y/n]: "));
|
|
3129
|
+
const stream = !streamInput || streamInput.toLowerCase() === "y" || streamInput.toLowerCase() === "yes";
|
|
3130
|
+
return { runnerId, ...tool ? { tool } : {}, ...invocationChannel ? { invocationChannel } : {}, ...model ? { model } : {}, session, intervalSeconds, stream };
|
|
3131
|
+
}
|
|
3132
|
+
async function stopSelectedRunner(state, selectedIndex, options) {
|
|
3133
|
+
const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
|
|
3134
|
+
if (!selectedRunner?.local) return "Select a local background runner to stop.";
|
|
3135
|
+
const stopResult = await stopRunnerDaemonProcess(selectedRunner.local.metadata);
|
|
3136
|
+
await markRunnerDaemonStopped(selectedRunner.local.metadata, options.metadataDir);
|
|
3137
|
+
const context = await loadRunnerTuiContext(state, options).catch(() => void 0);
|
|
3138
|
+
if (context?.token) {
|
|
3139
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, selectedRunner.runnerId, selectedRunner.repositoryLinkId, "offline", { version: "0.1.3", mode: "background", hostname: os5.hostname() }).catch(() => void 0);
|
|
3140
|
+
}
|
|
3141
|
+
return stopResult === "stopped" ? `Stopped background runner ${selectedRunner.runnerId}.` : `Marked background runner ${selectedRunner.runnerId} stopped; process was not running.`;
|
|
3142
|
+
}
|
|
3143
|
+
async function restartSelectedRunner(state, selectedIndex, options) {
|
|
3144
|
+
const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
|
|
3145
|
+
if (!selectedRunner?.local) return "Select a local background runner to restart.";
|
|
3146
|
+
await stopRunnerDaemonProcess(selectedRunner.local.metadata).catch(() => "not-running");
|
|
3147
|
+
await markRunnerDaemonStopped(selectedRunner.local.metadata, options.metadataDir);
|
|
3148
|
+
const replacement = await restartRunnerDaemonProcess(
|
|
3149
|
+
selectedRunner.local.metadata,
|
|
3150
|
+
buildBackgroundRunnerArgs({ apiUrl: selectedRunner.local.metadata.apiUrl, runnerId: selectedRunner.runnerId, root: selectedRunner.local.metadata.rootDir, session: "auto", intervalSeconds: options.intervalSeconds, stream: true }),
|
|
3151
|
+
{ ...options.metadataDir ? { metadataDir: options.metadataDir } : {} }
|
|
3152
|
+
);
|
|
3153
|
+
return `Restarted background runner ${replacement.runnerId} with PID ${replacement.pid}.`;
|
|
3154
|
+
}
|
|
3155
|
+
async function confirmAndUpdateCli(input, output) {
|
|
3156
|
+
const confirmation = await promptLine(input, output, "Run the official Amistio CLI update now? Type update to continue: ");
|
|
3157
|
+
if (confirmation.trim() !== "update") return "Update cancelled.";
|
|
3158
|
+
const result = await runOfficialCliUpdate();
|
|
3159
|
+
return result.succeeded ? result.message : `${result.message}${result.error ? `
|
|
3160
|
+
${result.error}` : ""}`;
|
|
3161
|
+
}
|
|
3162
|
+
async function confirmAndRemoveLocalCredential(state, options, input, output) {
|
|
3163
|
+
const context = await loadRunnerTuiContext(state, options);
|
|
3164
|
+
if (!context.token) return "No local runner credential is stored for this paired repository.";
|
|
3165
|
+
const confirmation = await promptLine(input, output, "Remove this machine's runner credential? This does not delete source files, local checkouts, hosted repositories, project records, or team data. Type remove local to continue: ");
|
|
3166
|
+
if (confirmation.trim() !== "remove local") return "Local remove cancelled.";
|
|
3167
|
+
const localRunners = state.runners.filter((runner2) => runner2.local);
|
|
3168
|
+
for (const runner2 of localRunners) {
|
|
3169
|
+
await stopRunnerDaemonProcess(runner2.local.metadata).catch(() => "not-running");
|
|
3170
|
+
await markRunnerDaemonStopped(runner2.local.metadata, options.metadataDir).catch(() => void 0);
|
|
3171
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runner2.runnerId, runner2.repositoryLinkId, "offline", { version: "0.1.3", mode: "background", hostname: os5.hostname() }).catch(() => void 0);
|
|
3172
|
+
}
|
|
3173
|
+
await context.credentialStore.delete(credentialKey(context.metadata.amistioAccountId, context.metadata.amistioProjectId, context.metadata.repositoryLinkId));
|
|
3174
|
+
return `Removed this machine's local runner credential for ${context.metadata.repositoryLinkId}. Source files, hosted repositories, project records, and team data were not deleted.`;
|
|
3175
|
+
}
|
|
3176
|
+
async function readSelectedRunnerLog(state, selectedIndex) {
|
|
3177
|
+
const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
|
|
3178
|
+
const logPath = selectedRunner?.local?.metadata.logPath;
|
|
3179
|
+
if (!logPath) return "Selected runner has no local log path.";
|
|
3180
|
+
try {
|
|
3181
|
+
const content = await readFile7(logPath, "utf8");
|
|
3182
|
+
const excerpt = content.trim().slice(-3e3);
|
|
3183
|
+
return excerpt ? `Log: ${logPath}
|
|
3184
|
+
${excerpt}` : `Log: ${logPath}
|
|
3185
|
+
No log output yet.`;
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
return `Could not read ${logPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
async function loadRunnerTuiContext(state, options) {
|
|
3191
|
+
if (!state.metadata) {
|
|
3192
|
+
throw new Error("Repository is not paired. Run `amistio pair` or `amistio import <code>` first.");
|
|
3193
|
+
}
|
|
3194
|
+
const credentialStore = options.credentialStore ?? new LocalCredentialStore();
|
|
3195
|
+
const token = await credentialStore.get(credentialKey(state.metadata.amistioAccountId, state.metadata.amistioProjectId, state.metadata.repositoryLinkId));
|
|
3196
|
+
return {
|
|
3197
|
+
metadata: state.metadata,
|
|
3198
|
+
credentialStore,
|
|
3199
|
+
...token ? { token } : {},
|
|
3200
|
+
client: new ApiClient({ apiUrl: options.apiUrl, accountId: state.metadata.amistioAccountId, ...token ? { token } : {} })
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
function formatRunnerListLine(runner2) {
|
|
3204
|
+
const local = runner2.local ? `local ${runner2.local.metadata.status}/${runner2.local.runtimeStatus} pid ${runner2.local.metadata.pid}` : "remote-only";
|
|
3205
|
+
const heartbeat = runner2.heartbeat ? `heartbeat ${runner2.heartbeat.status}${runner2.heartbeat.mode ? ` ${runner2.heartbeat.mode}` : ""} ${runner2.heartbeat.lastSeenAt}` : "no heartbeat";
|
|
3206
|
+
return `${runner2.runnerId} | ${local} | ${heartbeat}`;
|
|
3207
|
+
}
|
|
3208
|
+
function formatRunnerDetails(runner2) {
|
|
3209
|
+
const lines = [];
|
|
3210
|
+
lines.push(` Runner ID: ${runner2.runnerId}`);
|
|
3211
|
+
lines.push(` Repository link: ${runner2.repositoryLinkId}`);
|
|
3212
|
+
if (runner2.local) {
|
|
3213
|
+
lines.push(` Local process: ${runner2.local.runtimeStatus}, PID ${runner2.local.metadata.pid}, uptime ${runner2.local.uptime}`);
|
|
3214
|
+
lines.push(` Local root: ${runner2.local.metadata.rootDir}`);
|
|
3215
|
+
lines.push(` Host: ${runner2.local.metadata.hostname}`);
|
|
3216
|
+
if (runner2.local.metadata.logPath) lines.push(` Log: ${runner2.local.metadata.logPath}`);
|
|
3217
|
+
} else {
|
|
3218
|
+
lines.push(" Local process: not on this machine");
|
|
3219
|
+
}
|
|
3220
|
+
if (runner2.heartbeat) {
|
|
3221
|
+
const effectiveTool = runner2.heartbeat.effectiveTool ?? runner2.heartbeat.requestedTool ?? "unknown";
|
|
3222
|
+
const requestedTool = runner2.heartbeat.requestedTool ?? "auto";
|
|
3223
|
+
const channel = runner2.heartbeat.effectiveInvocationChannel ?? runner2.heartbeat.requestedInvocationChannel ?? "auto";
|
|
3224
|
+
lines.push(` Last heartbeat: ${runner2.heartbeat.status} at ${runner2.heartbeat.lastSeenAt}${runner2.heartbeat.version ? ` (${runner2.heartbeat.version})` : ""}`);
|
|
3225
|
+
lines.push(` Tool: ${requestedTool}${effectiveTool !== requestedTool ? ` -> ${effectiveTool}` : ""}`);
|
|
3226
|
+
lines.push(` Channel: ${channel}`);
|
|
3227
|
+
lines.push(` Model: ${runner2.heartbeat.effectiveModel ?? "provider default"}`);
|
|
3228
|
+
lines.push(` Preference: ${runner2.heartbeat.preferenceSource ?? "default"} / ${runner2.heartbeat.preferenceStatus ?? "pending"}`);
|
|
3229
|
+
if (runner2.heartbeat.preferenceMessage) lines.push(` Warning: ${runner2.heartbeat.preferenceMessage}`);
|
|
3230
|
+
lines.push(` Capabilities: ${formatCapabilities(runner2.heartbeat)}`);
|
|
3231
|
+
} else {
|
|
3232
|
+
lines.push(" Last heartbeat: not loaded");
|
|
3233
|
+
}
|
|
3234
|
+
if (runner2.latestCommand) {
|
|
3235
|
+
lines.push(` Latest command: ${runner2.latestCommand.commandKind} ${runner2.latestCommand.status}${runner2.latestCommand.message ? ` - ${runner2.latestCommand.message}` : ""}`);
|
|
3236
|
+
} else {
|
|
3237
|
+
lines.push(" Latest command: none loaded");
|
|
3238
|
+
}
|
|
3239
|
+
return lines;
|
|
3240
|
+
}
|
|
3241
|
+
function formatCapabilities(runner2) {
|
|
3242
|
+
const availableCapabilities = runner2.capabilities?.filter((capability) => capability.available) ?? [];
|
|
3243
|
+
if (!availableCapabilities.length) return "unknown";
|
|
3244
|
+
return availableCapabilities.map((capability) => `${capability.name} (${capability.sdkAvailable && capability.commandAvailable ? "sdk+command" : capability.sdkAvailable ? "sdk" : capability.commandAvailable ? "command" : capability.execution})`).join(", ");
|
|
3245
|
+
}
|
|
3246
|
+
function formatPreferenceSummary(preferences) {
|
|
3247
|
+
if (!preferences) return "not loaded";
|
|
3248
|
+
const effective = preferences.effective;
|
|
3249
|
+
return `${effective.source}: ${effective.tool} / ${effective.invocationChannel} / ${effective.model ?? "provider default"}${formatSettingsSuffix(preferences.project, "project")}${formatSettingsSuffix(preferences.account, "account")}`;
|
|
3250
|
+
}
|
|
3251
|
+
function formatSettingsSuffix(settings, label) {
|
|
3252
|
+
if (!settings) return "";
|
|
3253
|
+
const preference = settings.preferences;
|
|
3254
|
+
return `; ${label} ${preference.tool ?? "unset"}/${preference.invocationChannel ?? "unset"}/${preference.model ?? "provider default"}`;
|
|
3255
|
+
}
|
|
3256
|
+
function latestRunnerCommand(commands, runnerId, repositoryLinkId) {
|
|
3257
|
+
return commands.filter((command) => command.runnerId === runnerId && command.repositoryLinkId === repositoryLinkId).sort((first, second) => Date.parse(second.createdAt) - Date.parse(first.createdAt))[0];
|
|
3258
|
+
}
|
|
3259
|
+
function runnerKeyFor(runnerId, repositoryLinkId) {
|
|
3260
|
+
return `${repositoryLinkId}:${runnerId}`;
|
|
3261
|
+
}
|
|
3262
|
+
function clampSelectedIndex(state, selectedIndex) {
|
|
3263
|
+
if (!state.runners.length) return 0;
|
|
3264
|
+
return Math.max(0, Math.min(state.runners.length - 1, selectedIndex));
|
|
3265
|
+
}
|
|
3266
|
+
function fitLine(line, columns) {
|
|
3267
|
+
if (line.length <= columns) return line;
|
|
3268
|
+
return `${line.slice(0, Math.max(0, columns - 3))}...`;
|
|
3269
|
+
}
|
|
3270
|
+
function parseOptionalTool(value) {
|
|
3271
|
+
const trimmed = optionalTrim(value);
|
|
3272
|
+
if (!trimmed) return void 0;
|
|
3273
|
+
if (trimmed === "auto" || trimmed === "none" || runnerToolNames.includes(trimmed)) {
|
|
3274
|
+
return trimmed;
|
|
3275
|
+
}
|
|
3276
|
+
throw new Error(`Expected auto, none, ${runnerToolNames.join(", ")}; received ${trimmed}.`);
|
|
3277
|
+
}
|
|
3278
|
+
function parseOptionalInvocationChannel(value) {
|
|
3279
|
+
const trimmed = optionalTrim(value);
|
|
3280
|
+
if (!trimmed) return void 0;
|
|
3281
|
+
return runnerInvocationChannelSchema.parse(trimmed);
|
|
3282
|
+
}
|
|
3283
|
+
function parsePositiveInteger(value) {
|
|
3284
|
+
const parsed = Number(value);
|
|
3285
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3286
|
+
throw new Error(`Expected a positive integer, received ${value}.`);
|
|
3287
|
+
}
|
|
3288
|
+
return parsed;
|
|
3289
|
+
}
|
|
3290
|
+
function optionalTrim(value) {
|
|
3291
|
+
const trimmed = value.trim();
|
|
3292
|
+
return trimmed ? trimmed : void 0;
|
|
3293
|
+
}
|
|
3294
|
+
function promptLine(input, output, question) {
|
|
3295
|
+
input.setRawMode?.(false);
|
|
3296
|
+
return new Promise((resolve) => {
|
|
3297
|
+
const prompt = readline.createInterface({ input, output });
|
|
3298
|
+
prompt.question(question, (answer) => {
|
|
3299
|
+
prompt.close();
|
|
3300
|
+
input.setRawMode?.(true);
|
|
3301
|
+
resolve(answer);
|
|
3302
|
+
});
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
function teardownRunnerTui(input, output, keypressHandler) {
|
|
3306
|
+
input.setRawMode?.(false);
|
|
3307
|
+
input.off("keypress", keypressHandler);
|
|
3308
|
+
output.write("\x1B[?25h\n");
|
|
3309
|
+
}
|
|
3310
|
+
|
|
2487
3311
|
// src/index.ts
|
|
2488
3312
|
var program = new Command();
|
|
2489
3313
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
2490
3314
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
2491
|
-
program.name("amistio").description("Amistio project brain CLI").version("0.1.
|
|
2492
|
-
var CLI_VERSION = "0.1.
|
|
3315
|
+
program.name("amistio").description("Amistio project brain CLI").version("0.1.3");
|
|
3316
|
+
var CLI_VERSION = "0.1.3";
|
|
2493
3317
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
2494
3318
|
const created = await initControlPlane(options.root);
|
|
2495
3319
|
console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
|
|
@@ -2530,8 +3354,76 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
|
|
|
2530
3354
|
console.log(`Wrote non-secret project metadata to ${filePath}.`);
|
|
2531
3355
|
console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
|
|
2532
3356
|
});
|
|
3357
|
+
program.command("import").description("Pair an existing checkout and import legacy Markdown docs for review").argument("[code]", "Short-lived pairing code from the Amistio app").option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--root <path>", "Repository root", defaultRoot).option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--default-branch <branch>", "Default branch fallback", "main").option("--include <glob>", "Only import files matching a repo-relative glob", collectRepeatedOption, []).option("--exclude <glob>", "Exclude files matching a repo-relative glob", collectRepeatedOption, []).option("--max-file-kb <kb>", "Maximum Markdown file size to import", parsePositiveInteger2, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
|
|
3358
|
+
const pairingCode = (options.pairingCode ?? code)?.trim();
|
|
3359
|
+
if (!pairingCode) {
|
|
3360
|
+
throw new Error("Provide a pairing code as `amistio import <code>` or with `--pairing-code <code>`.");
|
|
3361
|
+
}
|
|
3362
|
+
const repository = await inspectLocalRepository(options.root, options.defaultBranch);
|
|
3363
|
+
const scan = await scanLegacyDocuments({
|
|
3364
|
+
rootDir: repository.rootDir,
|
|
3365
|
+
include: options.include,
|
|
3366
|
+
exclude: options.exclude,
|
|
3367
|
+
maxFileKb: options.maxFileKb
|
|
3368
|
+
});
|
|
3369
|
+
const skipCounts = importSkipCounts(scan.skipped);
|
|
3370
|
+
console.log(`Repository: ${repository.repoName}`);
|
|
3371
|
+
console.log(`Root: ${repository.rootDir}`);
|
|
3372
|
+
console.log(`Default branch: ${repository.defaultBranch}`);
|
|
3373
|
+
if (repository.originRemoteWarning) {
|
|
3374
|
+
console.log(repository.originRemoteWarning);
|
|
3375
|
+
}
|
|
3376
|
+
console.log(`Import candidates: ${scan.candidates.length}`);
|
|
3377
|
+
console.log(formatImportSkipSummary(skipCounts));
|
|
3378
|
+
for (const candidate of scan.candidates.slice(0, 12)) {
|
|
3379
|
+
console.log(`- ${candidate.sourcePath} -> ${candidate.repoPath} (${candidate.documentType})`);
|
|
3380
|
+
}
|
|
3381
|
+
if (scan.candidates.length > 12) {
|
|
3382
|
+
console.log(`...and ${scan.candidates.length - 12} more.`);
|
|
3383
|
+
}
|
|
3384
|
+
if (options.dryRun) {
|
|
3385
|
+
console.log("Dry run complete. No pairing code was consumed and no files or Amistio records were written.");
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const parsedCloneUrl = repository.parsedCloneUrl;
|
|
3389
|
+
const pairing = await new ApiClient({ apiUrl: options.apiUrl }).importPairingSession({
|
|
3390
|
+
pairingCode,
|
|
3391
|
+
repoName: repository.repoName,
|
|
3392
|
+
repoFingerprint: repository.repoFingerprint,
|
|
3393
|
+
defaultBranch: repository.defaultBranch,
|
|
3394
|
+
...parsedCloneUrl ? { cloneUrl: parsedCloneUrl.cloneUrl } : {},
|
|
3395
|
+
...parsedCloneUrl?.provider ? { provider: parsedCloneUrl.provider } : {},
|
|
3396
|
+
...parsedCloneUrl?.repoOwner ? { repoOwner: parsedCloneUrl.repoOwner } : {},
|
|
3397
|
+
...parsedCloneUrl?.repoFullName ? { repoFullName: parsedCloneUrl.repoFullName } : {}
|
|
3398
|
+
});
|
|
3399
|
+
await initControlPlane(repository.rootDir);
|
|
3400
|
+
const metadataFilePath = await writeProjectLink(repository.rootDir, {
|
|
3401
|
+
amistioAccountId: pairing.accountId,
|
|
3402
|
+
amistioProjectId: pairing.projectId,
|
|
3403
|
+
repositoryLinkId: pairing.repositoryLink.repositoryLinkId,
|
|
3404
|
+
defaultBranch: repository.defaultBranch,
|
|
3405
|
+
lastSyncedRevision: 0
|
|
3406
|
+
});
|
|
3407
|
+
await new LocalCredentialStore().set(credentialKey(pairing.accountId, pairing.projectId, pairing.repositoryLink.repositoryLinkId), pairing.token);
|
|
3408
|
+
const authenticatedClient = new ApiClient({ apiUrl: options.apiUrl, accountId: pairing.accountId, token: pairing.token });
|
|
3409
|
+
const { documents: existingDocuments } = await authenticatedClient.listBrainDocuments(pairing.projectId);
|
|
3410
|
+
const documents = buildImportedBrainDocuments({
|
|
3411
|
+
accountId: pairing.accountId,
|
|
3412
|
+
projectId: pairing.projectId,
|
|
3413
|
+
repositoryLinkId: pairing.repositoryLink.repositoryLinkId,
|
|
3414
|
+
candidates: scan.candidates,
|
|
3415
|
+
existingDocuments
|
|
3416
|
+
});
|
|
3417
|
+
if (documents.length) {
|
|
3418
|
+
await authenticatedClient.pushBrainDocuments(pairing.projectId, documents);
|
|
3419
|
+
}
|
|
3420
|
+
console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}; repository link ${pairing.repositoryLinkAction}.`);
|
|
3421
|
+
console.log(`Wrote non-secret project metadata to ${metadataFilePath}.`);
|
|
3422
|
+
console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"} for review.`);
|
|
3423
|
+
console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
|
|
3424
|
+
});
|
|
2533
3425
|
program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
|
|
2534
|
-
let repositoryLinkId = options.repositoryLink ?? `repo_${
|
|
3426
|
+
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
|
|
2535
3427
|
let credential = options.token;
|
|
2536
3428
|
if (options.pairingCode) {
|
|
2537
3429
|
const pairing = await new ApiClient({
|
|
@@ -2691,7 +3583,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
|
|
|
2691
3583
|
}
|
|
2692
3584
|
console.log("custom - pass --tool-command to use any other local runner command.");
|
|
2693
3585
|
});
|
|
2694
|
-
program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
|
|
3586
|
+
program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
|
|
2695
3587
|
const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
|
|
2696
3588
|
const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
|
|
2697
3589
|
if (options.promptOut) {
|
|
@@ -2703,16 +3595,17 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2703
3595
|
return;
|
|
2704
3596
|
}
|
|
2705
3597
|
const sessionPolicy = normalizeSessionPolicy(options.session);
|
|
2706
|
-
const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
|
|
3598
|
+
const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, invocationChannel: options.invocationChannel, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
|
|
2707
3599
|
console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
2708
3600
|
const result = await runLocalTool({
|
|
2709
3601
|
rootDir: options.root,
|
|
2710
3602
|
prompt,
|
|
2711
3603
|
tool: options.tool,
|
|
3604
|
+
invocationChannel: options.invocationChannel,
|
|
2712
3605
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
2713
3606
|
...options.model ? { model: options.model } : {},
|
|
2714
3607
|
streamOutput: options.stream,
|
|
2715
|
-
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${
|
|
3608
|
+
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
|
|
2716
3609
|
});
|
|
2717
3610
|
if (!options.stream && result.stdout.trim()) {
|
|
2718
3611
|
console.log(result.stdout.trim());
|
|
@@ -2724,7 +3617,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2724
3617
|
process.exitCode = result.exitCode;
|
|
2725
3618
|
}
|
|
2726
3619
|
});
|
|
2727
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${
|
|
3620
|
+
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID2()}`).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", parsePositiveInteger2, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger2).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
|
|
2728
3621
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2729
3622
|
if (!context) {
|
|
2730
3623
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -2746,7 +3639,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2746
3639
|
projectId: context.metadata.amistioProjectId,
|
|
2747
3640
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
2748
3641
|
runnerId: options.runnerId,
|
|
2749
|
-
rootDir:
|
|
3642
|
+
rootDir: path12.resolve(options.root),
|
|
2750
3643
|
apiUrl: options.apiUrl,
|
|
2751
3644
|
args: buildBackgroundRunnerArgs(options)
|
|
2752
3645
|
});
|
|
@@ -2773,6 +3666,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2773
3666
|
root: options.root,
|
|
2774
3667
|
sessionPolicy: normalizeSessionPolicy(options.session),
|
|
2775
3668
|
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
3669
|
+
...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
|
|
2776
3670
|
...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
|
|
2777
3671
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
2778
3672
|
dryRun: Boolean(options.dryRun),
|
|
@@ -2812,6 +3706,9 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2812
3706
|
}
|
|
2813
3707
|
});
|
|
2814
3708
|
var runner = program.command("runner").description("Manage local Amistio runner processes");
|
|
3709
|
+
runner.command("ui").alias("tui").description("Open an interactive terminal UI for local runner management").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Select or prefill a runner ID").option("--interval-seconds <seconds>", "Default polling interval for started background runners", parsePositiveInteger2, 10).option("--no-remote", "Skip remote API calls and show local runner state only").action(async (options) => {
|
|
3710
|
+
await runRunnerTui(options);
|
|
3711
|
+
});
|
|
2815
3712
|
runner.command("status").description("Show background runner status for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Limit status to one runner ID").action(async (options) => {
|
|
2816
3713
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2817
3714
|
if (!context) {
|
|
@@ -2892,6 +3789,7 @@ async function runNextWorkItem({
|
|
|
2892
3789
|
sessionPolicy,
|
|
2893
3790
|
stream,
|
|
2894
3791
|
explicitModel,
|
|
3792
|
+
explicitInvocationChannel,
|
|
2895
3793
|
explicitTool,
|
|
2896
3794
|
toolCommand,
|
|
2897
3795
|
commandContext,
|
|
@@ -2900,6 +3798,7 @@ async function runNextWorkItem({
|
|
|
2900
3798
|
const toolConfig = await resolveRunnerToolConfig({
|
|
2901
3799
|
apiClient,
|
|
2902
3800
|
projectId,
|
|
3801
|
+
...explicitInvocationChannel ? { explicitInvocationChannel } : {},
|
|
2903
3802
|
...explicitModel ? { explicitModel } : {},
|
|
2904
3803
|
...explicitTool ? { explicitTool } : {},
|
|
2905
3804
|
...toolCommand ? { toolCommand } : {}
|
|
@@ -2932,7 +3831,7 @@ async function runNextWorkItem({
|
|
|
2932
3831
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2933
3832
|
return { status: "preview", exitCode: 0 };
|
|
2934
3833
|
}
|
|
2935
|
-
const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
3834
|
+
const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
2936
3835
|
const sessionContext = await prepareToolSession({
|
|
2937
3836
|
apiClient,
|
|
2938
3837
|
projectId,
|
|
@@ -2953,6 +3852,7 @@ async function runNextWorkItem({
|
|
|
2953
3852
|
rootDir: root,
|
|
2954
3853
|
prompt,
|
|
2955
3854
|
tool: toolConfig.tool,
|
|
3855
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
2956
3856
|
...toolCommand ? { toolCommand } : {},
|
|
2957
3857
|
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
2958
3858
|
streamOutput: stream,
|
|
@@ -3012,7 +3912,7 @@ async function runNextWorkItem({
|
|
|
3012
3912
|
projectId,
|
|
3013
3913
|
result.workItem.workItemId,
|
|
3014
3914
|
finalStatus,
|
|
3015
|
-
`run_${result.workItem.workItemId}_${
|
|
3915
|
+
`run_${result.workItem.workItemId}_${randomUUID2()}`,
|
|
3016
3916
|
runnerId,
|
|
3017
3917
|
{
|
|
3018
3918
|
tool: preview.toolName,
|
|
@@ -3058,7 +3958,7 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
|
|
|
3058
3958
|
runnerId: context.runnerId,
|
|
3059
3959
|
repositoryLinkId: context.repositoryLinkId,
|
|
3060
3960
|
status,
|
|
3061
|
-
idempotencyKey: `runner_command_${command.commandId}_${status}_${
|
|
3961
|
+
idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
|
|
3062
3962
|
message,
|
|
3063
3963
|
...error ? { error } : {}
|
|
3064
3964
|
});
|
|
@@ -3088,37 +3988,6 @@ async function restartCurrentRunner(context) {
|
|
|
3088
3988
|
return { succeeded: false, message: "Background restart failed.", error: errorMessage2(error) };
|
|
3089
3989
|
}
|
|
3090
3990
|
}
|
|
3091
|
-
async function runOfficialCliUpdate() {
|
|
3092
|
-
const result = await runOfficialUpdateProcess("npm", ["install", "-g", "@amistio/cli"], 12e4);
|
|
3093
|
-
if (result.exitCode === 0) {
|
|
3094
|
-
return { succeeded: true, message: "Official Amistio CLI update command completed." };
|
|
3095
|
-
}
|
|
3096
|
-
return { succeeded: false, message: "Official Amistio CLI update command failed.", error: result.output || `npm exited with code ${result.exitCode}.` };
|
|
3097
|
-
}
|
|
3098
|
-
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
3099
|
-
return new Promise((resolve) => {
|
|
3100
|
-
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3101
|
-
let output = "";
|
|
3102
|
-
const timer = setTimeout(() => {
|
|
3103
|
-
output += "Timed out while running official CLI update.\n";
|
|
3104
|
-
child.kill("SIGTERM");
|
|
3105
|
-
}, timeoutMs);
|
|
3106
|
-
child.stdout?.on("data", (chunk) => {
|
|
3107
|
-
output += chunk.toString("utf8");
|
|
3108
|
-
});
|
|
3109
|
-
child.stderr?.on("data", (chunk) => {
|
|
3110
|
-
output += chunk.toString("utf8");
|
|
3111
|
-
});
|
|
3112
|
-
child.on("error", (error) => {
|
|
3113
|
-
clearTimeout(timer);
|
|
3114
|
-
resolve({ exitCode: 1, output: error.message });
|
|
3115
|
-
});
|
|
3116
|
-
child.on("close", (code) => {
|
|
3117
|
-
clearTimeout(timer);
|
|
3118
|
-
resolve({ exitCode: code ?? 1, output: truncateLogExcerpt(output) });
|
|
3119
|
-
});
|
|
3120
|
-
});
|
|
3121
|
-
}
|
|
3122
3991
|
function runnerCommandLabel(commandKind) {
|
|
3123
3992
|
if (commandKind === "update") return "update";
|
|
3124
3993
|
if (commandKind === "restart") return "restart";
|
|
@@ -3174,7 +4043,7 @@ ${toolResult.stderr}`);
|
|
|
3174
4043
|
const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
3175
4044
|
status: "completed",
|
|
3176
4045
|
runnerId,
|
|
3177
|
-
idempotencyKey: `generation_${workItem.workItemId}_${
|
|
4046
|
+
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
|
|
3178
4047
|
artifacts,
|
|
3179
4048
|
tool: toolName,
|
|
3180
4049
|
durationMs,
|
|
@@ -3188,7 +4057,7 @@ ${toolResult.stderr}`);
|
|
|
3188
4057
|
await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
3189
4058
|
status: "failed",
|
|
3190
4059
|
runnerId,
|
|
3191
|
-
idempotencyKey: `generation_${workItem.workItemId}_${
|
|
4060
|
+
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
|
|
3192
4061
|
tool: toolName,
|
|
3193
4062
|
durationMs,
|
|
3194
4063
|
...sessionTelemetry,
|
|
@@ -3304,7 +4173,7 @@ async function prepareToolSession({
|
|
|
3304
4173
|
});
|
|
3305
4174
|
return { ...selection, toolSession: toolSession2 };
|
|
3306
4175
|
}
|
|
3307
|
-
const toolSessionId = `tool_session_${
|
|
4176
|
+
const toolSessionId = `tool_session_${randomUUID2()}`;
|
|
3308
4177
|
const { toolSession } = await apiClient.createToolSession(projectId, {
|
|
3309
4178
|
toolSessionId,
|
|
3310
4179
|
repositoryLinkId,
|
|
@@ -3386,18 +4255,30 @@ function truncateLogExcerpt(value) {
|
|
|
3386
4255
|
const trimmed = value.trim();
|
|
3387
4256
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
3388
4257
|
}
|
|
3389
|
-
function
|
|
4258
|
+
function collectRepeatedOption(value, previous) {
|
|
4259
|
+
return [...previous, value];
|
|
4260
|
+
}
|
|
4261
|
+
function formatImportSkipSummary(counts) {
|
|
4262
|
+
return `Skipped: ${counts.excluded} excluded, ${counts.tooLarge} too large, ${counts.alreadyManaged} already managed, ${counts.unreadable} unreadable, ${counts.notMarkdown} non-Markdown.`;
|
|
4263
|
+
}
|
|
4264
|
+
function parsePositiveInteger2(value) {
|
|
3390
4265
|
const parsed = Number(value);
|
|
3391
4266
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3392
4267
|
throw new Error(`Expected a positive integer, received ${value}.`);
|
|
3393
4268
|
}
|
|
3394
4269
|
return parsed;
|
|
3395
4270
|
}
|
|
4271
|
+
function parseInvocationChannel(value) {
|
|
4272
|
+
if (value === "auto" || value === "sdk" || value === "command") {
|
|
4273
|
+
return value;
|
|
4274
|
+
}
|
|
4275
|
+
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
4276
|
+
}
|
|
3396
4277
|
function inferRepoName(root) {
|
|
3397
|
-
return
|
|
4278
|
+
return path12.basename(path12.resolve(root)) || "repository";
|
|
3398
4279
|
}
|
|
3399
4280
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
3400
|
-
return
|
|
4281
|
+
return createHash4("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
3401
4282
|
}
|
|
3402
4283
|
function defaultApiUrl() {
|
|
3403
4284
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -3412,7 +4293,7 @@ function formatApiUrlFlag(apiUrl) {
|
|
|
3412
4293
|
function formatShellArg(value) {
|
|
3413
4294
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
3414
4295
|
}
|
|
3415
|
-
async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool, projectId, toolCommand }) {
|
|
4296
|
+
async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
|
|
3416
4297
|
const capabilities = toRunnerToolCapabilities(await detectLocalTools());
|
|
3417
4298
|
if (toolCommand) {
|
|
3418
4299
|
return {
|
|
@@ -3422,6 +4303,8 @@ async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool,
|
|
|
3422
4303
|
source: "cli",
|
|
3423
4304
|
status: "custom",
|
|
3424
4305
|
effectiveTool: "custom",
|
|
4306
|
+
requestedInvocationChannel: explicitInvocationChannel ?? "command",
|
|
4307
|
+
effectiveInvocationChannel: "command",
|
|
3425
4308
|
...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
|
|
3426
4309
|
...explicitModel ? { model: explicitModel } : {},
|
|
3427
4310
|
message: "Using local custom tool command."
|
|
@@ -3429,43 +4312,51 @@ async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool,
|
|
|
3429
4312
|
}
|
|
3430
4313
|
if (explicitTool === "none") {
|
|
3431
4314
|
if (explicitModel) {
|
|
3432
|
-
return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", model: explicitModel });
|
|
4315
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", model: explicitModel });
|
|
3433
4316
|
}
|
|
3434
|
-
return { ready: true, tool: "none", capabilities, source: "cli", status: "none", message: "No local tool selected." };
|
|
4317
|
+
return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
|
|
3435
4318
|
}
|
|
3436
4319
|
if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
|
|
3437
|
-
return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, ...explicitModel ? { model: explicitModel } : {} });
|
|
4320
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...explicitModel ? { model: explicitModel } : {} });
|
|
3438
4321
|
}
|
|
3439
|
-
const remotePreference =
|
|
4322
|
+
const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
|
|
3440
4323
|
const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
|
|
4324
|
+
const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
|
|
3441
4325
|
const model = explicitModel ?? remotePreference?.model;
|
|
3442
|
-
const source = explicitTool || explicitModel ? "cli" : remotePreference?.source ?? "default";
|
|
3443
|
-
return resolveRequestedTool({ capabilities, requestedTool, source, ...model ? { model } : {} });
|
|
4326
|
+
const source = explicitTool || explicitInvocationChannel || explicitModel ? "cli" : remotePreference?.source ?? "default";
|
|
4327
|
+
return resolveRequestedTool({ capabilities, requestedInvocationChannel, requestedTool, source, ...model ? { model } : {} });
|
|
3444
4328
|
}
|
|
3445
|
-
function resolveRequestedTool({ capabilities, model, requestedTool, source }) {
|
|
4329
|
+
function resolveRequestedTool({ capabilities, model, requestedInvocationChannel, requestedTool, source }) {
|
|
3446
4330
|
if (requestedTool === "auto") {
|
|
3447
|
-
const candidate = capabilities.find((capability2) => capability2.available && (!model || capability2.supportsModelSelection));
|
|
4331
|
+
const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!model || capability2.supportsModelSelection));
|
|
3448
4332
|
if (!candidate) {
|
|
4333
|
+
const anyAvailable = capabilities.some((capability2) => capability2.available);
|
|
4334
|
+
const anyChannelAvailable = capabilities.some((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel));
|
|
4335
|
+
const status = !anyAvailable ? "unavailable" : requestedInvocationChannel !== "auto" && !anyChannelAvailable ? "channelUnsupported" : model ? "modelUnsupported" : "unavailable";
|
|
3449
4336
|
return unavailableToolConfig({
|
|
3450
4337
|
capabilities,
|
|
3451
4338
|
source,
|
|
3452
|
-
status
|
|
4339
|
+
status,
|
|
3453
4340
|
requestedTool,
|
|
4341
|
+
requestedInvocationChannel,
|
|
3454
4342
|
tool: "auto",
|
|
3455
4343
|
...model ? { model } : {},
|
|
3456
|
-
message: model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
|
|
4344
|
+
message: status === "channelUnsupported" ? `No installed local AI tool can honor ${requestedInvocationChannel} invocation.` : model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
|
|
3457
4345
|
});
|
|
3458
4346
|
}
|
|
3459
|
-
return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, effectiveTool: candidate.name, ...model ? { model } : {} };
|
|
4347
|
+
return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...model ? { model } : {} };
|
|
3460
4348
|
}
|
|
3461
4349
|
const capability = capabilities.find((candidate) => candidate.name === requestedTool);
|
|
3462
4350
|
if (!capability?.available) {
|
|
3463
|
-
return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
|
|
4351
|
+
return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, requestedInvocationChannel, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
|
|
4352
|
+
}
|
|
4353
|
+
if (!capabilitySupportsInvocationChannel(capability, requestedInvocationChannel)) {
|
|
4354
|
+
return unavailableToolConfig({ capabilities, source, status: "channelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is available but does not support ${requestedInvocationChannel} invocation on this runner.` });
|
|
3464
4355
|
}
|
|
3465
4356
|
if (model && !capability.supportsModelSelection) {
|
|
3466
|
-
return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, effectiveTool: requestedTool, tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
|
|
4357
|
+
return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
|
|
3467
4358
|
}
|
|
3468
|
-
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, effectiveTool: requestedTool, ...model ? { model } : {} };
|
|
4359
|
+
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...model ? { model } : {} };
|
|
3469
4360
|
}
|
|
3470
4361
|
function unavailableToolConfig(input) {
|
|
3471
4362
|
return {
|
|
@@ -3476,10 +4367,21 @@ function unavailableToolConfig(input) {
|
|
|
3476
4367
|
status: input.status,
|
|
3477
4368
|
message: input.message,
|
|
3478
4369
|
...input.requestedTool ? { requestedTool: input.requestedTool } : {},
|
|
4370
|
+
...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
|
|
3479
4371
|
...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
|
|
4372
|
+
...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
|
|
3480
4373
|
...input.model ? { model: input.model } : {}
|
|
3481
4374
|
};
|
|
3482
4375
|
}
|
|
4376
|
+
function capabilitySupportsInvocationChannel(capability, channel) {
|
|
4377
|
+
if (channel === "auto") return capability.available;
|
|
4378
|
+
if (channel === "sdk") return capability.sdkAvailable;
|
|
4379
|
+
return capability.commandAvailable;
|
|
4380
|
+
}
|
|
4381
|
+
function effectiveInvocationChannel(capability, channel) {
|
|
4382
|
+
if (channel === "sdk" || channel === "command") return channel;
|
|
4383
|
+
return capability.sdkAvailable ? "sdk" : "command";
|
|
4384
|
+
}
|
|
3483
4385
|
function toRunnerToolCapabilities(tools) {
|
|
3484
4386
|
return tools.map((tool) => ({
|
|
3485
4387
|
name: tool.name,
|
|
@@ -3493,46 +4395,16 @@ function toRunnerToolCapabilities(tools) {
|
|
|
3493
4395
|
supportsModelSelection: tool.supportsModelSelection
|
|
3494
4396
|
}));
|
|
3495
4397
|
}
|
|
3496
|
-
function buildBackgroundRunnerArgs(options) {
|
|
3497
|
-
const args = [
|
|
3498
|
-
"run",
|
|
3499
|
-
"--watch",
|
|
3500
|
-
"--api-url",
|
|
3501
|
-
options.apiUrl,
|
|
3502
|
-
"--runner-id",
|
|
3503
|
-
options.runnerId,
|
|
3504
|
-
"--root",
|
|
3505
|
-
path9.resolve(options.root),
|
|
3506
|
-
"--session",
|
|
3507
|
-
options.session,
|
|
3508
|
-
"--interval-seconds",
|
|
3509
|
-
String(options.intervalSeconds)
|
|
3510
|
-
];
|
|
3511
|
-
if (options.tool) {
|
|
3512
|
-
args.push("--tool", options.tool);
|
|
3513
|
-
}
|
|
3514
|
-
if (options.toolCommand) {
|
|
3515
|
-
args.push("--tool-command", options.toolCommand);
|
|
3516
|
-
}
|
|
3517
|
-
if (options.model) {
|
|
3518
|
-
args.push("--model", options.model);
|
|
3519
|
-
}
|
|
3520
|
-
if (options.maxIterations !== void 0) {
|
|
3521
|
-
args.push("--max-iterations", String(options.maxIterations));
|
|
3522
|
-
}
|
|
3523
|
-
if (!options.stream) {
|
|
3524
|
-
args.push("--no-stream");
|
|
3525
|
-
}
|
|
3526
|
-
return args;
|
|
3527
|
-
}
|
|
3528
4398
|
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
3529
4399
|
return {
|
|
3530
4400
|
version: CLI_VERSION,
|
|
3531
4401
|
mode,
|
|
3532
|
-
hostname:
|
|
4402
|
+
hostname: os6.hostname(),
|
|
3533
4403
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
3534
4404
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
4405
|
+
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
3535
4406
|
...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
|
|
4407
|
+
...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
|
|
3536
4408
|
...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
|
|
3537
4409
|
...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
|
|
3538
4410
|
...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
|