@amistio/cli 0.1.1 → 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/README.md +2 -2
- package/dist/index.js +2027 -113
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { writeFile as
|
|
6
|
-
import
|
|
4
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
5
|
+
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
6
|
+
import os6 from "node:os";
|
|
7
|
+
import path12 from "node:path";
|
|
7
8
|
import { Command } from "commander";
|
|
8
9
|
|
|
9
10
|
// ../shared/src/schemas.ts
|
|
@@ -24,6 +25,9 @@ var itemTypeSchema = z.enum([
|
|
|
24
25
|
"workItem",
|
|
25
26
|
"runnerHeartbeat",
|
|
26
27
|
"runnerExecutionLog",
|
|
28
|
+
"runnerCredential",
|
|
29
|
+
"runnerCommand",
|
|
30
|
+
"planReviewMessage",
|
|
27
31
|
"toolSession",
|
|
28
32
|
"activityEvent",
|
|
29
33
|
"pairingSession"
|
|
@@ -60,7 +64,7 @@ var workStatusSchema = z.enum([
|
|
|
60
64
|
]);
|
|
61
65
|
var sourceSchema = z.enum(["web", "repo", "generated", "runner"]);
|
|
62
66
|
var executionModeSchema = z.enum(["localRunner", "cloudSandbox"]);
|
|
63
|
-
var workKindSchema = z.enum(["brainGeneration", "implementation"]);
|
|
67
|
+
var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision"]);
|
|
64
68
|
var generatedDraftStatusSchema = z.enum(["queued", "generating", "reviewing", "approved", "rejected", "changesRequested", "failed"]);
|
|
65
69
|
var generatedBrainArtifactSchema = z.object({
|
|
66
70
|
documentType: documentTypeSchema,
|
|
@@ -76,8 +80,33 @@ var sessionPolicySchema = z.union([z.enum(["auto", "new", "none"]), z.string().r
|
|
|
76
80
|
var sessionDecisionSchema = z.enum(["created", "continued", "forcedNew", "forcedContinue", "notSupported", "skipped"]);
|
|
77
81
|
var toolSessionStatusSchema = z.enum(["open", "active", "closed", "archived", "blocked", "unavailable"]);
|
|
78
82
|
var sessionResumabilityScopeSchema = z.enum(["none", "localMachine", "repository", "account", "providerCloud"]);
|
|
83
|
+
var runnerToolNames = ["opencode", "claude", "codex", "copilot", "gemini", "aider", "cursor-agent"];
|
|
84
|
+
var runnerToolNameSchema = z.enum(runnerToolNames);
|
|
85
|
+
var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
|
|
86
|
+
var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
|
|
87
|
+
var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
|
|
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"]);
|
|
91
|
+
var runnerToolModelPreferenceSchema = z.object({
|
|
92
|
+
tool: runnerToolSelectionSchema.optional(),
|
|
93
|
+
invocationChannel: runnerInvocationChannelSchema.optional(),
|
|
94
|
+
model: z.string().trim().min(1).max(160).optional()
|
|
95
|
+
});
|
|
96
|
+
var runnerToolCapabilitySchema = z.object({
|
|
97
|
+
name: runnerToolNameSchema,
|
|
98
|
+
description: z.string().min(1),
|
|
99
|
+
available: z.boolean(),
|
|
100
|
+
sdkAvailable: z.boolean(),
|
|
101
|
+
commandAvailable: z.boolean(),
|
|
102
|
+
execution: z.enum(["sdk", "command", "unavailable"]),
|
|
103
|
+
supportsSessionReuse: z.boolean(),
|
|
104
|
+
resumabilityScope: sessionResumabilityScopeSchema,
|
|
105
|
+
supportsModelSelection: z.boolean()
|
|
106
|
+
});
|
|
79
107
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
80
108
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
109
|
+
var projectStatusSchema = z.enum(["active", "archived"]);
|
|
81
110
|
var baseItemSchema = z.object({
|
|
82
111
|
id: z.string().min(1),
|
|
83
112
|
type: itemTypeSchema,
|
|
@@ -104,7 +133,11 @@ var projectItemSchema = baseItemSchema.extend({
|
|
|
104
133
|
projectId: z.string().min(1),
|
|
105
134
|
name: z.string().min(1),
|
|
106
135
|
slug: z.string().min(1),
|
|
107
|
-
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()
|
|
108
141
|
});
|
|
109
142
|
var repositoryLinkItemSchema = baseItemSchema.extend({
|
|
110
143
|
type: z.literal("repositoryLink"),
|
|
@@ -182,6 +215,10 @@ var workItemSchema = baseItemSchema.extend({
|
|
|
182
215
|
approvedBy: z.string().min(1).optional(),
|
|
183
216
|
sourceWish: z.string().min(1).optional(),
|
|
184
217
|
generatedDraftId: z.string().min(1).optional(),
|
|
218
|
+
reviewThreadId: z.string().min(1).optional(),
|
|
219
|
+
reviewDocumentId: z.string().min(1).optional(),
|
|
220
|
+
reviewDocumentRevision: z.number().int().nonnegative().optional(),
|
|
221
|
+
reviewMessageId: z.string().min(1).optional(),
|
|
185
222
|
claimedByRunnerId: z.string().optional(),
|
|
186
223
|
leaseExpiresAt: isoDateTimeSchema.optional(),
|
|
187
224
|
attempt: z.number().int().nonnegative().default(0),
|
|
@@ -199,10 +236,29 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
199
236
|
projectId: z.string().min(1),
|
|
200
237
|
runnerId: z.string().min(1),
|
|
201
238
|
repositoryLinkId: z.string().min(1),
|
|
202
|
-
status: z.enum(["online", "offline", "running", "blocked"]),
|
|
239
|
+
status: z.enum(["online", "offline", "running", "blocked", "removed"]),
|
|
203
240
|
version: z.string().optional(),
|
|
241
|
+
mode: z.enum(["foreground", "background"]).optional(),
|
|
242
|
+
hostname: z.string().min(1).optional(),
|
|
243
|
+
runnerName: z.string().min(1).optional(),
|
|
244
|
+
capabilities: z.array(runnerToolCapabilitySchema).optional(),
|
|
245
|
+
requestedTool: runnerToolSelectionSchema.optional(),
|
|
246
|
+
requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
|
|
247
|
+
effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
|
|
248
|
+
effectiveInvocationChannel: runnerEffectiveInvocationChannelSchema.optional(),
|
|
249
|
+
effectiveModel: z.string().min(1).optional(),
|
|
250
|
+
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
251
|
+
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
252
|
+
preferenceMessage: z.string().optional(),
|
|
204
253
|
lastSeenAt: isoDateTimeSchema
|
|
205
254
|
});
|
|
255
|
+
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
256
|
+
type: z.literal("accountSettings"),
|
|
257
|
+
projectId: z.string().min(1),
|
|
258
|
+
settingsType: z.literal("runnerPreferences"),
|
|
259
|
+
scope: runnerPreferenceScopeSchema,
|
|
260
|
+
preferences: runnerToolModelPreferenceSchema
|
|
261
|
+
});
|
|
206
262
|
var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
207
263
|
type: z.literal("runnerExecutionLog"),
|
|
208
264
|
projectId: z.string().min(1),
|
|
@@ -232,6 +288,57 @@ var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
|
232
288
|
message: z.string().optional(),
|
|
233
289
|
error: z.string().optional()
|
|
234
290
|
});
|
|
291
|
+
var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
292
|
+
type: z.literal("runnerCredential"),
|
|
293
|
+
projectId: z.string().min(1),
|
|
294
|
+
runnerCredentialId: z.string().min(1),
|
|
295
|
+
repositoryLinkId: z.string().min(1),
|
|
296
|
+
tokenHash: z.string().min(32),
|
|
297
|
+
issuedAt: isoDateTimeSchema,
|
|
298
|
+
lastUsedAt: isoDateTimeSchema.optional(),
|
|
299
|
+
status: z.enum(["active", "revoked"]).default("active")
|
|
300
|
+
});
|
|
301
|
+
var runnerCommandKindSchema = z.enum(["update", "restart", "remove"]);
|
|
302
|
+
var runnerCommandStatusSchema = z.enum(["pending", "acknowledged", "running", "completed", "failed", "expired", "cancelled"]);
|
|
303
|
+
var runnerCommandItemSchema = baseItemSchema.extend({
|
|
304
|
+
type: z.literal("runnerCommand"),
|
|
305
|
+
projectId: z.string().min(1),
|
|
306
|
+
commandId: z.string().min(1),
|
|
307
|
+
commandKind: runnerCommandKindSchema,
|
|
308
|
+
status: runnerCommandStatusSchema,
|
|
309
|
+
runnerId: z.string().min(1),
|
|
310
|
+
repositoryLinkId: z.string().min(1),
|
|
311
|
+
requestedByUserId: z.string().min(1),
|
|
312
|
+
idempotencyKey: z.string().min(1),
|
|
313
|
+
lastStatusIdempotencyKey: z.string().min(1).optional(),
|
|
314
|
+
expiresAt: isoDateTimeSchema,
|
|
315
|
+
acknowledgedAt: isoDateTimeSchema.optional(),
|
|
316
|
+
startedAt: isoDateTimeSchema.optional(),
|
|
317
|
+
completedAt: isoDateTimeSchema.optional(),
|
|
318
|
+
cancelledAt: isoDateTimeSchema.optional(),
|
|
319
|
+
message: z.string().optional(),
|
|
320
|
+
error: z.string().optional()
|
|
321
|
+
});
|
|
322
|
+
var planReviewMessageRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
323
|
+
var planReviewMessageIntentSchema = z.enum(["ask", "revisionRequest", "revisionResult"]);
|
|
324
|
+
var planReviewMessageStatusSchema = z.enum(["posted", "queued", "running", "completed", "failed"]);
|
|
325
|
+
var planReviewMessageItemSchema = baseItemSchema.extend({
|
|
326
|
+
type: z.literal("planReviewMessage"),
|
|
327
|
+
projectId: z.string().min(1),
|
|
328
|
+
messageId: z.string().min(1),
|
|
329
|
+
threadId: z.string().min(1),
|
|
330
|
+
generatedDraftId: z.string().min(1),
|
|
331
|
+
documentId: z.string().min(1),
|
|
332
|
+
documentRevision: z.number().int().nonnegative(),
|
|
333
|
+
role: planReviewMessageRoleSchema,
|
|
334
|
+
intent: planReviewMessageIntentSchema,
|
|
335
|
+
status: planReviewMessageStatusSchema,
|
|
336
|
+
content: z.string().min(1),
|
|
337
|
+
workItemId: z.string().min(1).optional(),
|
|
338
|
+
responseToMessageId: z.string().min(1).optional(),
|
|
339
|
+
createdByUserId: z.string().min(1).optional(),
|
|
340
|
+
runnerId: z.string().min(1).optional()
|
|
341
|
+
});
|
|
235
342
|
var toolSessionItemSchema = baseItemSchema.extend({
|
|
236
343
|
type: z.literal("toolSession"),
|
|
237
344
|
projectId: z.string().min(1),
|
|
@@ -275,7 +382,11 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
|
275
382
|
syncConflictItemSchema,
|
|
276
383
|
workItemSchema,
|
|
277
384
|
runnerHeartbeatItemSchema,
|
|
385
|
+
runnerSettingsItemSchema,
|
|
278
386
|
runnerExecutionLogItemSchema,
|
|
387
|
+
runnerCredentialItemSchema,
|
|
388
|
+
runnerCommandItemSchema,
|
|
389
|
+
planReviewMessageItemSchema,
|
|
279
390
|
toolSessionItemSchema
|
|
280
391
|
]);
|
|
281
392
|
|
|
@@ -461,6 +572,211 @@ function decideSyncState(state) {
|
|
|
461
572
|
return "clean";
|
|
462
573
|
}
|
|
463
574
|
|
|
575
|
+
// ../shared/src/next-action.ts
|
|
576
|
+
var nextActionRunnerHeartbeatFreshMs = 15 * 60 * 1e3;
|
|
577
|
+
function computeProjectNextAction(input) {
|
|
578
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
579
|
+
if (!input.projectId) {
|
|
580
|
+
return {
|
|
581
|
+
kind: "noProject",
|
|
582
|
+
actor: "user",
|
|
583
|
+
tone: "neutral",
|
|
584
|
+
title: "Create a project",
|
|
585
|
+
message: "Create a project before linking a repository or starting runner work."
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const activeRepositoryLinks = input.repositoryLinks.filter((link) => link.status !== "revoked");
|
|
589
|
+
const repositoryLink = activeRepositoryLinks[0];
|
|
590
|
+
if (!repositoryLink) {
|
|
591
|
+
return {
|
|
592
|
+
kind: "linkRepository",
|
|
593
|
+
actor: "user",
|
|
594
|
+
tone: "warning",
|
|
595
|
+
title: "Link a repository",
|
|
596
|
+
message: "Add the project repository so Amistio can pair a local runner."
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const pairedRepositoryLinks = activeRepositoryLinks.filter((link) => Boolean(link.repoFingerprint));
|
|
600
|
+
if (!pairedRepositoryLinks.length) {
|
|
601
|
+
return {
|
|
602
|
+
kind: "pairRepository",
|
|
603
|
+
actor: "user",
|
|
604
|
+
tone: "warning",
|
|
605
|
+
title: "Pair the repository",
|
|
606
|
+
message: `Run the pairing command from ${repositoryLink.repoName} before queueing runner work.`,
|
|
607
|
+
repositoryLinkId: repositoryLink.repositoryLinkId,
|
|
608
|
+
updatedAt: repositoryLink.updatedAt
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const runningWork = latestWorkItem(input.workItems.filter((item) => item.status === "running"));
|
|
612
|
+
if (runningWork) {
|
|
613
|
+
if (runningWork.workKind === "brainGeneration") {
|
|
614
|
+
return workAction(runningWork, "brainGenerationRunning", "runner", "warning", "Generating project brain", "The local runner is generating draft brain artifacts for review.");
|
|
615
|
+
}
|
|
616
|
+
if (runningWork.workKind === "planRevision") {
|
|
617
|
+
return workAction(runningWork, "planRevisionRunning", "runner", "warning", "Revising the plan", "The local runner is revising the generated plan from the conversation.");
|
|
618
|
+
}
|
|
619
|
+
return workAction(runningWork, "runnerRunningWork", "runner", "warning", "Runner is working", "The local runner has claimed approved implementation work.");
|
|
620
|
+
}
|
|
621
|
+
const generatedReview = generatedReviewState(input.documents);
|
|
622
|
+
if (generatedReview.unapproved.length > 0) {
|
|
623
|
+
const partial = generatedReview.approved.length > 0;
|
|
624
|
+
const title = partial ? "Finish generated review" : "Review generated artifacts";
|
|
625
|
+
const message = partial ? `${generatedReview.unapproved.length} generated artifact${generatedReview.unapproved.length === 1 ? "" : "s"} still need approval before implementation work can queue.` : `${generatedReview.unapproved.length} generated artifact${generatedReview.unapproved.length === 1 ? "" : "s"} need approval in the web app before the runner can implement them.`;
|
|
626
|
+
return {
|
|
627
|
+
kind: partial ? "generatedArtifactsPartiallyApproved" : "generatedArtifactsReview",
|
|
628
|
+
actor: "user",
|
|
629
|
+
tone: "warning",
|
|
630
|
+
title,
|
|
631
|
+
message,
|
|
632
|
+
count: generatedReview.unapproved.length,
|
|
633
|
+
...generatedReview.unapproved[0]?.documentId ? { documentId: generatedReview.unapproved[0].documentId } : {},
|
|
634
|
+
...latestTimestamp(generatedReview.unapproved.map((document) => document.updatedAt)) ? { updatedAt: latestTimestamp(generatedReview.unapproved.map((document) => document.updatedAt)) } : {}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
const failedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "failed"));
|
|
638
|
+
if (failedBrainGeneration) {
|
|
639
|
+
return workAction(failedBrainGeneration, "brainGenerationFailed", "user", "danger", "Brain generation failed", failedBrainGeneration.lastStatusMessage ?? "Retry generation after checking the runner output.");
|
|
640
|
+
}
|
|
641
|
+
const failedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "failed"));
|
|
642
|
+
if (failedPlanRevision) {
|
|
643
|
+
return workAction(failedPlanRevision, "planRevisionFailed", "user", "danger", "Plan revision failed", failedPlanRevision.lastStatusMessage ?? "Review the conversation and request another revision if needed.");
|
|
644
|
+
}
|
|
645
|
+
const blockedWork = latestWorkItem(input.workItems.filter((item) => item.status === "blocked" || item.status === "changesRequested"));
|
|
646
|
+
if (blockedWork) {
|
|
647
|
+
return workAction(blockedWork, "workBlocked", "user", "danger", "Work is blocked", blockedWork.lastStatusMessage ?? "Review the blocked work item before the runner can continue.");
|
|
648
|
+
}
|
|
649
|
+
const failedWork = latestWorkItem(input.workItems.filter((item) => item.status === "failed"));
|
|
650
|
+
if (failedWork) {
|
|
651
|
+
return workAction(failedWork, "workFailed", "user", "danger", "Work failed", failedWork.lastStatusMessage ?? "Review the failure and retry or update the plan.");
|
|
652
|
+
}
|
|
653
|
+
const queuedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "approved"));
|
|
654
|
+
const queuedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "approved"));
|
|
655
|
+
const queuedImplementation = latestWorkItem(input.workItems.filter((item) => item.workKind !== "brainGeneration" && item.workKind !== "planRevision" && item.status === "approved"));
|
|
656
|
+
const queuedWork = queuedBrainGeneration ?? queuedPlanRevision ?? queuedImplementation;
|
|
657
|
+
const readiness = getSharedRunnerReadiness(pairedRepositoryLinks, input.runnerHeartbeats, nowMs);
|
|
658
|
+
if (queuedWork && !readiness.ready) {
|
|
659
|
+
const title = queuedWork.workKind === "brainGeneration" ? "Brain generation is queued" : queuedWork.workKind === "planRevision" ? "Plan revision is queued" : "Implementation is queued";
|
|
660
|
+
return runnerWaitAction(readiness, title);
|
|
661
|
+
}
|
|
662
|
+
if (queuedBrainGeneration) {
|
|
663
|
+
return workAction(queuedBrainGeneration, "brainGenerationQueued", "runner", "warning", "Brain generation queued", "The local runner can claim this queued brain-generation work.");
|
|
664
|
+
}
|
|
665
|
+
if (queuedPlanRevision) {
|
|
666
|
+
return workAction(queuedPlanRevision, "planRevisionQueued", "runner", "warning", "Plan revision queued", "The local runner can claim the requested plan revision.");
|
|
667
|
+
}
|
|
668
|
+
if (queuedImplementation) {
|
|
669
|
+
return workAction(queuedImplementation, "implementationQueued", "runner", "warning", "Implementation queued", "The local runner can claim approved implementation work.");
|
|
670
|
+
}
|
|
671
|
+
if (!readiness.ready) {
|
|
672
|
+
return runnerWaitAction(readiness, "Runner setup needed");
|
|
673
|
+
}
|
|
674
|
+
const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed"));
|
|
675
|
+
if (completedWork) {
|
|
676
|
+
return workAction(completedWork, "workCompleted", "none", "success", "Work completed", completedWork.lastStatusMessage ?? "The latest runner work is complete.");
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
kind: "idle",
|
|
680
|
+
actor: "user",
|
|
681
|
+
tone: "success",
|
|
682
|
+
title: "Ready for the next task",
|
|
683
|
+
message: "The repository and runner are ready for a new project-brain request.",
|
|
684
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
685
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
686
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function formatProjectNextAction(action) {
|
|
690
|
+
return `${action.title}: ${action.message}`;
|
|
691
|
+
}
|
|
692
|
+
function runnerWaitAction(readiness, fallbackTitle) {
|
|
693
|
+
const repositoryName = readiness.repositoryLink?.repoName ?? "the paired repository";
|
|
694
|
+
if (readiness.reason === "runnerOffline") {
|
|
695
|
+
return {
|
|
696
|
+
kind: "runnerOffline",
|
|
697
|
+
actor: "user",
|
|
698
|
+
tone: "danger",
|
|
699
|
+
title: "Restart the runner",
|
|
700
|
+
message: `The runner for ${repositoryName} is offline. Start amistio run --watch from the paired checkout.`,
|
|
701
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
702
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
703
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (readiness.reason === "runnerStale") {
|
|
707
|
+
return {
|
|
708
|
+
kind: "runnerStale",
|
|
709
|
+
actor: "user",
|
|
710
|
+
tone: "warning",
|
|
711
|
+
title: "Refresh the runner",
|
|
712
|
+
message: `The runner for ${repositoryName} has not checked in recently. Restart amistio run --watch from the paired checkout.`,
|
|
713
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
714
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
715
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
kind: "runnerMissing",
|
|
720
|
+
actor: "user",
|
|
721
|
+
tone: "warning",
|
|
722
|
+
title: fallbackTitle,
|
|
723
|
+
message: `Start amistio run --watch from ${repositoryName} so the local runner can claim work.`,
|
|
724
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function getSharedRunnerReadiness(repositoryLinks, runnerHeartbeats, nowMs) {
|
|
728
|
+
const repositoryLinkIds = new Set(repositoryLinks.map((link) => link.repositoryLinkId));
|
|
729
|
+
const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId) && heartbeat.status !== "removed").sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
|
|
730
|
+
const repositoryLink = latestHeartbeat ? repositoryLinks.find((link) => link.repositoryLinkId === latestHeartbeat.repositoryLinkId) ?? repositoryLinks[0] : repositoryLinks[0];
|
|
731
|
+
if (!latestHeartbeat) {
|
|
732
|
+
return { ready: false, reason: "runnerMissing", ...repositoryLink ? { repositoryLink } : {} };
|
|
733
|
+
}
|
|
734
|
+
if (latestHeartbeat.status === "offline") {
|
|
735
|
+
return { ready: false, reason: "runnerOffline", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
736
|
+
}
|
|
737
|
+
if (!isFreshHeartbeat(latestHeartbeat, nowMs)) {
|
|
738
|
+
return { ready: false, reason: "runnerStale", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
739
|
+
}
|
|
740
|
+
return { ready: true, reason: "ready", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
741
|
+
}
|
|
742
|
+
function generatedReviewState(documents) {
|
|
743
|
+
const generated = documents.filter(isGeneratedDocument);
|
|
744
|
+
return {
|
|
745
|
+
approved: generated.filter((document) => document.status === "approved"),
|
|
746
|
+
unapproved: generated.filter((document) => document.status !== "approved" && document.status !== "rejected" && document.syncState !== "archived")
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function isGeneratedDocument(document) {
|
|
750
|
+
const generatedDraftId = document.frontmatter.generatedDraftId;
|
|
751
|
+
return typeof generatedDraftId === "string" && generatedDraftId.length > 0;
|
|
752
|
+
}
|
|
753
|
+
function workAction(workItem, kind, actor, tone, title, message) {
|
|
754
|
+
return {
|
|
755
|
+
kind,
|
|
756
|
+
actor,
|
|
757
|
+
tone,
|
|
758
|
+
title,
|
|
759
|
+
message,
|
|
760
|
+
workItemId: workItem.workItemId,
|
|
761
|
+
...workItem.claimedByRunnerId ? { runnerId: workItem.claimedByRunnerId } : {},
|
|
762
|
+
updatedAt: workItem.lastStatusAt
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function latestWorkItem(workItems) {
|
|
766
|
+
return [...workItems].sort((first, second) => Date.parse(second.updatedAt) - Date.parse(first.updatedAt))[0];
|
|
767
|
+
}
|
|
768
|
+
function latestTimestamp(values) {
|
|
769
|
+
return values.sort((first, second) => Date.parse(second) - Date.parse(first))[0];
|
|
770
|
+
}
|
|
771
|
+
function heartbeatTime(heartbeat) {
|
|
772
|
+
const timestamp = Date.parse(heartbeat.lastSeenAt);
|
|
773
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
774
|
+
}
|
|
775
|
+
function isFreshHeartbeat(heartbeat, nowMs) {
|
|
776
|
+
const lastSeenMs = heartbeatTime(heartbeat);
|
|
777
|
+
return lastSeenMs > 0 && nowMs - lastSeenMs <= nextActionRunnerHeartbeatFreshMs;
|
|
778
|
+
}
|
|
779
|
+
|
|
464
780
|
// src/bootstrap.ts
|
|
465
781
|
import { execFile } from "node:child_process";
|
|
466
782
|
import { mkdir, readdir, stat } from "node:fs/promises";
|
|
@@ -544,6 +860,16 @@ var LocalCredentialStore = class {
|
|
|
544
860
|
const data = await this.read();
|
|
545
861
|
return data.credentials[key];
|
|
546
862
|
}
|
|
863
|
+
async delete(key) {
|
|
864
|
+
const data = await this.read();
|
|
865
|
+
if (!(key in data.credentials)) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
delete data.credentials[key];
|
|
869
|
+
await mkdir2(path2.dirname(this.filePath), { recursive: true });
|
|
870
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
|
|
871
|
+
await chmod(this.filePath, 384);
|
|
872
|
+
}
|
|
547
873
|
async read() {
|
|
548
874
|
try {
|
|
549
875
|
return JSON.parse(await readFile(this.filePath, "utf8"));
|
|
@@ -675,6 +1001,22 @@ var ApiClient = class {
|
|
|
675
1001
|
}
|
|
676
1002
|
);
|
|
677
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
|
+
}
|
|
678
1020
|
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300) {
|
|
679
1021
|
return this.request(
|
|
680
1022
|
`/projects/${projectId}/work-items/claim`,
|
|
@@ -699,6 +1041,14 @@ var ApiClient = class {
|
|
|
699
1041
|
{ method: "GET" }
|
|
700
1042
|
);
|
|
701
1043
|
}
|
|
1044
|
+
async listPlanReviewMessages(projectId, documentId) {
|
|
1045
|
+
const suffix = documentId ? `?documentId=${encodeURIComponent(documentId)}` : "";
|
|
1046
|
+
return this.request(
|
|
1047
|
+
`/projects/${projectId}/plan-review-messages${suffix}`,
|
|
1048
|
+
z3.object({ messages: z3.array(planReviewMessageItemSchema) }),
|
|
1049
|
+
{ method: "GET" }
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
702
1052
|
async pushBrainDocuments(projectId, documents) {
|
|
703
1053
|
return this.request(
|
|
704
1054
|
`/projects/${projectId}/brain-documents`,
|
|
@@ -709,13 +1059,59 @@ var ApiClient = class {
|
|
|
709
1059
|
}
|
|
710
1060
|
);
|
|
711
1061
|
}
|
|
712
|
-
async
|
|
1062
|
+
async listRunners(projectId) {
|
|
1063
|
+
return this.request(
|
|
1064
|
+
`/projects/${projectId}/runners`,
|
|
1065
|
+
z3.object({ runners: z3.array(runnerHeartbeatItemSchema) }),
|
|
1066
|
+
{ method: "GET" }
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
async listRunnerCommands(projectId, runnerId, repositoryLinkId) {
|
|
1070
|
+
return this.request(
|
|
1071
|
+
`/projects/${projectId}/runner-commands?runnerId=${encodeURIComponent(runnerId)}&repositoryLinkId=${encodeURIComponent(repositoryLinkId)}`,
|
|
1072
|
+
z3.object({ commands: z3.array(runnerCommandItemSchema) }),
|
|
1073
|
+
{ method: "GET" }
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
async updateRunnerCommand(projectId, commandId, input) {
|
|
1077
|
+
return this.request(
|
|
1078
|
+
`/projects/${projectId}/runner-commands/${commandId}`,
|
|
1079
|
+
z3.object({ command: runnerCommandItemSchema }),
|
|
1080
|
+
{
|
|
1081
|
+
method: "PATCH",
|
|
1082
|
+
body: JSON.stringify(input)
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
async getRunnerPreferences(projectId) {
|
|
1087
|
+
const response = await this.request(
|
|
1088
|
+
`/projects/${projectId}/runner-preferences`,
|
|
1089
|
+
z3.object({
|
|
1090
|
+
account: runnerSettingsItemSchema.optional(),
|
|
1091
|
+
project: runnerSettingsItemSchema.optional(),
|
|
1092
|
+
effective: z3.object({
|
|
1093
|
+
tool: runnerToolSelectionSchema,
|
|
1094
|
+
invocationChannel: runnerInvocationChannelSchema,
|
|
1095
|
+
model: z3.string().optional(),
|
|
1096
|
+
source: runnerPreferenceSourceSchema
|
|
1097
|
+
})
|
|
1098
|
+
}),
|
|
1099
|
+
{ method: "GET" }
|
|
1100
|
+
);
|
|
1101
|
+
return {
|
|
1102
|
+
...response.account ? { account: response.account } : {},
|
|
1103
|
+
...response.project ? { project: response.project } : {},
|
|
1104
|
+
effective: response.effective
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, status, metadata) {
|
|
1108
|
+
const heartbeatMetadata = typeof metadata === "string" ? { version: metadata } : metadata ?? {};
|
|
713
1109
|
return this.request(
|
|
714
1110
|
`/projects/${projectId}/runners`,
|
|
715
1111
|
z3.object({ runner: runnerHeartbeatItemSchema }),
|
|
716
1112
|
{
|
|
717
1113
|
method: "POST",
|
|
718
|
-
body: JSON.stringify({ runnerId, repositoryLinkId, status, ...
|
|
1114
|
+
body: JSON.stringify({ runnerId, repositoryLinkId, status, ...heartbeatMetadata, lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
719
1115
|
}
|
|
720
1116
|
);
|
|
721
1117
|
}
|
|
@@ -771,7 +1167,7 @@ var ApiClient = class {
|
|
|
771
1167
|
...init,
|
|
772
1168
|
headers: {
|
|
773
1169
|
"content-type": "application/json",
|
|
774
|
-
"x-amistio-account-id": this.options.accountId,
|
|
1170
|
+
...this.options.accountId ? { "x-amistio-account-id": this.options.accountId } : {},
|
|
775
1171
|
...this.options.token ? { authorization: `Bearer ${this.options.token}` } : {}
|
|
776
1172
|
}
|
|
777
1173
|
});
|
|
@@ -808,8 +1204,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
808
1204
|
});
|
|
809
1205
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
810
1206
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
811
|
-
const
|
|
812
|
-
return new URL(`${base}${
|
|
1207
|
+
const path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1208
|
+
return new URL(`${base}${path13}`);
|
|
813
1209
|
}
|
|
814
1210
|
|
|
815
1211
|
// src/orchestrator.ts
|
|
@@ -878,7 +1274,7 @@ import { spawn } from "node:child_process";
|
|
|
878
1274
|
import { mkdtemp, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
879
1275
|
import os2 from "node:os";
|
|
880
1276
|
import path5 from "node:path";
|
|
881
|
-
var localToolNames =
|
|
1277
|
+
var localToolNames = runnerToolNames;
|
|
882
1278
|
var localToolAdapters = [
|
|
883
1279
|
{
|
|
884
1280
|
name: "opencode",
|
|
@@ -931,6 +1327,7 @@ var localToolAdapters = [
|
|
|
931
1327
|
description: "GitHub Copilot SDK adapter using @github/copilot-sdk.",
|
|
932
1328
|
sdkPackageName: "@github/copilot-sdk",
|
|
933
1329
|
sdkDisplayCommand: "@github/copilot-sdk CopilotClient.createSession().sendAndWait()",
|
|
1330
|
+
supportsModelSelection: true,
|
|
934
1331
|
supportsSessionReuse: false,
|
|
935
1332
|
resumabilityScope: "none",
|
|
936
1333
|
runWithSdk: runCopilotSdk
|
|
@@ -988,7 +1385,8 @@ async function detectLocalTools() {
|
|
|
988
1385
|
commandAvailable,
|
|
989
1386
|
execution: sdkAvailable ? "sdk" : commandAvailable ? "command" : "unavailable",
|
|
990
1387
|
supportsSessionReuse: Boolean(adapter.supportsSessionReuse),
|
|
991
|
-
resumabilityScope: adapter.resumabilityScope ?? "none"
|
|
1388
|
+
resumabilityScope: adapter.resumabilityScope ?? "none",
|
|
1389
|
+
supportsModelSelection: Boolean(adapter.supportsModelSelection)
|
|
992
1390
|
};
|
|
993
1391
|
})
|
|
994
1392
|
);
|
|
@@ -1002,13 +1400,15 @@ async function runLocalTool(options) {
|
|
|
1002
1400
|
rootDir: options.rootDir,
|
|
1003
1401
|
prompt: options.prompt,
|
|
1004
1402
|
promptFilePath,
|
|
1005
|
-
tool: options.tool ?? "auto"
|
|
1403
|
+
tool: options.tool ?? "auto",
|
|
1404
|
+
invocationChannel: options.invocationChannel ?? "auto",
|
|
1405
|
+
...options.model ? { model: options.model } : {}
|
|
1006
1406
|
};
|
|
1007
1407
|
if (options.toolCommand) {
|
|
1008
1408
|
runnerOptions.toolCommand = options.toolCommand;
|
|
1009
1409
|
}
|
|
1010
|
-
const
|
|
1011
|
-
const result = await executeToolRunner(
|
|
1410
|
+
const runner2 = await createToolRunner(runnerOptions);
|
|
1411
|
+
const result = await executeToolRunner(runner2, {
|
|
1012
1412
|
rootDir: options.rootDir,
|
|
1013
1413
|
prompt: options.prompt,
|
|
1014
1414
|
promptFilePath,
|
|
@@ -1016,10 +1416,11 @@ async function runLocalTool(options) {
|
|
|
1016
1416
|
...options.session ? { session: options.session } : {}
|
|
1017
1417
|
});
|
|
1018
1418
|
return {
|
|
1019
|
-
toolName:
|
|
1020
|
-
displayCommand:
|
|
1021
|
-
supportsSessionReuse:
|
|
1022
|
-
resumabilityScope:
|
|
1419
|
+
toolName: runner2.toolName,
|
|
1420
|
+
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1421
|
+
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1422
|
+
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1423
|
+
...options.model ? { model: options.model } : {},
|
|
1023
1424
|
...result
|
|
1024
1425
|
};
|
|
1025
1426
|
} finally {
|
|
@@ -1032,17 +1433,20 @@ async function createToolRunPreview(options) {
|
|
|
1032
1433
|
rootDir: options.rootDir,
|
|
1033
1434
|
prompt: options.prompt,
|
|
1034
1435
|
promptFilePath,
|
|
1035
|
-
tool: options.tool ?? "auto"
|
|
1436
|
+
tool: options.tool ?? "auto",
|
|
1437
|
+
invocationChannel: options.invocationChannel ?? "auto",
|
|
1438
|
+
...options.model ? { model: options.model } : {}
|
|
1036
1439
|
};
|
|
1037
1440
|
if (options.toolCommand) {
|
|
1038
1441
|
runnerOptions.toolCommand = options.toolCommand;
|
|
1039
1442
|
}
|
|
1040
|
-
const
|
|
1443
|
+
const runner2 = await createToolRunner(runnerOptions);
|
|
1041
1444
|
return {
|
|
1042
|
-
toolName:
|
|
1043
|
-
displayCommand:
|
|
1044
|
-
supportsSessionReuse:
|
|
1045
|
-
resumabilityScope:
|
|
1445
|
+
toolName: runner2.toolName,
|
|
1446
|
+
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1447
|
+
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1448
|
+
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1449
|
+
...options.model ? { model: options.model } : {}
|
|
1046
1450
|
};
|
|
1047
1451
|
}
|
|
1048
1452
|
function createCustomToolInvocation(commandTemplate, input) {
|
|
@@ -1067,35 +1471,45 @@ async function createToolRunner(options) {
|
|
|
1067
1471
|
if (tool === "none") {
|
|
1068
1472
|
throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
|
|
1069
1473
|
}
|
|
1070
|
-
const adapter = tool === "auto" ? await selectFirstAvailableAdapter() : await selectRequestedAdapter(tool);
|
|
1071
|
-
if (
|
|
1474
|
+
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model), options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel);
|
|
1475
|
+
if (options.model && !adapter.supportsModelSelection) {
|
|
1476
|
+
throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
|
|
1477
|
+
}
|
|
1478
|
+
if (options.invocationChannel !== "command" && adapter.runWithSdk && await isSdkAvailable(adapter)) {
|
|
1072
1479
|
return {
|
|
1073
1480
|
toolName: adapter.name,
|
|
1074
1481
|
kind: "sdk",
|
|
1075
1482
|
displayCommand: adapter.sdkDisplayCommand ?? `${adapter.name} SDK`,
|
|
1076
|
-
adapter
|
|
1483
|
+
adapter,
|
|
1484
|
+
allowCommandFallback: options.invocationChannel === "auto"
|
|
1077
1485
|
};
|
|
1078
1486
|
}
|
|
1079
|
-
if (adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
|
|
1487
|
+
if (options.invocationChannel !== "sdk" && adapter.buildInvocation && adapter.executable && await commandExists(adapter.executable)) {
|
|
1080
1488
|
return {
|
|
1081
1489
|
toolName: adapter.name,
|
|
1082
1490
|
kind: "command",
|
|
1083
1491
|
invocation: adapter.buildInvocation(options)
|
|
1084
1492
|
};
|
|
1085
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
|
+
}
|
|
1086
1500
|
throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
|
|
1087
1501
|
}
|
|
1088
|
-
async function executeToolRunner(
|
|
1089
|
-
if (
|
|
1090
|
-
return executeToolInvocation(
|
|
1502
|
+
async function executeToolRunner(runner2, input) {
|
|
1503
|
+
if (runner2.kind === "command") {
|
|
1504
|
+
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput);
|
|
1091
1505
|
}
|
|
1092
1506
|
try {
|
|
1093
|
-
return await
|
|
1507
|
+
return await runner2.adapter.runWithSdk(input);
|
|
1094
1508
|
} catch (error) {
|
|
1095
|
-
if (
|
|
1096
|
-
const fallback =
|
|
1509
|
+
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1510
|
+
const fallback = runner2.adapter.buildInvocation(input);
|
|
1097
1511
|
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
|
|
1098
|
-
const sdkFailure = `SDK execution for ${
|
|
1512
|
+
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
|
|
1099
1513
|
return {
|
|
1100
1514
|
...result,
|
|
1101
1515
|
stderr: result.stderr ? `${sdkFailure}
|
|
@@ -1111,31 +1525,53 @@ function normalizeToolRequest(value) {
|
|
|
1111
1525
|
}
|
|
1112
1526
|
throw new Error(`Unsupported local tool: ${value}. Supported tools: auto, none, ${localToolNames.join(", ")}.`);
|
|
1113
1527
|
}
|
|
1114
|
-
async function selectFirstAvailableAdapter() {
|
|
1528
|
+
async function selectFirstAvailableAdapter(requiresModelSelection = false, invocationChannel = "auto") {
|
|
1115
1529
|
for (const adapter of localToolAdapters) {
|
|
1530
|
+
if (requiresModelSelection && !adapter.supportsModelSelection) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1116
1533
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
1117
1534
|
const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
|
|
1118
|
-
if (sdkAvailable
|
|
1535
|
+
if (supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
|
|
1119
1536
|
return adapter;
|
|
1120
1537
|
}
|
|
1121
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
|
+
}
|
|
1122
1542
|
throw new Error(
|
|
1123
|
-
`No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
|
|
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}".`
|
|
1124
1544
|
);
|
|
1125
1545
|
}
|
|
1126
|
-
async function selectRequestedAdapter(tool) {
|
|
1546
|
+
async function selectRequestedAdapter(tool, invocationChannel = "auto") {
|
|
1127
1547
|
const adapter = localToolAdapters.find((candidate) => candidate.name === tool);
|
|
1128
1548
|
if (!adapter) {
|
|
1129
1549
|
throw new Error(`Unsupported local tool: ${tool}.`);
|
|
1130
1550
|
}
|
|
1131
|
-
|
|
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) {
|
|
1132
1563
|
return adapter;
|
|
1133
1564
|
}
|
|
1134
|
-
if (
|
|
1565
|
+
if (invocationChannel !== "sdk" && commandAvailable) {
|
|
1135
1566
|
return adapter;
|
|
1136
1567
|
}
|
|
1137
1568
|
throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
|
|
1138
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
|
+
}
|
|
1139
1575
|
async function isSdkAvailable(adapter) {
|
|
1140
1576
|
if (!adapter.sdkPackageName || !adapter.runWithSdk) {
|
|
1141
1577
|
return false;
|
|
@@ -1292,7 +1728,7 @@ async function runCopilotSdk(input) {
|
|
|
1292
1728
|
await client.start();
|
|
1293
1729
|
const session = await client.createSession({
|
|
1294
1730
|
clientName: "amistio-cli",
|
|
1295
|
-
model: process.env.AMISTIO_COPILOT_MODEL ?? "gpt-5",
|
|
1731
|
+
model: input.model ?? process.env.AMISTIO_COPILOT_MODEL ?? "gpt-5",
|
|
1296
1732
|
workingDirectory: input.rootDir,
|
|
1297
1733
|
enableConfigDiscovery: true,
|
|
1298
1734
|
streaming: input.streamOutput,
|
|
@@ -1334,6 +1770,187 @@ function shellQuote(value) {
|
|
|
1334
1770
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1335
1771
|
}
|
|
1336
1772
|
|
|
1773
|
+
// src/runner-daemon.ts
|
|
1774
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1775
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1776
|
+
import { openSync } from "node:fs";
|
|
1777
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
|
|
1778
|
+
import os3 from "node:os";
|
|
1779
|
+
import path6 from "node:path";
|
|
1780
|
+
function currentRunnerMode() {
|
|
1781
|
+
return process.env.AMISTIO_RUNNER_MODE === "background" ? "background" : "foreground";
|
|
1782
|
+
}
|
|
1783
|
+
function defaultRunnerMetadataDir() {
|
|
1784
|
+
return path6.join(os3.homedir(), ".config", "amistio", "runners");
|
|
1785
|
+
}
|
|
1786
|
+
async function startRunnerDaemon(input) {
|
|
1787
|
+
const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
|
|
1788
|
+
const existing = await readRunnerDaemonMetadata(input, metadataDir);
|
|
1789
|
+
if (existing?.status === "running" && isProcessRunning(existing.pid)) {
|
|
1790
|
+
throw new Error(`Background runner ${existing.runnerId} is already running with PID ${existing.pid}.`);
|
|
1791
|
+
}
|
|
1792
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1793
|
+
const logPath = path6.join(metadataDir, `${runnerDaemonKey(input)}.log`);
|
|
1794
|
+
const logFd = openSync(logPath, "a");
|
|
1795
|
+
const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...input.args], {
|
|
1796
|
+
cwd: input.rootDir,
|
|
1797
|
+
detached: true,
|
|
1798
|
+
env: {
|
|
1799
|
+
...process.env,
|
|
1800
|
+
AMISTIO_RUNNER_MODE: "background"
|
|
1801
|
+
},
|
|
1802
|
+
stdio: ["ignore", logFd, logFd]
|
|
1803
|
+
});
|
|
1804
|
+
if (!child.pid) {
|
|
1805
|
+
throw new Error("Failed to start background runner process.");
|
|
1806
|
+
}
|
|
1807
|
+
child.unref();
|
|
1808
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1809
|
+
const metadata = {
|
|
1810
|
+
schemaVersion: 1,
|
|
1811
|
+
accountId: input.accountId,
|
|
1812
|
+
projectId: input.projectId,
|
|
1813
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
1814
|
+
runnerId: input.runnerId,
|
|
1815
|
+
rootDir: path6.resolve(input.rootDir),
|
|
1816
|
+
apiUrl: input.apiUrl,
|
|
1817
|
+
pid: child.pid,
|
|
1818
|
+
status: "running",
|
|
1819
|
+
startedAt: now,
|
|
1820
|
+
updatedAt: now,
|
|
1821
|
+
hostname: os3.hostname(),
|
|
1822
|
+
logPath
|
|
1823
|
+
};
|
|
1824
|
+
await writeRunnerDaemonMetadata(metadata, metadataDir);
|
|
1825
|
+
return metadata;
|
|
1826
|
+
}
|
|
1827
|
+
async function restartRunnerDaemonProcess(metadata, args, input = {}) {
|
|
1828
|
+
const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
|
|
1829
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1830
|
+
const logPath = metadata.logPath ?? path6.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
|
|
1831
|
+
const logFd = openSync(logPath, "a");
|
|
1832
|
+
const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...args], {
|
|
1833
|
+
cwd: metadata.rootDir,
|
|
1834
|
+
detached: true,
|
|
1835
|
+
env: {
|
|
1836
|
+
...process.env,
|
|
1837
|
+
AMISTIO_RUNNER_MODE: "background"
|
|
1838
|
+
},
|
|
1839
|
+
stdio: ["ignore", logFd, logFd]
|
|
1840
|
+
});
|
|
1841
|
+
if (!child.pid) {
|
|
1842
|
+
throw new Error("Failed to start replacement background runner process.");
|
|
1843
|
+
}
|
|
1844
|
+
child.unref();
|
|
1845
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1846
|
+
const replacement = {
|
|
1847
|
+
...metadata,
|
|
1848
|
+
pid: child.pid,
|
|
1849
|
+
status: "running",
|
|
1850
|
+
startedAt: now,
|
|
1851
|
+
updatedAt: now,
|
|
1852
|
+
hostname: os3.hostname(),
|
|
1853
|
+
logPath
|
|
1854
|
+
};
|
|
1855
|
+
await writeRunnerDaemonMetadata(replacement, metadataDir);
|
|
1856
|
+
return replacement;
|
|
1857
|
+
}
|
|
1858
|
+
async function listRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
1859
|
+
let entries;
|
|
1860
|
+
try {
|
|
1861
|
+
entries = await readdir2(metadataDir);
|
|
1862
|
+
} catch {
|
|
1863
|
+
return [];
|
|
1864
|
+
}
|
|
1865
|
+
const records = await Promise.all(
|
|
1866
|
+
entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(path6.join(metadataDir, entry)))
|
|
1867
|
+
);
|
|
1868
|
+
return records.filter((record) => Boolean(record)).filter((record) => record.accountId === input.accountId && record.projectId === input.projectId && record.repositoryLinkId === input.repositoryLinkId).filter((record) => !input.runnerId || record.runnerId === input.runnerId).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1869
|
+
}
|
|
1870
|
+
async function readRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
1871
|
+
return readRunnerDaemonMetadataFile(runnerDaemonMetadataPath(input, metadataDir));
|
|
1872
|
+
}
|
|
1873
|
+
async function writeRunnerDaemonMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
1874
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1875
|
+
await writeFile5(runnerDaemonMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
1876
|
+
}
|
|
1877
|
+
async function markRunnerDaemonStopped(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
1878
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1879
|
+
const stopped = {
|
|
1880
|
+
...metadata,
|
|
1881
|
+
status: "stopped",
|
|
1882
|
+
stoppedAt: now,
|
|
1883
|
+
updatedAt: now
|
|
1884
|
+
};
|
|
1885
|
+
await writeRunnerDaemonMetadata(stopped, metadataDir);
|
|
1886
|
+
return stopped;
|
|
1887
|
+
}
|
|
1888
|
+
async function stopRunnerDaemonProcess(metadata) {
|
|
1889
|
+
if (!isProcessRunning(metadata.pid)) {
|
|
1890
|
+
return "not-running";
|
|
1891
|
+
}
|
|
1892
|
+
process.kill(metadata.pid, "SIGTERM");
|
|
1893
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
1894
|
+
if (!isProcessRunning(metadata.pid)) {
|
|
1895
|
+
return "stopped";
|
|
1896
|
+
}
|
|
1897
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1898
|
+
}
|
|
1899
|
+
return isProcessRunning(metadata.pid) ? "not-running" : "stopped";
|
|
1900
|
+
}
|
|
1901
|
+
function isProcessRunning(pid) {
|
|
1902
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
try {
|
|
1906
|
+
process.kill(pid, 0);
|
|
1907
|
+
return true;
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
1910
|
+
return code === "EPERM";
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function runnerDaemonRuntimeStatus(metadata) {
|
|
1914
|
+
if (metadata.status === "stopped") {
|
|
1915
|
+
return "stopped";
|
|
1916
|
+
}
|
|
1917
|
+
return isProcessRunning(metadata.pid) ? "running" : "stale";
|
|
1918
|
+
}
|
|
1919
|
+
function runnerDaemonUptime(metadata, now = Date.now()) {
|
|
1920
|
+
const startedAt = Date.parse(metadata.startedAt);
|
|
1921
|
+
if (!Number.isFinite(startedAt)) {
|
|
1922
|
+
return "unknown";
|
|
1923
|
+
}
|
|
1924
|
+
const totalSeconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
1925
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1926
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1927
|
+
const seconds = totalSeconds % 60;
|
|
1928
|
+
if (hours > 0) {
|
|
1929
|
+
return `${hours}h ${minutes}m`;
|
|
1930
|
+
}
|
|
1931
|
+
if (minutes > 0) {
|
|
1932
|
+
return `${minutes}m ${seconds}s`;
|
|
1933
|
+
}
|
|
1934
|
+
return `${seconds}s`;
|
|
1935
|
+
}
|
|
1936
|
+
function runnerDaemonMetadataPath(input, metadataDir) {
|
|
1937
|
+
return path6.join(metadataDir, `${runnerDaemonKey(input)}.json`);
|
|
1938
|
+
}
|
|
1939
|
+
function runnerDaemonKey(input) {
|
|
1940
|
+
return createHash2("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
1941
|
+
}
|
|
1942
|
+
async function readRunnerDaemonMetadataFile(filePath) {
|
|
1943
|
+
try {
|
|
1944
|
+
const parsed = JSON.parse(await readFile3(filePath, "utf8"));
|
|
1945
|
+
if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
|
|
1946
|
+
return void 0;
|
|
1947
|
+
}
|
|
1948
|
+
return parsed;
|
|
1949
|
+
} catch {
|
|
1950
|
+
return void 0;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1337
1954
|
// src/session-policy.ts
|
|
1338
1955
|
var maxIdleMs = 24 * 60 * 60 * 1e3;
|
|
1339
1956
|
var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -1455,8 +2072,8 @@ function tokens(value) {
|
|
|
1455
2072
|
}
|
|
1456
2073
|
|
|
1457
2074
|
// src/sync.ts
|
|
1458
|
-
import { mkdir as
|
|
1459
|
-
import
|
|
2075
|
+
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2076
|
+
import path7 from "node:path";
|
|
1460
2077
|
var syncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
1461
2078
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
1462
2079
|
const localDocuments = await readLocalSyncedDocuments(rootDir);
|
|
@@ -1533,7 +2150,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
1533
2150
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
1534
2151
|
const documents = [];
|
|
1535
2152
|
for (const fullPath of markdownFiles) {
|
|
1536
|
-
const raw = await
|
|
2153
|
+
const raw = await readFile4(fullPath, "utf8");
|
|
1537
2154
|
const parsed = parseSyncedMarkdown(raw);
|
|
1538
2155
|
if (!parsed) {
|
|
1539
2156
|
continue;
|
|
@@ -1574,8 +2191,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
1574
2191
|
result.skipped.push(document.repoPath);
|
|
1575
2192
|
continue;
|
|
1576
2193
|
}
|
|
1577
|
-
await
|
|
1578
|
-
await
|
|
2194
|
+
await mkdir6(path7.dirname(fullPath), { recursive: true });
|
|
2195
|
+
await writeFile6(fullPath, createSyncedDocumentMarkdown(document), "utf8");
|
|
1579
2196
|
result.written.push(document.repoPath);
|
|
1580
2197
|
}
|
|
1581
2198
|
return result;
|
|
@@ -1636,7 +2253,7 @@ function parseSyncedMarkdown(content) {
|
|
|
1636
2253
|
}
|
|
1637
2254
|
async function readExistingSyncedDocument(fullPath) {
|
|
1638
2255
|
try {
|
|
1639
|
-
const raw = await
|
|
2256
|
+
const raw = await readFile4(fullPath, "utf8");
|
|
1640
2257
|
const parsed = parseSyncedMarkdown(raw);
|
|
1641
2258
|
if (!parsed) {
|
|
1642
2259
|
return { exists: true };
|
|
@@ -1661,7 +2278,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
1661
2278
|
async function findMarkdownFiles(rootDir) {
|
|
1662
2279
|
const files = [];
|
|
1663
2280
|
for (const syncRoot of syncRoots) {
|
|
1664
|
-
const fullRoot =
|
|
2281
|
+
const fullRoot = path7.join(rootDir, syncRoot);
|
|
1665
2282
|
if (!await exists2(fullRoot)) {
|
|
1666
2283
|
continue;
|
|
1667
2284
|
}
|
|
@@ -1670,8 +2287,8 @@ async function findMarkdownFiles(rootDir) {
|
|
|
1670
2287
|
return files;
|
|
1671
2288
|
}
|
|
1672
2289
|
async function walkMarkdownFiles(directory, files) {
|
|
1673
|
-
for (const entry of await
|
|
1674
|
-
const fullPath =
|
|
2290
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
2291
|
+
const fullPath = path7.join(directory, entry.name);
|
|
1675
2292
|
if (entry.isDirectory()) {
|
|
1676
2293
|
await walkMarkdownFiles(fullPath, files);
|
|
1677
2294
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -1680,30 +2297,30 @@ async function walkMarkdownFiles(directory, files) {
|
|
|
1680
2297
|
}
|
|
1681
2298
|
}
|
|
1682
2299
|
function safeRepoPath(rootDir, repoPath) {
|
|
1683
|
-
if (
|
|
2300
|
+
if (path7.isAbsolute(repoPath)) {
|
|
1684
2301
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
1685
2302
|
}
|
|
1686
|
-
const normalized =
|
|
1687
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
2303
|
+
const normalized = path7.normalize(repoPath);
|
|
2304
|
+
if (normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
|
|
1688
2305
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
1689
2306
|
}
|
|
1690
|
-
const root =
|
|
1691
|
-
const fullPath =
|
|
1692
|
-
if (!fullPath.startsWith(`${root}${
|
|
2307
|
+
const root = path7.resolve(rootDir);
|
|
2308
|
+
const fullPath = path7.resolve(root, normalized);
|
|
2309
|
+
if (!fullPath.startsWith(`${root}${path7.sep}`)) {
|
|
1693
2310
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
1694
2311
|
}
|
|
1695
2312
|
return fullPath;
|
|
1696
2313
|
}
|
|
1697
2314
|
function isControlPlanePath(repoPath) {
|
|
1698
|
-
const normalized =
|
|
1699
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
2315
|
+
const normalized = path7.normalize(repoPath);
|
|
2316
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path7.sep}`));
|
|
1700
2317
|
}
|
|
1701
2318
|
function toRepoPath(rootDir, fullPath) {
|
|
1702
|
-
return
|
|
2319
|
+
return path7.relative(rootDir, fullPath).split(path7.sep).join("/");
|
|
1703
2320
|
}
|
|
1704
2321
|
function inferTitle(content, repoPath) {
|
|
1705
2322
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
1706
|
-
return heading ||
|
|
2323
|
+
return heading || path7.basename(repoPath, path7.extname(repoPath));
|
|
1707
2324
|
}
|
|
1708
2325
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
1709
2326
|
return {
|
|
@@ -1724,9 +2341,9 @@ async function exists2(filePath) {
|
|
|
1724
2341
|
}
|
|
1725
2342
|
|
|
1726
2343
|
// src/tool-session-store.ts
|
|
1727
|
-
import { mkdir as
|
|
1728
|
-
import
|
|
1729
|
-
import
|
|
2344
|
+
import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
|
|
2345
|
+
import os4 from "node:os";
|
|
2346
|
+
import path8 from "node:path";
|
|
1730
2347
|
var LocalToolSessionStore = class {
|
|
1731
2348
|
constructor(filePath = defaultSessionStorePath()) {
|
|
1732
2349
|
this.filePath = filePath;
|
|
@@ -1740,12 +2357,12 @@ var LocalToolSessionStore = class {
|
|
|
1740
2357
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
1741
2358
|
const data = await this.read();
|
|
1742
2359
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1743
|
-
await
|
|
1744
|
-
await
|
|
2360
|
+
await mkdir7(path8.dirname(this.filePath), { recursive: true });
|
|
2361
|
+
await writeFile7(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1745
2362
|
}
|
|
1746
2363
|
async read() {
|
|
1747
2364
|
try {
|
|
1748
|
-
return JSON.parse(await
|
|
2365
|
+
return JSON.parse(await readFile5(this.filePath, "utf8"));
|
|
1749
2366
|
} catch {
|
|
1750
2367
|
return {};
|
|
1751
2368
|
}
|
|
@@ -1753,21 +2370,24 @@ var LocalToolSessionStore = class {
|
|
|
1753
2370
|
};
|
|
1754
2371
|
function defaultSessionStorePath() {
|
|
1755
2372
|
if (process.platform === "darwin") {
|
|
1756
|
-
return
|
|
2373
|
+
return path8.join(os4.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
1757
2374
|
}
|
|
1758
2375
|
if (process.platform === "win32") {
|
|
1759
|
-
return
|
|
2376
|
+
return path8.join(process.env.APPDATA ?? os4.homedir(), "Amistio", "tool-sessions.json");
|
|
1760
2377
|
}
|
|
1761
|
-
return
|
|
2378
|
+
return path8.join(process.env.XDG_STATE_HOME ?? path8.join(os4.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
1762
2379
|
}
|
|
1763
2380
|
|
|
1764
2381
|
// src/work-runner.ts
|
|
1765
2382
|
var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
|
|
1766
2383
|
var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
|
|
1767
|
-
function createWorkExecutionPrompt(workItem) {
|
|
2384
|
+
function createWorkExecutionPrompt(workItem, context) {
|
|
1768
2385
|
if (workItem.workKind === "brainGeneration") {
|
|
1769
2386
|
return createBrainGenerationPrompt(workItem);
|
|
1770
2387
|
}
|
|
2388
|
+
if (workItem.workKind === "planRevision") {
|
|
2389
|
+
return createPlanRevisionPrompt(workItem, context?.planRevision);
|
|
2390
|
+
}
|
|
1771
2391
|
return [
|
|
1772
2392
|
"# Amistio Work Execution",
|
|
1773
2393
|
"",
|
|
@@ -1792,6 +2412,42 @@ function createWorkExecutionPrompt(workItem) {
|
|
|
1792
2412
|
"- Run relevant verification commands when feasible and summarize results."
|
|
1793
2413
|
].join("\n");
|
|
1794
2414
|
}
|
|
2415
|
+
function createPlanRevisionPrompt(workItem, context) {
|
|
2416
|
+
const messages = context?.messages ?? [];
|
|
2417
|
+
return [
|
|
2418
|
+
"# Amistio Plan Revision",
|
|
2419
|
+
"",
|
|
2420
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
2421
|
+
"Revise the selected generated plan using the user's conversation. Do not implement product/source code changes in this pass.",
|
|
2422
|
+
"",
|
|
2423
|
+
"## Work Item",
|
|
2424
|
+
"",
|
|
2425
|
+
`Title: ${workItem.title}`,
|
|
2426
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
2427
|
+
`Project ID: ${workItem.projectId}`,
|
|
2428
|
+
`Document ID: ${workItem.reviewDocumentId ?? "unknown"}`,
|
|
2429
|
+
`Document revision: ${workItem.reviewDocumentRevision ?? "unknown"}`,
|
|
2430
|
+
"",
|
|
2431
|
+
"## Current Plan",
|
|
2432
|
+
"",
|
|
2433
|
+
context?.planDocument.content ?? "The current plan document could not be loaded. Explain the blocker in the result summary.",
|
|
2434
|
+
"",
|
|
2435
|
+
"## Conversation",
|
|
2436
|
+
"",
|
|
2437
|
+
messages.length ? messages.map((message) => `- ${message.role} / ${message.intent} / ${message.status} / rev ${message.documentRevision}: ${message.content}`).join("\n") : "No conversation messages were loaded.",
|
|
2438
|
+
"",
|
|
2439
|
+
"## Output Contract",
|
|
2440
|
+
"",
|
|
2441
|
+
"Print exactly one JSON object between the markers below with an artifacts array containing one revised plan artifact.",
|
|
2442
|
+
'The artifact must use documentType "plan", keep the plan under plans/, and include the complete revised plan content.',
|
|
2443
|
+
"",
|
|
2444
|
+
generationResultStart,
|
|
2445
|
+
'{"artifacts":[{"documentType":"plan","title":"Revised Plan","repoPath":"plans/PLAN-revised.md","content":"# Revised Plan\\n\\n## Goal\\n..."}]}',
|
|
2446
|
+
generationResultEnd,
|
|
2447
|
+
"",
|
|
2448
|
+
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
2449
|
+
].join("\n");
|
|
2450
|
+
}
|
|
1795
2451
|
function parseBrainGenerationArtifacts(output) {
|
|
1796
2452
|
const start = output.indexOf(generationResultStart);
|
|
1797
2453
|
const end = output.indexOf(generationResultEnd, start + generationResultStart.length);
|
|
@@ -1861,12 +2517,803 @@ function stripJsonFence(value) {
|
|
|
1861
2517
|
return trimmed.replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "").trim();
|
|
1862
2518
|
}
|
|
1863
2519
|
|
|
2520
|
+
// src/runner-status.ts
|
|
2521
|
+
var watchStateReminderMs = 60 * 1e3;
|
|
2522
|
+
function formatWatchStartupContext(input) {
|
|
2523
|
+
return [
|
|
2524
|
+
`Runner ${input.runnerId} is watching project ${input.projectId}.`,
|
|
2525
|
+
`Repository link: ${input.repositoryLinkId}`,
|
|
2526
|
+
`API: ${input.apiUrl}`,
|
|
2527
|
+
`Polling interval: ${input.intervalSeconds}s. Press Ctrl+C to stop.`
|
|
2528
|
+
];
|
|
2529
|
+
}
|
|
2530
|
+
function formatWatchIdleLine(action, intervalSeconds) {
|
|
2531
|
+
return `${formatProjectNextAction(action)} Checking again in ${intervalSeconds}s.`;
|
|
2532
|
+
}
|
|
2533
|
+
function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
|
|
2534
|
+
const key = watchStateKey(action);
|
|
2535
|
+
return !previous || previous.key !== key || nowMs - previous.printedAtMs >= reminderMs;
|
|
2536
|
+
}
|
|
2537
|
+
function watchStateKey(action) {
|
|
2538
|
+
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
2539
|
+
}
|
|
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
|
+
|
|
1864
3311
|
// src/index.ts
|
|
1865
3312
|
var program = new Command();
|
|
1866
3313
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
1867
3314
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
1868
|
-
program.name("amistio").description("Amistio project brain CLI").version("0.1.
|
|
1869
|
-
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";
|
|
1870
3317
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
1871
3318
|
const created = await initControlPlane(options.root);
|
|
1872
3319
|
console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
|
|
@@ -1907,8 +3354,76 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
|
|
|
1907
3354
|
console.log(`Wrote non-secret project metadata to ${filePath}.`);
|
|
1908
3355
|
console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
|
|
1909
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
|
+
});
|
|
1910
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) => {
|
|
1911
|
-
let repositoryLinkId = options.repositoryLink ?? `repo_${
|
|
3426
|
+
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
|
|
1912
3427
|
let credential = options.token;
|
|
1913
3428
|
if (options.pairingCode) {
|
|
1914
3429
|
const pairing = await new ApiClient({
|
|
@@ -2019,7 +3534,19 @@ work.command("list").description("List queued work without claiming it").option(
|
|
|
2019
3534
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
2020
3535
|
return;
|
|
2021
3536
|
}
|
|
2022
|
-
const { workItems } = await
|
|
3537
|
+
const [{ workItems }, { documents }, { runners }] = await Promise.all([
|
|
3538
|
+
context.client.listWorkItems(context.metadata.amistioProjectId),
|
|
3539
|
+
context.client.listBrainDocuments(context.metadata.amistioProjectId),
|
|
3540
|
+
context.client.listRunners(context.metadata.amistioProjectId).catch(() => ({ runners: [] }))
|
|
3541
|
+
]);
|
|
3542
|
+
const nextAction = computeProjectNextAction({
|
|
3543
|
+
projectId: context.metadata.amistioProjectId,
|
|
3544
|
+
repositoryLinks: [createCliRepositoryLink(context.metadata, options.root)],
|
|
3545
|
+
documents,
|
|
3546
|
+
workItems,
|
|
3547
|
+
runnerHeartbeats: runners
|
|
3548
|
+
});
|
|
3549
|
+
console.log(`Next action: ${formatProjectNextAction(nextAction)}`);
|
|
2023
3550
|
if (!workItems.length) {
|
|
2024
3551
|
console.log("No work items are queued for this project.");
|
|
2025
3552
|
return;
|
|
@@ -2042,7 +3569,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
2042
3569
|
}
|
|
2043
3570
|
const prompt = createWorkExecutionPrompt(workItem);
|
|
2044
3571
|
if (options.out) {
|
|
2045
|
-
await
|
|
3572
|
+
await writeFile8(options.out, prompt, "utf8");
|
|
2046
3573
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
2047
3574
|
} else {
|
|
2048
3575
|
console.log(prompt);
|
|
@@ -2056,7 +3583,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
|
|
|
2056
3583
|
}
|
|
2057
3584
|
console.log("custom - pass --tool-command to use any other local runner command.");
|
|
2058
3585
|
});
|
|
2059
|
-
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("--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) => {
|
|
2060
3587
|
const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
|
|
2061
3588
|
const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
|
|
2062
3589
|
if (options.promptOut) {
|
|
@@ -2068,15 +3595,17 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2068
3595
|
return;
|
|
2069
3596
|
}
|
|
2070
3597
|
const sessionPolicy = normalizeSessionPolicy(options.session);
|
|
2071
|
-
const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, ...options.toolCommand ? { toolCommand: options.toolCommand } : {} });
|
|
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 } : {} });
|
|
2072
3599
|
console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
2073
3600
|
const result = await runLocalTool({
|
|
2074
3601
|
rootDir: options.root,
|
|
2075
3602
|
prompt,
|
|
2076
3603
|
tool: options.tool,
|
|
3604
|
+
invocationChannel: options.invocationChannel,
|
|
2077
3605
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
3606
|
+
...options.model ? { model: options.model } : {},
|
|
2078
3607
|
streamOutput: options.stream,
|
|
2079
|
-
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${
|
|
3608
|
+
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
|
|
2080
3609
|
});
|
|
2081
3610
|
if (!options.stream && result.stdout.trim()) {
|
|
2082
3611
|
console.log(result.stdout.trim());
|
|
@@ -2088,7 +3617,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2088
3617
|
process.exitCode = result.exitCode;
|
|
2089
3618
|
}
|
|
2090
3619
|
});
|
|
2091
|
-
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) => {
|
|
2092
3621
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2093
3622
|
if (!context) {
|
|
2094
3623
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -2099,10 +3628,34 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2099
3628
|
process.exitCode = 1;
|
|
2100
3629
|
return;
|
|
2101
3630
|
}
|
|
3631
|
+
if (options.background) {
|
|
3632
|
+
if (options.dryRun) {
|
|
3633
|
+
console.log("Background runners cannot be started in dry-run mode.");
|
|
3634
|
+
process.exitCode = 1;
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
const metadata = await startRunnerDaemon({
|
|
3638
|
+
accountId: context.metadata.amistioAccountId,
|
|
3639
|
+
projectId: context.metadata.amistioProjectId,
|
|
3640
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3641
|
+
runnerId: options.runnerId,
|
|
3642
|
+
rootDir: path12.resolve(options.root),
|
|
3643
|
+
apiUrl: options.apiUrl,
|
|
3644
|
+
args: buildBackgroundRunnerArgs(options)
|
|
3645
|
+
});
|
|
3646
|
+
console.log(`Started background runner ${metadata.runnerId} with PID ${metadata.pid}.`);
|
|
3647
|
+
if (metadata.logPath) {
|
|
3648
|
+
console.log(`Log: ${metadata.logPath}`);
|
|
3649
|
+
}
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
2102
3652
|
if (options.watch) {
|
|
2103
|
-
|
|
3653
|
+
for (const line of formatWatchStartupContext({ runnerId: options.runnerId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, apiUrl: options.apiUrl, intervalSeconds: options.intervalSeconds })) {
|
|
3654
|
+
console.log(line);
|
|
3655
|
+
}
|
|
2104
3656
|
}
|
|
2105
3657
|
let iterations = 0;
|
|
3658
|
+
let lastWatchStateLog;
|
|
2106
3659
|
while (true) {
|
|
2107
3660
|
iterations += 1;
|
|
2108
3661
|
const result = await runNextWorkItem({
|
|
@@ -2112,10 +3665,22 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2112
3665
|
runnerId: options.runnerId,
|
|
2113
3666
|
root: options.root,
|
|
2114
3667
|
sessionPolicy: normalizeSessionPolicy(options.session),
|
|
2115
|
-
tool: options.tool,
|
|
3668
|
+
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
3669
|
+
...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
|
|
3670
|
+
...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
|
|
2116
3671
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
2117
3672
|
dryRun: Boolean(options.dryRun),
|
|
2118
|
-
stream: options.stream
|
|
3673
|
+
stream: options.stream,
|
|
3674
|
+
commandContext: {
|
|
3675
|
+
accountId: context.metadata.amistioAccountId,
|
|
3676
|
+
apiUrl: options.apiUrl,
|
|
3677
|
+
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
3678
|
+
projectId: context.metadata.amistioProjectId,
|
|
3679
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3680
|
+
root: options.root,
|
|
3681
|
+
runnerId: options.runnerId
|
|
3682
|
+
},
|
|
3683
|
+
suppressIdleOutput: Boolean(options.watch)
|
|
2119
3684
|
});
|
|
2120
3685
|
if (!options.watch || options.dryRun) {
|
|
2121
3686
|
if (result.exitCode !== 0) {
|
|
@@ -2123,16 +3688,97 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2123
3688
|
}
|
|
2124
3689
|
return;
|
|
2125
3690
|
}
|
|
3691
|
+
if (result.status === "idle" && result.nextAction) {
|
|
3692
|
+
const nowMs = Date.now();
|
|
3693
|
+
if (shouldPrintWatchState(result.nextAction, lastWatchStateLog, nowMs)) {
|
|
3694
|
+
console.log(formatWatchIdleLine(result.nextAction, options.intervalSeconds));
|
|
3695
|
+
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
if (result.stopRunner) {
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
2126
3701
|
if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
|
|
2127
3702
|
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
2128
3703
|
return;
|
|
2129
3704
|
}
|
|
2130
|
-
if (result.status === "idle") {
|
|
2131
|
-
console.log(`No approved work item is available. Checking again in ${options.intervalSeconds}s.`);
|
|
2132
|
-
}
|
|
2133
3705
|
await delay(options.intervalSeconds * 1e3);
|
|
2134
3706
|
}
|
|
2135
3707
|
});
|
|
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
|
+
});
|
|
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) => {
|
|
3713
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
3714
|
+
if (!context) {
|
|
3715
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const records = await listRunnerDaemonMetadata({
|
|
3719
|
+
accountId: context.metadata.amistioAccountId,
|
|
3720
|
+
projectId: context.metadata.amistioProjectId,
|
|
3721
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3722
|
+
...options.runnerId ? { runnerId: options.runnerId } : {}
|
|
3723
|
+
});
|
|
3724
|
+
const runners = await context.client.listRunners(context.metadata.amistioProjectId).then((result) => result.runners).catch(() => []);
|
|
3725
|
+
if (!records.length) {
|
|
3726
|
+
console.log("No background runner metadata found for this paired repository.");
|
|
3727
|
+
if (runners.length) {
|
|
3728
|
+
console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
|
|
3729
|
+
}
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
for (const record of records) {
|
|
3733
|
+
const runtimeStatus = runnerDaemonRuntimeStatus(record);
|
|
3734
|
+
const heartbeat = runners.find((item) => item.runnerId === record.runnerId);
|
|
3735
|
+
console.log(`Runner ${record.runnerId}: ${runtimeStatus}`);
|
|
3736
|
+
console.log(` PID: ${record.pid}`);
|
|
3737
|
+
console.log(` Uptime: ${runtimeStatus === "running" ? runnerDaemonUptime(record) : "not running"}`);
|
|
3738
|
+
console.log(` Project: ${record.projectId}`);
|
|
3739
|
+
console.log(` Repository link: ${record.repositoryLinkId}`);
|
|
3740
|
+
console.log(` Root: ${record.rootDir}`);
|
|
3741
|
+
console.log(` API: ${record.apiUrl}`);
|
|
3742
|
+
console.log(` Host: ${record.hostname}`);
|
|
3743
|
+
if (record.logPath) {
|
|
3744
|
+
console.log(` Log: ${record.logPath}`);
|
|
3745
|
+
}
|
|
3746
|
+
if (heartbeat) {
|
|
3747
|
+
console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
});
|
|
3751
|
+
runner.command("stop").description("Stop a background runner for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Runner ID to stop when multiple background runners exist").action(async (options) => {
|
|
3752
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
3753
|
+
if (!context) {
|
|
3754
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const records = await listRunnerDaemonMetadata({
|
|
3758
|
+
accountId: context.metadata.amistioAccountId,
|
|
3759
|
+
projectId: context.metadata.amistioProjectId,
|
|
3760
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3761
|
+
...options.runnerId ? { runnerId: options.runnerId } : {}
|
|
3762
|
+
});
|
|
3763
|
+
if (!records.length) {
|
|
3764
|
+
console.log("No background runner metadata found for this paired repository.");
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
if (records.length > 1 && !options.runnerId) {
|
|
3768
|
+
console.log(`Multiple background runners found: ${records.map((record2) => record2.runnerId).join(", ")}. Pass --runner-id to stop one.`);
|
|
3769
|
+
process.exitCode = 1;
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const record = records[0];
|
|
3773
|
+
const stopResult = await stopRunnerDaemonProcess(record);
|
|
3774
|
+
await markRunnerDaemonStopped(record);
|
|
3775
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
|
|
3776
|
+
version: CLI_VERSION,
|
|
3777
|
+
mode: "background",
|
|
3778
|
+
hostname: record.hostname
|
|
3779
|
+
}).catch(() => void 0);
|
|
3780
|
+
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
3781
|
+
});
|
|
2136
3782
|
async function runNextWorkItem({
|
|
2137
3783
|
apiClient,
|
|
2138
3784
|
dryRun,
|
|
@@ -2142,23 +3788,50 @@ async function runNextWorkItem({
|
|
|
2142
3788
|
runnerId,
|
|
2143
3789
|
sessionPolicy,
|
|
2144
3790
|
stream,
|
|
2145
|
-
|
|
2146
|
-
|
|
3791
|
+
explicitModel,
|
|
3792
|
+
explicitInvocationChannel,
|
|
3793
|
+
explicitTool,
|
|
3794
|
+
toolCommand,
|
|
3795
|
+
commandContext,
|
|
3796
|
+
suppressIdleOutput
|
|
2147
3797
|
}) {
|
|
2148
|
-
|
|
3798
|
+
const toolConfig = await resolveRunnerToolConfig({
|
|
3799
|
+
apiClient,
|
|
3800
|
+
projectId,
|
|
3801
|
+
...explicitInvocationChannel ? { explicitInvocationChannel } : {},
|
|
3802
|
+
...explicitModel ? { explicitModel } : {},
|
|
3803
|
+
...explicitTool ? { explicitTool } : {},
|
|
3804
|
+
...toolCommand ? { toolCommand } : {}
|
|
3805
|
+
});
|
|
3806
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, toolConfig.ready ? "online" : "blocked", runnerHeartbeatMetadata(toolConfig));
|
|
3807
|
+
const commandResult = await runPendingRunnerCommand(apiClient, commandContext, runnerHeartbeatMetadata(toolConfig));
|
|
3808
|
+
if (commandResult.handled) {
|
|
3809
|
+
if (commandResult.message) {
|
|
3810
|
+
console.log(commandResult.message);
|
|
3811
|
+
}
|
|
3812
|
+
return { status: commandResult.succeeded ? "completed" : "failed", exitCode: commandResult.succeeded ? 0 : 1, ...commandResult.stopRunner ? { stopRunner: true } : {} };
|
|
3813
|
+
}
|
|
3814
|
+
if (!toolConfig.ready) {
|
|
3815
|
+
console.log(toolConfig.message);
|
|
3816
|
+
return { status: "blocked", exitCode: 1 };
|
|
3817
|
+
}
|
|
2149
3818
|
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId);
|
|
2150
3819
|
if (!result.workItem) {
|
|
2151
|
-
|
|
2152
|
-
|
|
3820
|
+
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
3821
|
+
const message = formatProjectNextAction(nextAction);
|
|
3822
|
+
if (!suppressIdleOutput) {
|
|
3823
|
+
console.log(message);
|
|
3824
|
+
}
|
|
3825
|
+
return { status: "idle", exitCode: 0, nextAction, message };
|
|
2153
3826
|
}
|
|
2154
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running",
|
|
2155
|
-
const prompt =
|
|
2156
|
-
if (dryRun || tool === "none") {
|
|
3827
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", runnerHeartbeatMetadata(toolConfig));
|
|
3828
|
+
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
3829
|
+
if (dryRun || toolConfig.tool === "none") {
|
|
2157
3830
|
console.log(prompt);
|
|
2158
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
3831
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2159
3832
|
return { status: "preview", exitCode: 0 };
|
|
2160
3833
|
}
|
|
2161
|
-
const preview = await createToolRunPreview({ rootDir: root, prompt, tool, ...toolCommand ? { toolCommand } : {} });
|
|
3834
|
+
const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
2162
3835
|
const sessionContext = await prepareToolSession({
|
|
2163
3836
|
apiClient,
|
|
2164
3837
|
projectId,
|
|
@@ -2166,6 +3839,7 @@ async function runNextWorkItem({
|
|
|
2166
3839
|
runnerId,
|
|
2167
3840
|
sessionPolicy: result.workItem.sessionPolicy ?? sessionPolicy,
|
|
2168
3841
|
toolName: preview.toolName,
|
|
3842
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
2169
3843
|
supportsSessionReuse: preview.supportsSessionReuse,
|
|
2170
3844
|
resumabilityScope: preview.resumabilityScope,
|
|
2171
3845
|
workItem: result.workItem
|
|
@@ -2177,8 +3851,10 @@ async function runNextWorkItem({
|
|
|
2177
3851
|
const toolResult = await runLocalTool({
|
|
2178
3852
|
rootDir: root,
|
|
2179
3853
|
prompt,
|
|
2180
|
-
tool,
|
|
3854
|
+
tool: toolConfig.tool,
|
|
3855
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
2181
3856
|
...toolCommand ? { toolCommand } : {},
|
|
3857
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
2182
3858
|
streamOutput: stream,
|
|
2183
3859
|
...sessionContext.toolSession ? {
|
|
2184
3860
|
session: {
|
|
@@ -2189,7 +3865,7 @@ async function runNextWorkItem({
|
|
|
2189
3865
|
}
|
|
2190
3866
|
} : {}
|
|
2191
3867
|
}).catch(async (error) => {
|
|
2192
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked",
|
|
3868
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", runnerHeartbeatMetadata(toolConfig));
|
|
2193
3869
|
await markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage2(error));
|
|
2194
3870
|
throw error;
|
|
2195
3871
|
});
|
|
@@ -2202,7 +3878,7 @@ async function runNextWorkItem({
|
|
|
2202
3878
|
if (!stream && toolResult.stderr.trim()) {
|
|
2203
3879
|
console.error(toolResult.stderr.trim());
|
|
2204
3880
|
}
|
|
2205
|
-
if (result.workItem.workKind === "brainGeneration") {
|
|
3881
|
+
if (result.workItem.workKind === "brainGeneration" || result.workItem.workKind === "planRevision") {
|
|
2206
3882
|
return finalizeBrainGenerationWork({
|
|
2207
3883
|
apiClient,
|
|
2208
3884
|
durationMs: Date.now() - startedAt,
|
|
@@ -2210,6 +3886,7 @@ async function runNextWorkItem({
|
|
|
2210
3886
|
repositoryLinkId,
|
|
2211
3887
|
runnerId,
|
|
2212
3888
|
sessionContext,
|
|
3889
|
+
toolConfig,
|
|
2213
3890
|
toolName: preview.toolName,
|
|
2214
3891
|
toolResult,
|
|
2215
3892
|
workItem: result.workItem
|
|
@@ -2235,10 +3912,11 @@ async function runNextWorkItem({
|
|
|
2235
3912
|
projectId,
|
|
2236
3913
|
result.workItem.workItemId,
|
|
2237
3914
|
finalStatus,
|
|
2238
|
-
`run_${result.workItem.workItemId}_${
|
|
3915
|
+
`run_${result.workItem.workItemId}_${randomUUID2()}`,
|
|
2239
3916
|
runnerId,
|
|
2240
3917
|
{
|
|
2241
3918
|
tool: preview.toolName,
|
|
3919
|
+
...toolResult.model ? { model: toolResult.model } : {},
|
|
2242
3920
|
durationMs,
|
|
2243
3921
|
message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
2244
3922
|
sessionPolicy: sessionContext.policy,
|
|
@@ -2252,11 +3930,69 @@ async function runNextWorkItem({
|
|
|
2252
3930
|
...failureExcerpt ? { error: failureExcerpt } : {}
|
|
2253
3931
|
}
|
|
2254
3932
|
);
|
|
2255
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
3933
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2256
3934
|
const durationSeconds = Math.round(durationMs / 1e3);
|
|
2257
3935
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
2258
3936
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
2259
3937
|
}
|
|
3938
|
+
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
3939
|
+
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
3940
|
+
const command = commands.filter((item) => item.status === "pending" || item.status === "acknowledged" || item.status === "running").sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
|
|
3941
|
+
if (!command) {
|
|
3942
|
+
return { handled: false, succeeded: true };
|
|
3943
|
+
}
|
|
3944
|
+
await updateRunnerCommandStatus(apiClient, context, command, "acknowledged", "Command acknowledged by local runner.");
|
|
3945
|
+
await updateRunnerCommandStatus(apiClient, context, command, "running", `Running ${runnerCommandLabel(command.commandKind)} command.`);
|
|
3946
|
+
const result = await executeRunnerCommand(command, context);
|
|
3947
|
+
await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error);
|
|
3948
|
+
if (command.commandKind === "remove" && result.succeeded) {
|
|
3949
|
+
await new LocalCredentialStore().delete(credentialKey(context.accountId, context.projectId, context.repositoryLinkId));
|
|
3950
|
+
}
|
|
3951
|
+
if (result.stopRunner && command.commandKind === "remove") {
|
|
3952
|
+
await apiClient.sendRunnerHeartbeat(context.projectId, context.runnerId, context.repositoryLinkId, "offline", heartbeatMetadata).catch(() => void 0);
|
|
3953
|
+
}
|
|
3954
|
+
return { handled: true, succeeded: result.succeeded, message: result.message, ...result.stopRunner ? { stopRunner: true } : {} };
|
|
3955
|
+
}
|
|
3956
|
+
async function updateRunnerCommandStatus(apiClient, context, command, status, message, error) {
|
|
3957
|
+
const result = await apiClient.updateRunnerCommand(context.projectId, command.commandId, {
|
|
3958
|
+
runnerId: context.runnerId,
|
|
3959
|
+
repositoryLinkId: context.repositoryLinkId,
|
|
3960
|
+
status,
|
|
3961
|
+
idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
|
|
3962
|
+
message,
|
|
3963
|
+
...error ? { error } : {}
|
|
3964
|
+
});
|
|
3965
|
+
return result.command;
|
|
3966
|
+
}
|
|
3967
|
+
async function executeRunnerCommand(command, context) {
|
|
3968
|
+
if (command.commandKind === "remove") {
|
|
3969
|
+
return { succeeded: true, stopRunner: true, message: "Runner credential revoked. This local runner will stop after removing its stored credential." };
|
|
3970
|
+
}
|
|
3971
|
+
if (command.commandKind === "restart") {
|
|
3972
|
+
return restartCurrentRunner(context);
|
|
3973
|
+
}
|
|
3974
|
+
return runOfficialCliUpdate();
|
|
3975
|
+
}
|
|
3976
|
+
async function restartCurrentRunner(context) {
|
|
3977
|
+
if (currentRunnerMode() !== "background") {
|
|
3978
|
+
return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
|
|
3979
|
+
}
|
|
3980
|
+
const [metadata] = await listRunnerDaemonMetadata({ accountId: context.accountId, projectId: context.projectId, repositoryLinkId: context.repositoryLinkId, runnerId: context.runnerId });
|
|
3981
|
+
if (!metadata) {
|
|
3982
|
+
return { succeeded: false, message: "Background runner metadata was not found, so restart needs manual action." };
|
|
3983
|
+
}
|
|
3984
|
+
try {
|
|
3985
|
+
const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
|
|
3986
|
+
return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
return { succeeded: false, message: "Background restart failed.", error: errorMessage2(error) };
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
function runnerCommandLabel(commandKind) {
|
|
3992
|
+
if (commandKind === "update") return "update";
|
|
3993
|
+
if (commandKind === "restart") return "restart";
|
|
3994
|
+
return "remove";
|
|
3995
|
+
}
|
|
2260
3996
|
async function finalizeBrainGenerationWork({
|
|
2261
3997
|
apiClient,
|
|
2262
3998
|
durationMs,
|
|
@@ -2264,6 +4000,7 @@ async function finalizeBrainGenerationWork({
|
|
|
2264
4000
|
repositoryLinkId,
|
|
2265
4001
|
runnerId,
|
|
2266
4002
|
sessionContext,
|
|
4003
|
+
toolConfig,
|
|
2267
4004
|
toolName,
|
|
2268
4005
|
toolResult,
|
|
2269
4006
|
workItem
|
|
@@ -2302,34 +4039,48 @@ ${toolResult.stderr}`);
|
|
|
2302
4039
|
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
2303
4040
|
};
|
|
2304
4041
|
if (artifacts) {
|
|
4042
|
+
const completionMessage = workItem.workKind === "planRevision" ? `${toolName} returned a revised plan for review.` : `${toolName} generated ${artifacts.length} brain artifact${artifacts.length === 1 ? "" : "s"}.`;
|
|
2305
4043
|
const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
2306
4044
|
status: "completed",
|
|
2307
4045
|
runnerId,
|
|
2308
|
-
idempotencyKey: `generation_${workItem.workItemId}_${
|
|
4046
|
+
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
|
|
2309
4047
|
artifacts,
|
|
2310
4048
|
tool: toolName,
|
|
2311
4049
|
durationMs,
|
|
2312
4050
|
...sessionTelemetry,
|
|
2313
|
-
message:
|
|
4051
|
+
message: completionMessage
|
|
2314
4052
|
});
|
|
2315
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
2316
|
-
console.log(`Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
4053
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4054
|
+
console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
2317
4055
|
return { status: "completed", exitCode: 0 };
|
|
2318
4056
|
}
|
|
2319
4057
|
await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
2320
4058
|
status: "failed",
|
|
2321
4059
|
runnerId,
|
|
2322
|
-
idempotencyKey: `generation_${workItem.workItemId}_${
|
|
4060
|
+
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
|
|
2323
4061
|
tool: toolName,
|
|
2324
4062
|
durationMs,
|
|
2325
4063
|
...sessionTelemetry,
|
|
2326
4064
|
message: `${toolName} did not produce valid brain artifacts.`,
|
|
2327
4065
|
...generationError ? { error: generationError } : {}
|
|
2328
4066
|
});
|
|
2329
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
4067
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2330
4068
|
console.error(generationError ?? "Local runner generation failed.");
|
|
2331
4069
|
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
2332
4070
|
}
|
|
4071
|
+
async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
|
|
4072
|
+
if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
|
|
4073
|
+
return createWorkExecutionPrompt(workItem);
|
|
4074
|
+
}
|
|
4075
|
+
const [{ documents }, { messages }] = await Promise.all([
|
|
4076
|
+
apiClient.listBrainDocuments(projectId),
|
|
4077
|
+
apiClient.listPlanReviewMessages(projectId, workItem.reviewDocumentId)
|
|
4078
|
+
]);
|
|
4079
|
+
const planDocument = documents.find((document) => document.documentId === workItem.reviewDocumentId || document.id === workItem.reviewDocumentId);
|
|
4080
|
+
return createWorkExecutionPrompt(workItem, {
|
|
4081
|
+
...planDocument ? { planRevision: { planDocument, messages } } : {}
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
2333
4084
|
async function loadPairedApiContext(root, apiUrl) {
|
|
2334
4085
|
const metadata = await readProjectLink(root);
|
|
2335
4086
|
if (!metadata) {
|
|
@@ -2348,6 +4099,37 @@ async function loadPairedApiContext(root, apiUrl) {
|
|
|
2348
4099
|
})
|
|
2349
4100
|
};
|
|
2350
4101
|
}
|
|
4102
|
+
async function loadProjectNextAction(apiClient, projectId, repositoryLinkId, root) {
|
|
4103
|
+
const [{ workItems }, { documents }, { runners }] = await Promise.all([
|
|
4104
|
+
apiClient.listWorkItems(projectId),
|
|
4105
|
+
apiClient.listBrainDocuments(projectId),
|
|
4106
|
+
apiClient.listRunners(projectId).catch(() => ({ runners: [] }))
|
|
4107
|
+
]);
|
|
4108
|
+
return computeProjectNextAction({
|
|
4109
|
+
projectId,
|
|
4110
|
+
repositoryLinks: [createCliRepositoryLink({ amistioAccountId: "local_runner", amistioProjectId: projectId, repositoryLinkId, defaultBranch: "main", lastSyncedRevision: 0 }, root)],
|
|
4111
|
+
documents,
|
|
4112
|
+
workItems,
|
|
4113
|
+
runnerHeartbeats: runners
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
function createCliRepositoryLink(metadata, root) {
|
|
4117
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4118
|
+
return {
|
|
4119
|
+
id: metadata.repositoryLinkId,
|
|
4120
|
+
type: "repositoryLink",
|
|
4121
|
+
schemaVersion: 1,
|
|
4122
|
+
accountId: metadata.amistioAccountId,
|
|
4123
|
+
projectId: metadata.amistioProjectId,
|
|
4124
|
+
repositoryLinkId: metadata.repositoryLinkId,
|
|
4125
|
+
repoName: inferRepoName(root),
|
|
4126
|
+
repoFingerprint: createRepoFingerprint(metadata.amistioAccountId, metadata.amistioProjectId, metadata.repositoryLinkId),
|
|
4127
|
+
defaultBranch: metadata.defaultBranch,
|
|
4128
|
+
status: "active",
|
|
4129
|
+
createdAt: now,
|
|
4130
|
+
updatedAt: now
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
2351
4133
|
function selectPromptWorkItem(workItems, workItemId) {
|
|
2352
4134
|
if (workItemId) {
|
|
2353
4135
|
return workItems.find((item) => item.workItemId === workItemId || item.id === workItemId);
|
|
@@ -2366,6 +4148,7 @@ async function prepareToolSession({
|
|
|
2366
4148
|
supportsSessionReuse,
|
|
2367
4149
|
resumabilityScope,
|
|
2368
4150
|
toolName,
|
|
4151
|
+
model,
|
|
2369
4152
|
workItem
|
|
2370
4153
|
}) {
|
|
2371
4154
|
const { toolSessions } = await apiClient.listToolSessions(projectId);
|
|
@@ -2390,12 +4173,13 @@ async function prepareToolSession({
|
|
|
2390
4173
|
});
|
|
2391
4174
|
return { ...selection, toolSession: toolSession2 };
|
|
2392
4175
|
}
|
|
2393
|
-
const toolSessionId = `tool_session_${
|
|
4176
|
+
const toolSessionId = `tool_session_${randomUUID2()}`;
|
|
2394
4177
|
const { toolSession } = await apiClient.createToolSession(projectId, {
|
|
2395
4178
|
toolSessionId,
|
|
2396
4179
|
repositoryLinkId,
|
|
2397
4180
|
tool: toolName,
|
|
2398
4181
|
provider: toolName,
|
|
4182
|
+
...model ? { model } : {},
|
|
2399
4183
|
resumabilityScope: supportsSessionReuse ? resumabilityScope : "none",
|
|
2400
4184
|
title: workItem.title,
|
|
2401
4185
|
summary: selection.reason,
|
|
@@ -2471,18 +4255,30 @@ function truncateLogExcerpt(value) {
|
|
|
2471
4255
|
const trimmed = value.trim();
|
|
2472
4256
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
2473
4257
|
}
|
|
2474
|
-
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) {
|
|
2475
4265
|
const parsed = Number(value);
|
|
2476
4266
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2477
4267
|
throw new Error(`Expected a positive integer, received ${value}.`);
|
|
2478
4268
|
}
|
|
2479
4269
|
return parsed;
|
|
2480
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
|
+
}
|
|
2481
4277
|
function inferRepoName(root) {
|
|
2482
|
-
return
|
|
4278
|
+
return path12.basename(path12.resolve(root)) || "repository";
|
|
2483
4279
|
}
|
|
2484
4280
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
2485
|
-
return
|
|
4281
|
+
return createHash4("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
2486
4282
|
}
|
|
2487
4283
|
function defaultApiUrl() {
|
|
2488
4284
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -2497,6 +4293,124 @@ function formatApiUrlFlag(apiUrl) {
|
|
|
2497
4293
|
function formatShellArg(value) {
|
|
2498
4294
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
2499
4295
|
}
|
|
4296
|
+
async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, explicitModel, explicitTool, projectId, toolCommand }) {
|
|
4297
|
+
const capabilities = toRunnerToolCapabilities(await detectLocalTools());
|
|
4298
|
+
if (toolCommand) {
|
|
4299
|
+
return {
|
|
4300
|
+
ready: true,
|
|
4301
|
+
tool: explicitTool ?? "auto",
|
|
4302
|
+
capabilities,
|
|
4303
|
+
source: "cli",
|
|
4304
|
+
status: "custom",
|
|
4305
|
+
effectiveTool: "custom",
|
|
4306
|
+
requestedInvocationChannel: explicitInvocationChannel ?? "command",
|
|
4307
|
+
effectiveInvocationChannel: "command",
|
|
4308
|
+
...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
|
|
4309
|
+
...explicitModel ? { model: explicitModel } : {},
|
|
4310
|
+
message: "Using local custom tool command."
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
if (explicitTool === "none") {
|
|
4314
|
+
if (explicitModel) {
|
|
4315
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", model: explicitModel });
|
|
4316
|
+
}
|
|
4317
|
+
return { ready: true, tool: "none", capabilities, source: "cli", status: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", message: "No local tool selected." };
|
|
4318
|
+
}
|
|
4319
|
+
if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
|
|
4320
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...explicitModel ? { model: explicitModel } : {} });
|
|
4321
|
+
}
|
|
4322
|
+
const remotePreference = await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
|
|
4323
|
+
const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
|
|
4324
|
+
const requestedInvocationChannel = explicitInvocationChannel ?? remotePreference?.invocationChannel ?? "auto";
|
|
4325
|
+
const model = explicitModel ?? remotePreference?.model;
|
|
4326
|
+
const source = explicitTool || explicitInvocationChannel || explicitModel ? "cli" : remotePreference?.source ?? "default";
|
|
4327
|
+
return resolveRequestedTool({ capabilities, requestedInvocationChannel, requestedTool, source, ...model ? { model } : {} });
|
|
4328
|
+
}
|
|
4329
|
+
function resolveRequestedTool({ capabilities, model, requestedInvocationChannel, requestedTool, source }) {
|
|
4330
|
+
if (requestedTool === "auto") {
|
|
4331
|
+
const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!model || capability2.supportsModelSelection));
|
|
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";
|
|
4336
|
+
return unavailableToolConfig({
|
|
4337
|
+
capabilities,
|
|
4338
|
+
source,
|
|
4339
|
+
status,
|
|
4340
|
+
requestedTool,
|
|
4341
|
+
requestedInvocationChannel,
|
|
4342
|
+
tool: "auto",
|
|
4343
|
+
...model ? { model } : {},
|
|
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."
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: candidate.name, effectiveInvocationChannel: effectiveInvocationChannel(candidate, requestedInvocationChannel), ...model ? { model } : {} };
|
|
4348
|
+
}
|
|
4349
|
+
const capability = capabilities.find((candidate) => candidate.name === requestedTool);
|
|
4350
|
+
if (!capability?.available) {
|
|
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.` });
|
|
4355
|
+
}
|
|
4356
|
+
if (model && !capability.supportsModelSelection) {
|
|
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.` });
|
|
4358
|
+
}
|
|
4359
|
+
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveTool: requestedTool, effectiveInvocationChannel: effectiveInvocationChannel(capability, requestedInvocationChannel), ...model ? { model } : {} };
|
|
4360
|
+
}
|
|
4361
|
+
function unavailableToolConfig(input) {
|
|
4362
|
+
return {
|
|
4363
|
+
ready: false,
|
|
4364
|
+
tool: input.tool,
|
|
4365
|
+
capabilities: input.capabilities,
|
|
4366
|
+
source: input.source,
|
|
4367
|
+
status: input.status,
|
|
4368
|
+
message: input.message,
|
|
4369
|
+
...input.requestedTool ? { requestedTool: input.requestedTool } : {},
|
|
4370
|
+
...input.requestedInvocationChannel ? { requestedInvocationChannel: input.requestedInvocationChannel } : {},
|
|
4371
|
+
...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
|
|
4372
|
+
...input.effectiveInvocationChannel ? { effectiveInvocationChannel: input.effectiveInvocationChannel } : {},
|
|
4373
|
+
...input.model ? { model: input.model } : {}
|
|
4374
|
+
};
|
|
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
|
+
}
|
|
4385
|
+
function toRunnerToolCapabilities(tools) {
|
|
4386
|
+
return tools.map((tool) => ({
|
|
4387
|
+
name: tool.name,
|
|
4388
|
+
description: tool.description,
|
|
4389
|
+
available: tool.available,
|
|
4390
|
+
sdkAvailable: tool.sdkAvailable,
|
|
4391
|
+
commandAvailable: tool.commandAvailable,
|
|
4392
|
+
execution: tool.execution,
|
|
4393
|
+
supportsSessionReuse: tool.supportsSessionReuse,
|
|
4394
|
+
resumabilityScope: tool.resumabilityScope,
|
|
4395
|
+
supportsModelSelection: tool.supportsModelSelection
|
|
4396
|
+
}));
|
|
4397
|
+
}
|
|
4398
|
+
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
4399
|
+
return {
|
|
4400
|
+
version: CLI_VERSION,
|
|
4401
|
+
mode,
|
|
4402
|
+
hostname: os6.hostname(),
|
|
4403
|
+
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
4404
|
+
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
4405
|
+
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
4406
|
+
...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
|
|
4407
|
+
...toolConfig?.effectiveInvocationChannel ? { effectiveInvocationChannel: toolConfig.effectiveInvocationChannel } : {},
|
|
4408
|
+
...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
|
|
4409
|
+
...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
|
|
4410
|
+
...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
|
|
4411
|
+
...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
2500
4414
|
async function delay(milliseconds) {
|
|
2501
4415
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2502
4416
|
}
|