@kisev/skills-opencode 1.1.0 → 1.2.0
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 +28 -13
- package/dist/agent-profiles.js +3 -1
- package/dist/cli-output.d.ts +11 -0
- package/dist/cli-output.js +149 -0
- package/dist/cli.js +176 -58
- package/dist/index.d.ts +6 -1
- package/dist/index.js +13 -5
- package/dist/installer.js +1 -1
- package/dist/plugins/background-attempts.js +15 -4
- package/dist/plugins/goal-loop.d.ts +11 -0
- package/dist/plugins/goal-loop.js +12 -0
- package/dist/routing.d.ts +59 -2
- package/dist/routing.js +147 -7
- package/dist/runtime/state.d.ts +1 -0
- package/dist/runtime/state.js +12 -5
- package/dist/terminal-wizard.d.ts +2 -0
- package/dist/terminal-wizard.js +125 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx --yes skills add kisev/skills --agent opencode --skill '*' --copy --yes
|
|
|
16
16
|
|
|
17
17
|
Для одного skill укажите `--skill <name>`. Для воспроизводимой установки можно
|
|
18
18
|
передать URL GitHub tag, например
|
|
19
|
-
`https://github.com/kisev/skills/tree/v1.1.
|
|
19
|
+
`https://github.com/kisev/skills/tree/v1.1.1`. Package никогда не устанавливает
|
|
20
20
|
и не обновляет skills. Если команда не нашла skill, она сообщает точную команду
|
|
21
21
|
`npx skills add` для его установки.
|
|
22
22
|
|
|
@@ -25,16 +25,18 @@ npx --yes skills add kisev/skills --agent opencode --skill '*' --copy --yes
|
|
|
25
25
|
Установите npm package там, где OpenCode сможет разрешить plugin:
|
|
26
26
|
|
|
27
27
|
```shell
|
|
28
|
-
npm install @kisev/skills-opencode@1.1.
|
|
28
|
+
npm install @kisev/skills-opencode@1.1.1
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Сначала покажите план installer.
|
|
31
|
+
Сначала покажите план installer. По умолчанию CLI выводит короткую сводку:
|
|
32
|
+
счётчики по группам, только изменяемые paths, conflicts, restart flag, digest и
|
|
33
|
+
готовую confirm-команду. Эта команда не меняет deployment:
|
|
32
34
|
|
|
33
35
|
```shell
|
|
34
36
|
npm exec -- skills-opencode install --scope global --dry-run
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Проверьте
|
|
39
|
+
Проверьте сводку и примените только показанный digest:
|
|
38
40
|
|
|
39
41
|
```shell
|
|
40
42
|
npm exec -- skills-opencode install --scope global --confirm <digest>
|
|
@@ -54,6 +56,14 @@ Project assets находятся в `.opencode/agents`, `.opencode/commands` и
|
|
|
54
56
|
перезаписывает неизвестные или изменённые files и сохраняет ownership manifests
|
|
55
57
|
только после confirmed apply.
|
|
56
58
|
|
|
59
|
+
Для automation добавьте `--json`. Этот режим сохраняет полный стабильный
|
|
60
|
+
machine-readable plan, включая `operations` и `requires_restart`:
|
|
61
|
+
|
|
62
|
+
```shell
|
|
63
|
+
npm exec -- skills-opencode install --scope global --dry-run --json
|
|
64
|
+
npm exec -- skills-opencode agent list --scope global --json
|
|
65
|
+
```
|
|
66
|
+
|
|
57
67
|
Добавьте plugin в `opencode.json` вручную:
|
|
58
68
|
|
|
59
69
|
```json
|
|
@@ -81,10 +91,14 @@ npm exec -- skills-opencode agent model-set worker --scope global \
|
|
|
81
91
|
npm exec -- skills-opencode agent reconcile --scope global --dry-run
|
|
82
92
|
```
|
|
83
93
|
|
|
84
|
-
`agent configure` предлагает terminal
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
`agent configure` предлагает настоящий terminal wizard: стрелками выбираются
|
|
95
|
+
agent из inventory, затем provider, только его models и, если metadata выбранной
|
|
96
|
+
модели публикует variants, variant. Текущие model/variant и target показываются
|
|
97
|
+
перед выбором; доступны `keep`, `change`, `clear variant`, `back` и `cancel`.
|
|
98
|
+
Wizard не вызывает LLM, OpenCode Question или refresh catalog. Если catalog
|
|
99
|
+
недоступен, он завершается без записи и печатает инструкцию для exact
|
|
100
|
+
`--model <provider/model>` с optional `--variant`. Для модели без variants
|
|
101
|
+
дополнительный selector не показывается.
|
|
88
102
|
|
|
89
103
|
Additional critic имеет имя `critic-<safe-suffix>`. Стандартный `critic` и fixed
|
|
90
104
|
roles нельзя удалить или переименовать:
|
|
@@ -95,11 +109,12 @@ npm exec -- skills-opencode critic add security --scope global \
|
|
|
95
109
|
npm exec -- skills-opencode critic remove security --scope global --dry-run
|
|
96
110
|
```
|
|
97
111
|
|
|
98
|
-
Для любой mutation
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
Для любой mutation используйте готовую confirm-команду из preview либо замените
|
|
113
|
+
`--dry-run` на `--confirm <digest>` и повторите те же аргументы. Человекочитаемый
|
|
114
|
+
plan не печатает полный JSON и сворачивает длинные группы paths. Digest связан с
|
|
115
|
+
одноразовым private receipt, действует 10 минут и повторно не применяется. Для
|
|
116
|
+
machine-readable result добавьте `--json`; поле `requires_restart` сообщает о
|
|
117
|
+
необходимости полностью перезапустить OpenCode.
|
|
103
118
|
|
|
104
119
|
В global scope profile configuration хранится в
|
|
105
120
|
`~/.config/opencode/.skills-opencode/agent-profiles.json`, а semantic deployment
|
package/dist/agent-profiles.js
CHANGED
|
@@ -719,10 +719,12 @@ export async function availableModelVariants(model) {
|
|
|
719
719
|
document += `${lines[index]}\n`;
|
|
720
720
|
try {
|
|
721
721
|
const metadata = JSON.parse(document);
|
|
722
|
+
if (metadata.variants === undefined)
|
|
723
|
+
return [];
|
|
722
724
|
if (!metadata.variants ||
|
|
723
725
|
typeof metadata.variants !== "object" ||
|
|
724
726
|
Array.isArray(metadata.variants))
|
|
725
|
-
throw new Error("variants
|
|
727
|
+
throw new Error("invalid variants metadata");
|
|
726
728
|
const variants = Object.keys(metadata.variants);
|
|
727
729
|
if (!variants.every((variant) => VARIANT_PATTERN.test(variant)))
|
|
728
730
|
throw new Error("unsafe variant");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentInventory, AgentProfilePlan } from "./agent-profiles.js";
|
|
2
|
+
import type { Plan as InstallerPlan } from "./installer.js";
|
|
3
|
+
type DisplayPlan = InstallerPlan | AgentProfilePlan;
|
|
4
|
+
export declare function terminalSafe(value: string): string;
|
|
5
|
+
export declare function renderPlan(plan: DisplayPlan, options: {
|
|
6
|
+
applied: boolean;
|
|
7
|
+
confirmationCommand?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function renderInventory(inventory: AgentInventory): string;
|
|
10
|
+
export declare function shellCommand(arguments_: readonly string[]): string;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const GROUPS = ["Agents", "Commands", "Plugins", "State"];
|
|
2
|
+
const OPERATIONS = ["create", "update", "remove", "conflict", "missing", "unchanged"];
|
|
3
|
+
const UNSAFE_TERMINAL_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
4
|
+
export function terminalSafe(value) {
|
|
5
|
+
return [...value]
|
|
6
|
+
.map((character) => {
|
|
7
|
+
const code = character.codePointAt(0);
|
|
8
|
+
const unsafe = character === "\\" || UNSAFE_TERMINAL_CHARACTER.test(character);
|
|
9
|
+
if (!unsafe)
|
|
10
|
+
return character;
|
|
11
|
+
if (character === "\\")
|
|
12
|
+
return "\\\\";
|
|
13
|
+
if (character === "\n")
|
|
14
|
+
return "\\n";
|
|
15
|
+
if (character === "\r")
|
|
16
|
+
return "\\r";
|
|
17
|
+
if (character === "\t")
|
|
18
|
+
return "\\t";
|
|
19
|
+
if (code <= 0xff)
|
|
20
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
21
|
+
if (code <= 0xffff)
|
|
22
|
+
return `\\u${code.toString(16).padStart(4, "0")}`;
|
|
23
|
+
return `\\u{${code.toString(16)}}`;
|
|
24
|
+
})
|
|
25
|
+
.join("");
|
|
26
|
+
}
|
|
27
|
+
function groupFor(path) {
|
|
28
|
+
if (path.startsWith("agents/"))
|
|
29
|
+
return "Agents";
|
|
30
|
+
if (path.startsWith("commands/"))
|
|
31
|
+
return "Commands";
|
|
32
|
+
if (path.startsWith("plugins/"))
|
|
33
|
+
return "Plugins";
|
|
34
|
+
return "State";
|
|
35
|
+
}
|
|
36
|
+
function actionName(action) {
|
|
37
|
+
return {
|
|
38
|
+
install: "Install",
|
|
39
|
+
uninstall: "Uninstall",
|
|
40
|
+
"model-set": "Configure agent model",
|
|
41
|
+
"critic-add": "Add critic",
|
|
42
|
+
"critic-remove": "Remove critic",
|
|
43
|
+
reconcile: "Reconcile agents",
|
|
44
|
+
}[action];
|
|
45
|
+
}
|
|
46
|
+
function operationSummary(operations) {
|
|
47
|
+
return OPERATIONS.map((operation) => {
|
|
48
|
+
const count = operations.filter((item) => item.operation === operation).length;
|
|
49
|
+
return count ? `${operation} ${count}` : undefined;
|
|
50
|
+
})
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join(", ");
|
|
53
|
+
}
|
|
54
|
+
function shortPath(path, group) {
|
|
55
|
+
if (group === "State")
|
|
56
|
+
return terminalSafe(path);
|
|
57
|
+
const value = path.slice(path.indexOf("/") + 1);
|
|
58
|
+
return terminalSafe(value.endsWith(".md") || value.endsWith(".js") ? value.slice(0, -3) : value);
|
|
59
|
+
}
|
|
60
|
+
function detailLines(operations) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
for (const group of GROUPS) {
|
|
63
|
+
const grouped = operations.filter((item) => groupFor(item.path) === group && item.operation !== "unchanged");
|
|
64
|
+
for (const operation of OPERATIONS.filter((value) => value !== "unchanged" && grouped.some((item) => item.operation === value))) {
|
|
65
|
+
const values = grouped
|
|
66
|
+
.filter((item) => item.operation === operation)
|
|
67
|
+
.map((item) => operation === "conflict"
|
|
68
|
+
? `${shortPath(item.path, group)} (${terminalSafe(item.reason ?? "conflict")})`
|
|
69
|
+
: shortPath(item.path, group));
|
|
70
|
+
const visible = operation === "conflict" ? values : values.slice(0, 8);
|
|
71
|
+
const rest = values.length - visible.length;
|
|
72
|
+
lines.push(` ${group}/${operation}: ${visible.join(", ")}${rest ? ` (+${rest} more)` : ""}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
function migrationSummary(operations) {
|
|
78
|
+
const count = operations.filter((item) => item.reason === "v1.0.0 ownership transfer").length;
|
|
79
|
+
return count
|
|
80
|
+
? ` Ownership migration: ${count} agent${count === 1 ? "" : "s"} from v1.0.0`
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
export function renderPlan(plan, options) {
|
|
84
|
+
const version = "package_version" in plan ? ` @kisev/skills-opencode ${plan.package_version}` : "";
|
|
85
|
+
const lines = [
|
|
86
|
+
`${actionName(plan.action)}${version} (${plan.scope})`,
|
|
87
|
+
`Target: ${terminalSafe(plan.root)}`,
|
|
88
|
+
"",
|
|
89
|
+
options.applied ? "Applied changes:" : "Planned changes:",
|
|
90
|
+
];
|
|
91
|
+
for (const group of GROUPS) {
|
|
92
|
+
const operations = plan.operations.filter((item) => groupFor(item.path) === group);
|
|
93
|
+
if (operations.length)
|
|
94
|
+
lines.push(` ${group}: ${operationSummary(operations)}`);
|
|
95
|
+
}
|
|
96
|
+
const migration = migrationSummary(plan.operations);
|
|
97
|
+
if (migration)
|
|
98
|
+
lines.push(migration);
|
|
99
|
+
const details = detailLines(plan.operations);
|
|
100
|
+
if (details.length)
|
|
101
|
+
lines.push("", "Details:", ...details);
|
|
102
|
+
const conflicts = plan.operations.filter((item) => item.operation === "conflict").length;
|
|
103
|
+
lines.push("", `Conflicts: ${conflicts || "none"}`);
|
|
104
|
+
lines.push(options.applied
|
|
105
|
+
? `Restart required: ${plan.requires_restart ? "yes" : "no"}`
|
|
106
|
+
: `Restart after apply: ${plan.requires_restart ? "yes" : "no"}`);
|
|
107
|
+
if (!options.applied) {
|
|
108
|
+
if (plan.receipt_expires_at)
|
|
109
|
+
lines.push(`Confirmation expires: ${plan.receipt_expires_at}`);
|
|
110
|
+
lines.push(`Digest: ${plan.digest}`);
|
|
111
|
+
if (options.confirmationCommand)
|
|
112
|
+
lines.push("", "Apply:", ` ${options.confirmationCommand}`);
|
|
113
|
+
}
|
|
114
|
+
return `${lines.join("\n")}\n`;
|
|
115
|
+
}
|
|
116
|
+
function table(rows) {
|
|
117
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
|
|
118
|
+
return rows.map((row) => row
|
|
119
|
+
.map((value, column) => value.padEnd(widths[column]))
|
|
120
|
+
.join(" ")
|
|
121
|
+
.trimEnd());
|
|
122
|
+
}
|
|
123
|
+
export function renderInventory(inventory) {
|
|
124
|
+
const rows = [
|
|
125
|
+
["NAME", "MODEL", "VARIANT", "OWNER", "STATE"],
|
|
126
|
+
...inventory.profiles.map((profile) => [
|
|
127
|
+
terminalSafe(profile.name),
|
|
128
|
+
terminalSafe(profile.model ?? "default"),
|
|
129
|
+
terminalSafe(profile.variant ?? "-"),
|
|
130
|
+
profile.ownership,
|
|
131
|
+
profile.state,
|
|
132
|
+
]),
|
|
133
|
+
];
|
|
134
|
+
return `${[
|
|
135
|
+
`OpenCode agents (${inventory.scope})`,
|
|
136
|
+
`Target: ${terminalSafe(inventory.root)}`,
|
|
137
|
+
"",
|
|
138
|
+
...table(rows),
|
|
139
|
+
"",
|
|
140
|
+
`Critic pool: ${inventory.critic_pool.map(terminalSafe).join(", ")}`,
|
|
141
|
+
`Collisions: ${inventory.collisions.length ? inventory.collisions.map(terminalSafe).join(", ") : "none"}`,
|
|
142
|
+
`Drift: ${inventory.drift.length ? inventory.drift.map(terminalSafe).join(", ") : "none"}`,
|
|
143
|
+
].join("\n")}\n`;
|
|
144
|
+
}
|
|
145
|
+
export function shellCommand(arguments_) {
|
|
146
|
+
return ["npm", "exec", "--", "skills-opencode", ...arguments_]
|
|
147
|
+
.map((value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`)
|
|
148
|
+
.join(" ");
|
|
149
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
3
2
|
import { AgentProfileError, applyAgentProfileChange, availableModels, availableModelVariants, FIXED_AGENT_ROLES, listAgentProfiles, previewAgentProfileChange, validateAgentName, validateModel, validateVariant, } from "./agent-profiles.js";
|
|
4
|
-
import {
|
|
3
|
+
import { renderInventory, renderPlan, shellCommand, terminalSafe } from "./cli-output.js";
|
|
4
|
+
import { apply, InstallerError, preview } from "./installer.js";
|
|
5
5
|
import { LifecycleError } from "./lifecycle.js";
|
|
6
|
+
import { promptText, selectOption } from "./terminal-wizard.js";
|
|
6
7
|
function parseOptions(values) {
|
|
7
|
-
const options = { dryRun: false };
|
|
8
|
+
const options = { dryRun: false, json: false };
|
|
8
9
|
for (let index = 0; index < values.length; index += 1) {
|
|
9
10
|
const value = values[index];
|
|
10
11
|
if (!value.startsWith("--")) {
|
|
@@ -52,6 +53,11 @@ function parseOptions(values) {
|
|
|
52
53
|
throw new InstallerError("invalid_input", "Use only one variant option");
|
|
53
54
|
options.variant = null;
|
|
54
55
|
}
|
|
56
|
+
else if (value === "--json") {
|
|
57
|
+
if (options.json)
|
|
58
|
+
throw new InstallerError("invalid_input", "--json may be supplied once");
|
|
59
|
+
options.json = true;
|
|
60
|
+
}
|
|
55
61
|
else {
|
|
56
62
|
throw new InstallerError("invalid_input", `Unknown argument: ${value}`);
|
|
57
63
|
}
|
|
@@ -77,68 +83,156 @@ function exactModel(options) {
|
|
|
77
83
|
throw new InstallerError("invalid_input", "--provider is required when --model is not provider/model");
|
|
78
84
|
return validateModel(`${options.provider}/${options.model}`);
|
|
79
85
|
}
|
|
80
|
-
async function
|
|
81
|
-
process.
|
|
82
|
-
const answer = await input.question("> ");
|
|
83
|
-
const index = Number(answer) - 1;
|
|
84
|
-
if (!Number.isSafeInteger(index) || !values[index])
|
|
85
|
-
throw new InstallerError("invalid_input", `Invalid ${label.toLowerCase()} selection`);
|
|
86
|
-
return values[index];
|
|
87
|
-
}
|
|
88
|
-
async function interactiveSelection(options) {
|
|
89
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
86
|
+
async function interactiveSelection(options, action = "model-set") {
|
|
87
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
90
88
|
throw new InstallerError("terminal_required", "agent configure requires a terminal or explicit --provider and --model");
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
}
|
|
90
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
91
|
+
const configurable = inventory.profiles.filter((item) => item.ownership !== "user-owned");
|
|
92
|
+
let name;
|
|
93
|
+
if (options.name) {
|
|
94
|
+
name = action === "critic-add" && !options.name.startsWith("critic-")
|
|
95
|
+
? validateAgentName(`critic-${options.name}`)
|
|
96
|
+
: validateAgentName(options.name);
|
|
97
|
+
}
|
|
98
|
+
else if (action === "critic-add") {
|
|
99
|
+
const raw = await promptText("Critic name (critic-<suffix>):");
|
|
100
|
+
if (raw === null)
|
|
101
|
+
throw new InstallerError("cancelled", "Wizard cancelled");
|
|
102
|
+
name = raw.startsWith("critic-") ? raw : `critic-${raw}`;
|
|
103
|
+
validateAgentName(name);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const agentNames = configurable.map((item) => item.name);
|
|
107
|
+
if (agentNames.length === 0)
|
|
108
|
+
agentNames.push(...FIXED_AGENT_ROLES);
|
|
109
|
+
const index = await selectOption("Select agent:", agentNames);
|
|
110
|
+
if (index === null)
|
|
111
|
+
throw new InstallerError("cancelled", "Wizard cancelled");
|
|
112
|
+
name = agentNames[index];
|
|
113
|
+
}
|
|
114
|
+
const currentProfile = configurable.find((item) => item.name === name);
|
|
115
|
+
const currentModel = currentProfile?.model;
|
|
116
|
+
const currentVariant = currentProfile?.variant;
|
|
117
|
+
const showTarget = (model, variant) => {
|
|
118
|
+
process.stderr.write(`Target: ${model}${variant ? ` / ${variant}` : ""}\n`);
|
|
119
|
+
};
|
|
120
|
+
if (options.provider && options.model) {
|
|
121
|
+
const model = exactModel(options);
|
|
122
|
+
const variant = validateVariant(options.variant);
|
|
123
|
+
return { name, model, ...(variant ? { variant } : {}) };
|
|
124
|
+
}
|
|
125
|
+
while (true) {
|
|
126
|
+
const actions = [];
|
|
127
|
+
if (currentModel) {
|
|
128
|
+
actions.push(`Keep current (${currentModel}${currentVariant ? ` / ${currentVariant}` : ""})`);
|
|
129
|
+
}
|
|
130
|
+
actions.push("Change model");
|
|
131
|
+
if (currentVariant)
|
|
132
|
+
actions.push("Clear variant");
|
|
133
|
+
if (!options.name && action === "model-set")
|
|
134
|
+
actions.push("Back");
|
|
135
|
+
actions.push("Cancel");
|
|
136
|
+
const currentLabel = `Agent: ${name}${currentModel ? ` (current: ${currentModel}${currentVariant ? ` / ${currentVariant}` : ""})` : ""}`;
|
|
137
|
+
const actionIndex = await selectOption(currentLabel, actions);
|
|
138
|
+
if (actionIndex === null)
|
|
139
|
+
throw new InstallerError("cancelled", "Wizard cancelled");
|
|
140
|
+
const chosen = actions[actionIndex];
|
|
141
|
+
if (chosen.startsWith("Keep current")) {
|
|
142
|
+
if (!currentModel)
|
|
143
|
+
throw new InstallerError("invalid_state", "No current model to keep");
|
|
144
|
+
showTarget(currentModel, currentVariant);
|
|
145
|
+
return { name, model: currentModel, ...(currentVariant ? { variant: currentVariant } : {}) };
|
|
146
|
+
}
|
|
147
|
+
if (chosen === "Change model") {
|
|
148
|
+
let models;
|
|
149
|
+
try {
|
|
150
|
+
models = await availableModels();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof AgentProfileError && error.code === "catalog_unavailable") {
|
|
154
|
+
process.stderr.write("\nModel catalog is unavailable.\nUse direct CLI with exact --model provider/model and optional --variant.\n\n");
|
|
155
|
+
throw new InstallerError("catalog_unavailable", "Use direct CLI with exact --model provider/model");
|
|
156
|
+
}
|
|
104
157
|
throw error;
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
}
|
|
159
|
+
const providers = [...new Set(models.map((m) => m.split("/", 1)[0]))].sort();
|
|
160
|
+
const providerIndex = await selectOption("Select provider:", providers);
|
|
161
|
+
if (providerIndex === null)
|
|
162
|
+
continue;
|
|
163
|
+
const provider = providers[providerIndex];
|
|
164
|
+
const filteredModels = models.filter((m) => m.startsWith(`${provider}/`));
|
|
165
|
+
const modelIndex = await selectOption("Select model:", filteredModels);
|
|
166
|
+
if (modelIndex === null)
|
|
167
|
+
continue;
|
|
168
|
+
const selectedModel = filteredModels[modelIndex];
|
|
169
|
+
let selectedVariant;
|
|
170
|
+
try {
|
|
171
|
+
const variants = await availableModelVariants(selectedModel);
|
|
172
|
+
if (variants.length) {
|
|
173
|
+
const variantOptions = ["(none)", ...variants];
|
|
174
|
+
const variantIndex = await selectOption("Select variant:", variantOptions);
|
|
175
|
+
if (variantIndex === null)
|
|
176
|
+
continue;
|
|
177
|
+
if (variantIndex > 0)
|
|
178
|
+
selectedVariant = variantOptions[variantIndex];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof AgentProfileError && error.code === "catalog_unavailable") {
|
|
183
|
+
process.stderr.write("\nModel variant metadata is unavailable.\nUse direct CLI with exact --model provider/model and optional --variant.\n\n");
|
|
184
|
+
throw new InstallerError("catalog_unavailable", "Use direct CLI with exact --model provider/model");
|
|
185
|
+
}
|
|
121
186
|
throw error;
|
|
187
|
+
}
|
|
188
|
+
showTarget(selectedModel, selectedVariant);
|
|
189
|
+
return { name, model: selectedModel, ...(selectedVariant ? { variant: selectedVariant } : {}) };
|
|
190
|
+
}
|
|
191
|
+
if (chosen === "Clear variant") {
|
|
192
|
+
if (!currentModel)
|
|
193
|
+
throw new InstallerError("invalid_state", "No model to clear variant for");
|
|
194
|
+
showTarget(currentModel);
|
|
195
|
+
return { name, model: currentModel };
|
|
196
|
+
}
|
|
197
|
+
if (chosen === "Back") {
|
|
198
|
+
return interactiveSelection({ ...options, name: undefined }, action);
|
|
199
|
+
}
|
|
200
|
+
if (chosen === "Cancel") {
|
|
201
|
+
throw new InstallerError("cancelled", "Wizard cancelled");
|
|
122
202
|
}
|
|
123
|
-
const variant = options.variant === null
|
|
124
|
-
? undefined
|
|
125
|
-
: options.variant ?? (variants.length ? await choose("Variant", ["none", ...variants.filter((value) => value !== "none")], input) : (await input.question("Variant (optional): ")).trim());
|
|
126
|
-
return { name, model: selectedModel, ...(variant && variant !== "none" ? { variant: validateVariant(variant) } : {}) };
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
input.close();
|
|
130
203
|
}
|
|
131
204
|
}
|
|
132
205
|
async function runProfile(request, options) {
|
|
133
206
|
requireConfirmationMode(options);
|
|
134
207
|
if (options.dryRun) {
|
|
135
208
|
const plan = await previewAgentProfileChange(request, options.scope);
|
|
136
|
-
|
|
209
|
+
if (options.json)
|
|
210
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: false, plan }, null, 2)}\n`);
|
|
211
|
+
else
|
|
212
|
+
process.stdout.write(renderPlan(plan, { applied: false, confirmationCommand: shellCommand(profileConfirmationArguments(request, options.scope, plan.digest)) }));
|
|
137
213
|
}
|
|
138
214
|
else {
|
|
139
|
-
|
|
215
|
+
const applied = await applyAgentProfileChange(request, options.scope, options.confirm);
|
|
216
|
+
if (options.json)
|
|
217
|
+
process.stdout.write(`${JSON.stringify(applied, null, 2)}\n`);
|
|
218
|
+
else
|
|
219
|
+
process.stdout.write(renderPlan(applied.plan, { applied: true }));
|
|
140
220
|
}
|
|
141
221
|
}
|
|
222
|
+
function profileConfirmationArguments(request, scope, digest) {
|
|
223
|
+
if (request.action === "reconcile")
|
|
224
|
+
return ["agent", "reconcile", "--scope", scope, "--confirm", digest];
|
|
225
|
+
if (request.action === "critic-remove")
|
|
226
|
+
return ["critic", "remove", request.name, "--scope", scope, "--confirm", digest];
|
|
227
|
+
const command = request.action === "critic-add" ? ["critic", "add", request.name] : ["agent", "model-set", request.name];
|
|
228
|
+
command.push("--scope", scope, "--model", request.model);
|
|
229
|
+
if (request.variant)
|
|
230
|
+
command.push("--variant", request.variant);
|
|
231
|
+
else if (request.action === "model-set")
|
|
232
|
+
command.push("--clear-variant");
|
|
233
|
+
command.push("--confirm", digest);
|
|
234
|
+
return command;
|
|
235
|
+
}
|
|
142
236
|
async function run(arguments_) {
|
|
143
237
|
const [domain, operation, ...rest] = arguments_;
|
|
144
238
|
if (domain === "install" || domain === "uninstall") {
|
|
@@ -151,14 +245,31 @@ async function run(arguments_) {
|
|
|
151
245
|
throw new InstallerError("invalid_input", "Installer accepts only scope and confirmation options");
|
|
152
246
|
requireConfirmationMode(options);
|
|
153
247
|
const action = domain;
|
|
154
|
-
|
|
248
|
+
if (options.dryRun) {
|
|
249
|
+
const plan = await preview(action, options.scope);
|
|
250
|
+
if (options.json)
|
|
251
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: false, plan }, null, 2)}\n`);
|
|
252
|
+
else
|
|
253
|
+
process.stdout.write(renderPlan(plan, { applied: false, confirmationCommand: shellCommand([action, "--scope", options.scope, "--confirm", plan.digest]) }));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const plan = await apply(action, options.scope, options.confirm);
|
|
257
|
+
if (options.json)
|
|
258
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", applied: true, requires_restart: plan.requires_restart, plan }, null, 2)}\n`);
|
|
259
|
+
else
|
|
260
|
+
process.stdout.write(renderPlan(plan, { applied: true }));
|
|
261
|
+
}
|
|
155
262
|
return;
|
|
156
263
|
}
|
|
157
264
|
if (domain === "agent" && operation === "list") {
|
|
158
265
|
const options = parseOptions(rest);
|
|
159
266
|
if (options.dryRun || options.confirm || options.name || options.provider || options.model || options.variant !== undefined)
|
|
160
267
|
throw new InstallerError("invalid_input", "agent list accepts only --scope");
|
|
161
|
-
|
|
268
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
269
|
+
if (options.json)
|
|
270
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", inventory }, null, 2)}\n`);
|
|
271
|
+
else
|
|
272
|
+
process.stdout.write(renderInventory(inventory));
|
|
162
273
|
return;
|
|
163
274
|
}
|
|
164
275
|
if (domain === "agent" && operation === "configure") {
|
|
@@ -166,7 +277,7 @@ async function run(arguments_) {
|
|
|
166
277
|
requireConfirmationMode(options);
|
|
167
278
|
const selected = options.provider && options.model && options.name
|
|
168
279
|
? { name: validateAgentName(options.name), model: exactModel(options), ...(validateVariant(options.variant) ? { variant: validateVariant(options.variant) } : {}) }
|
|
169
|
-
: await interactiveSelection(options);
|
|
280
|
+
: await interactiveSelection(options, "model-set");
|
|
170
281
|
await runProfile({ action: "model-set", ...selected, variant: selected.variant ?? null }, options);
|
|
171
282
|
return;
|
|
172
283
|
}
|
|
@@ -193,9 +304,13 @@ async function run(arguments_) {
|
|
|
193
304
|
: `critic-${options.name ?? ""}`;
|
|
194
305
|
if (operation === "add") {
|
|
195
306
|
const model = exactModel(options);
|
|
196
|
-
if (!model)
|
|
197
|
-
|
|
198
|
-
|
|
307
|
+
if (!model) {
|
|
308
|
+
const selected = await interactiveSelection(options, "critic-add");
|
|
309
|
+
await runProfile({ action: "critic-add", name: selected.name, model: selected.model, variant: selected.variant ?? null }, options);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
await runProfile({ action: "critic-add", name, model, variant: options.variant ?? null }, options);
|
|
313
|
+
}
|
|
199
314
|
}
|
|
200
315
|
else {
|
|
201
316
|
if (options.provider || options.model || options.variant !== undefined)
|
|
@@ -212,7 +327,10 @@ async function main() {
|
|
|
212
327
|
}
|
|
213
328
|
catch (error) {
|
|
214
329
|
const known = error instanceof LifecycleError ? error : new InstallerError("internal_error", error instanceof Error ? error.message : String(error));
|
|
215
|
-
process.
|
|
330
|
+
if (process.argv.includes("--json"))
|
|
331
|
+
process.stdout.write(`${JSON.stringify({ status: "error", error: { code: known.code, message: known.message } })}\n`);
|
|
332
|
+
else
|
|
333
|
+
process.stderr.write(`Error [${known.code}]: ${terminalSafe(known.message)}\n`);
|
|
216
334
|
process.exitCode = 2;
|
|
217
335
|
}
|
|
218
336
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ import rtk, { type RtkOptions } from "./plugins/rtk.js";
|
|
|
7
7
|
import zedBell, { type ZedBellOptions } from "./plugins/zed-bell.js";
|
|
8
8
|
import zedClickablePaths, { type ZedClickablePathsOptions } from "./plugins/zed-clickable-paths.js";
|
|
9
9
|
export { COMMAND_REGISTRY, renderCommand } from "./registry.js";
|
|
10
|
-
export { CATEGORIES, resolveRouting, RoutingGate } from "./routing.js";
|
|
10
|
+
export { CATEGORIES, resolveRouting, RoutingGate, ExecutionCardLifecycle, validateExecutionCard } from "./routing.js";
|
|
11
|
+
export type { ExecutionCard, ExecutionCardStatus } from "./routing.js";
|
|
11
12
|
export { AgentProfileError, FIXED_AGENT_ROLES, applyAgentProfileChange, availableModels, availableModelVariants, listAgentProfiles, previewAgentProfileChange, renderAgentProfile, validateAgentName, validateModel, validateVariant, } from "./agent-profiles.js";
|
|
12
13
|
export type { AgentInventory, AgentModelSelection, AgentOwnership, AgentProfileAction, AgentProfileConfig, AgentProfileOperation, AgentProfilePlan, AgentProfileRecord, AgentProfileRequest, AgentProfileResult, AgentProfileScope, AgentState, DeploymentManifest, DeploymentRecord, FixedAgentRole, } from "./agent-profiles.js";
|
|
13
14
|
export { backgroundAttempts, goalLoop, scheduler, autonomyPolicy, rulesInjector, rtk, zedBell, zedClickablePaths };
|
|
@@ -48,6 +49,7 @@ declare const plugin: (input: {
|
|
|
48
49
|
capabilities: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
49
50
|
tools: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
50
51
|
}, import("zod/v4/core").$strip>>;
|
|
52
|
+
execution_card: import("zod").ZodOptional<import("zod").ZodAny>;
|
|
51
53
|
override: import("zod").ZodOptional<import("zod").ZodString>;
|
|
52
54
|
budget: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
53
55
|
cost_class: import("zod").ZodOptional<import("zod").ZodString>;
|
|
@@ -66,6 +68,7 @@ declare const plugin: (input: {
|
|
|
66
68
|
capabilities?: string[] | undefined;
|
|
67
69
|
tools?: string[] | undefined;
|
|
68
70
|
}[];
|
|
71
|
+
execution_card?: any;
|
|
69
72
|
override?: string | undefined;
|
|
70
73
|
budget?: {
|
|
71
74
|
cost_class?: string | undefined;
|
|
@@ -151,6 +154,7 @@ export declare const server: (input: {
|
|
|
151
154
|
capabilities: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
152
155
|
tools: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
153
156
|
}, import("zod/v4/core").$strip>>;
|
|
157
|
+
execution_card: import("zod").ZodOptional<import("zod").ZodAny>;
|
|
154
158
|
override: import("zod").ZodOptional<import("zod").ZodString>;
|
|
155
159
|
budget: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
156
160
|
cost_class: import("zod").ZodOptional<import("zod").ZodString>;
|
|
@@ -169,6 +173,7 @@ export declare const server: (input: {
|
|
|
169
173
|
capabilities?: string[] | undefined;
|
|
170
174
|
tools?: string[] | undefined;
|
|
171
175
|
}[];
|
|
176
|
+
execution_card?: any;
|
|
172
177
|
override?: string | undefined;
|
|
173
178
|
budget?: {
|
|
174
179
|
cost_class?: string | undefined;
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,14 @@ import zedBell from "./plugins/zed-bell.js";
|
|
|
10
10
|
import zedClickablePaths from "./plugins/zed-clickable-paths.js";
|
|
11
11
|
import { applyAgentProfileChange, listAgentProfiles, previewAgentProfileChange, } from "./agent-profiles.js";
|
|
12
12
|
export { COMMAND_REGISTRY, renderCommand } from "./registry.js";
|
|
13
|
-
export { CATEGORIES, resolveRouting, RoutingGate } from "./routing.js";
|
|
13
|
+
export { CATEGORIES, resolveRouting, RoutingGate, ExecutionCardLifecycle, validateExecutionCard } from "./routing.js";
|
|
14
14
|
export { AgentProfileError, FIXED_AGENT_ROLES, applyAgentProfileChange, availableModels, availableModelVariants, listAgentProfiles, previewAgentProfileChange, renderAgentProfile, validateAgentName, validateModel, validateVariant, } from "./agent-profiles.js";
|
|
15
15
|
export { backgroundAttempts, goalLoop, scheduler, autonomyPolicy, rulesInjector, rtk, zedBell, zedClickablePaths };
|
|
16
16
|
const CATALOG = {
|
|
17
17
|
skills: ["attempt", "goal", "schedule", "multi-run", "usage", "overview", "lsp-report"],
|
|
18
18
|
plugins: ["background-attempts", "goal-loop", "schedule", "autonomy-policy", "rules-injector", "rtk", "zed-bell", "zed-clickable-paths"],
|
|
19
19
|
replacements: ["capabilities", "route", "doctor", "agent_profiles"],
|
|
20
|
-
version: "1.1.
|
|
20
|
+
version: "1.1.1",
|
|
21
21
|
};
|
|
22
22
|
const plugin = (async (input) => {
|
|
23
23
|
const gate = new RoutingGate();
|
|
@@ -29,16 +29,17 @@ const plugin = (async (input) => {
|
|
|
29
29
|
task: tool.schema.string(),
|
|
30
30
|
requirements: tool.schema.array(tool.schema.string()).default([]),
|
|
31
31
|
agents: tool.schema.array(tool.schema.object({ agent: tool.schema.string(), available: tool.schema.boolean().optional(), capabilities: tool.schema.array(tool.schema.string()).optional(), tools: tool.schema.array(tool.schema.string()).optional() })),
|
|
32
|
+
execution_card: tool.schema.any().optional(),
|
|
32
33
|
override: tool.schema.string().optional(),
|
|
33
34
|
budget: tool.schema.object({ cost_class: tool.schema.string().optional(), latency_class: tool.schema.string().optional() }).optional(),
|
|
34
35
|
decision: tool.schema.any().optional()
|
|
35
36
|
},
|
|
36
37
|
async execute(args, context) {
|
|
37
|
-
const input = { category: args.category, requirements: args.requirements, agents: args.agents, override: args.override, budget: args.budget };
|
|
38
|
+
const input = { category: args.category, task: args.task, requirements: args.requirements, agents: args.agents, execution_card: args.execution_card, override: args.override, budget: args.budget };
|
|
38
39
|
if (args.action === "preview")
|
|
39
40
|
return JSON.stringify(gate.preview(input));
|
|
40
41
|
const decision = gate.dispatch(input, args.decision);
|
|
41
|
-
gate.grant(context.sessionID, decision);
|
|
42
|
+
gate.grant(context.sessionID, decision, { task: args.task, requirements: args.requirements, card: args.execution_card });
|
|
42
43
|
return JSON.stringify({ decision, status: "routed" });
|
|
43
44
|
}
|
|
44
45
|
});
|
|
@@ -92,7 +93,14 @@ const plugin = (async (input) => {
|
|
|
92
93
|
const agent = typeof args.agent === "string" ? args.agent : typeof args.subagent_type === "string" ? args.subagent_type : undefined;
|
|
93
94
|
if (!agent)
|
|
94
95
|
throw new Error("Native Task requires an explicit agent and an active routing receipt");
|
|
95
|
-
|
|
96
|
+
const hasBinding = "task" in args || "requirements" in args || "execution_card" in args;
|
|
97
|
+
if (!hasBinding)
|
|
98
|
+
gate.consume(input.sessionID, agent);
|
|
99
|
+
else {
|
|
100
|
+
const task = typeof args.task === "string" ? args.task : "";
|
|
101
|
+
const requirements = Array.isArray(args.requirements) ? args.requirements : [];
|
|
102
|
+
gate.consume(input.sessionID, agent, { task, requirements, card: args.execution_card });
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
};
|
|
98
106
|
});
|
package/dist/installer.js
CHANGED
|
@@ -179,7 +179,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
179
179
|
mutations.push({ path: MANIFEST_NAME, operation: "remove", expected: { sha256: sha256(owned.raw) } });
|
|
180
180
|
}
|
|
181
181
|
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
182
|
-
const base = { schema_version: 1, action, scope, root, package_version: packageVersion(), operations: sorted, requires_restart: profiles.plan.requires_restart ||
|
|
182
|
+
const base = { schema_version: 1, action, scope, root, package_version: packageVersion(), operations: sorted, requires_restart: (action === "install" && owned.manifest?.version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
|
|
183
183
|
return {
|
|
184
184
|
plan: { ...base, digest: digest(base) },
|
|
185
185
|
mutations,
|
|
@@ -22,12 +22,23 @@ export async function backgroundAttempts({ client, directory, cwd }, options = {
|
|
|
22
22
|
await appendState(join(root, "receipts.jsonl"), root, { attempt_id: item.attempt_id, event, status: item.status, revision: item.revision, at: new Date().toISOString() });
|
|
23
23
|
};
|
|
24
24
|
const transition = async (item, status, event) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const allowed = {
|
|
26
|
+
queued: ["running", "failed", "cancelled"],
|
|
27
|
+
running: ["waiting", "completed", "failed", "cancelled", "orphaned"],
|
|
28
|
+
waiting: ["running", "completed", "failed", "cancelled", "orphaned"],
|
|
29
|
+
completed: [], failed: [], cancelled: [], orphaned: [],
|
|
30
|
+
};
|
|
31
|
+
if (!allowed[item.status].includes(status))
|
|
32
|
+
throw new Error(`Invalid background-attempt transition: ${item.status} -> ${status}`);
|
|
33
|
+
item.status = status;
|
|
34
|
+
item.revision += 1;
|
|
29
35
|
await save(item, event);
|
|
30
36
|
};
|
|
37
|
+
for (const item of await all()) {
|
|
38
|
+
if (item.project === project && (item.status === "running" || item.status === "waiting")) {
|
|
39
|
+
await transition(item, "orphaned", "recovery.orphaned");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
31
42
|
const pump = async () => {
|
|
32
43
|
const records = await all();
|
|
33
44
|
const running = records.filter((item) => item.status === "running" || item.status === "waiting");
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
type WorkItem = {
|
|
2
|
+
contract_version: "work-item/v1";
|
|
3
|
+
acceptance_criteria: Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
1
7
|
type Goal = {
|
|
2
8
|
schema_version: 1;
|
|
3
9
|
goal_id: string;
|
|
@@ -14,6 +20,11 @@ type Goal = {
|
|
|
14
20
|
};
|
|
15
21
|
receipts: Array<Record<string, unknown>>;
|
|
16
22
|
updated_at: string;
|
|
23
|
+
work_item?: WorkItem;
|
|
24
|
+
completion_evidence?: Array<{
|
|
25
|
+
criterion_id: string;
|
|
26
|
+
evidence: string;
|
|
27
|
+
}>;
|
|
17
28
|
};
|
|
18
29
|
type Client = {
|
|
19
30
|
session: {
|
|
@@ -12,6 +12,14 @@ function tokens(messages) {
|
|
|
12
12
|
const value = last?.info?.tokens?.total ?? last?.info?.tokens?.output;
|
|
13
13
|
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
14
14
|
}
|
|
15
|
+
function validWorkItem(goal) {
|
|
16
|
+
return goal.work_item?.contract_version === "work-item/v1" && Array.isArray(goal.work_item.acceptance_criteria) && goal.work_item.acceptance_criteria.length > 0;
|
|
17
|
+
}
|
|
18
|
+
function hasCompletionEvidence(goal) {
|
|
19
|
+
const criteria = goal.work_item?.acceptance_criteria ?? [];
|
|
20
|
+
const evidence = goal.completion_evidence ?? [];
|
|
21
|
+
return criteria.every((criterion) => evidence.some((item) => item.criterion_id === criterion.id && item.evidence.trim().length > 0));
|
|
22
|
+
}
|
|
15
23
|
export async function goalLoop({ client }, options = {}) {
|
|
16
24
|
if (!options.enabled)
|
|
17
25
|
return {};
|
|
@@ -29,6 +37,8 @@ export async function goalLoop({ client }, options = {}) {
|
|
|
29
37
|
const item = await lookup(session);
|
|
30
38
|
if (!item || item.state.status !== "running")
|
|
31
39
|
return;
|
|
40
|
+
if (!validWorkItem(item.state))
|
|
41
|
+
return settle(session, "blocked", "invalid work item");
|
|
32
42
|
item.state.status = status;
|
|
33
43
|
item.state.revision += 1;
|
|
34
44
|
item.state.updated_at = new Date().toISOString();
|
|
@@ -53,6 +63,8 @@ export async function goalLoop({ client }, options = {}) {
|
|
|
53
63
|
if (item.state.limits.token_budget > 0 && item.state.usage.tokens >= item.state.limits.token_budget)
|
|
54
64
|
return settle(session, "blocked", "token budget reached");
|
|
55
65
|
const verdict = await (options.audit?.(item.state) ?? Promise.resolve("continue"));
|
|
66
|
+
if (verdict === "complete" && !hasCompletionEvidence(item.state))
|
|
67
|
+
return settle(session, "blocked", "completion evidence is incomplete");
|
|
56
68
|
if (verdict === "complete" || verdict === "blocked")
|
|
57
69
|
return settle(session, verdict, "audit verdict");
|
|
58
70
|
await client.session.prompt({ path: { id: session }, body: { parts: [{ type: "text", text: "Continue the current goal within its declared constraints and boundaries." }] } });
|
package/dist/routing.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export type AvailableAgent = {
|
|
|
8
8
|
};
|
|
9
9
|
export type RoutingInput = {
|
|
10
10
|
category: Category;
|
|
11
|
+
task?: string;
|
|
11
12
|
requirements: string[];
|
|
12
13
|
agents: AvailableAgent[];
|
|
14
|
+
execution_card?: unknown;
|
|
13
15
|
override?: string;
|
|
14
16
|
budget?: {
|
|
15
17
|
cost_class?: string;
|
|
@@ -27,13 +29,68 @@ export type RoutingDecision = {
|
|
|
27
29
|
excluded_reasons: string[];
|
|
28
30
|
}[];
|
|
29
31
|
matrix_revision: string;
|
|
32
|
+
task_digest: string;
|
|
33
|
+
requirements_digest: string;
|
|
34
|
+
execution_card_digest?: string;
|
|
35
|
+
execution_card_revision?: number;
|
|
30
36
|
decision_digest: string;
|
|
31
37
|
};
|
|
38
|
+
export type ControlMarker = {
|
|
39
|
+
path: string;
|
|
40
|
+
expected?: string;
|
|
41
|
+
expected_absent?: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type ExecutionCard = {
|
|
44
|
+
status: "READY";
|
|
45
|
+
card_id: string;
|
|
46
|
+
revision: number;
|
|
47
|
+
objective: string;
|
|
48
|
+
changed_behavior: string[];
|
|
49
|
+
risks: string[];
|
|
50
|
+
write_set: string[];
|
|
51
|
+
control_markers: ControlMarker[];
|
|
52
|
+
decisions: string[];
|
|
53
|
+
steps: Array<{
|
|
54
|
+
path: string;
|
|
55
|
+
operation: string;
|
|
56
|
+
}>;
|
|
57
|
+
acceptance_criteria: string[];
|
|
58
|
+
checks: string[];
|
|
59
|
+
boundaries: {
|
|
60
|
+
forbidden_paths: string[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export type ExecutionCardStatus = ExecutionCard["status"] | "RUNNING" | "COMPLETED" | "BLOCKED" | "FAILED" | "REJECTED_PLAN" | "APPROVED" | "CHANGES_REQUIRED" | "CANCELLED";
|
|
32
64
|
export declare function resolveRouting(input: RoutingInput): RoutingDecision;
|
|
65
|
+
export declare function validateExecutionCard(card: unknown): {
|
|
66
|
+
valid: true;
|
|
67
|
+
card: ExecutionCard;
|
|
68
|
+
} | {
|
|
69
|
+
valid: false;
|
|
70
|
+
failedField: string;
|
|
71
|
+
};
|
|
72
|
+
type ReceiptContext = {
|
|
73
|
+
task?: string;
|
|
74
|
+
requirements?: string[];
|
|
75
|
+
card?: unknown;
|
|
76
|
+
};
|
|
77
|
+
export declare class ExecutionCardLifecycle {
|
|
78
|
+
#private;
|
|
79
|
+
constructor(card: unknown);
|
|
80
|
+
get status(): ExecutionCardStatus;
|
|
81
|
+
get card_id(): string;
|
|
82
|
+
get revision(): number;
|
|
83
|
+
transition(status: ExecutionCardStatus, identity: {
|
|
84
|
+
card_id: string;
|
|
85
|
+
revision: number;
|
|
86
|
+
}): ExecutionCardStatus;
|
|
87
|
+
}
|
|
33
88
|
export declare class RoutingGate {
|
|
34
89
|
#private;
|
|
35
90
|
preview(input: RoutingInput): RoutingDecision;
|
|
36
91
|
dispatch(input: RoutingInput, provided: unknown): RoutingDecision;
|
|
37
|
-
grant(sessionID: string, decision: RoutingDecision): void;
|
|
38
|
-
|
|
92
|
+
grant(sessionID: string, decision: RoutingDecision, context?: ReceiptContext): void;
|
|
93
|
+
cancel(sessionID: string): void;
|
|
94
|
+
consume(sessionID: string, agent: string, context?: ReceiptContext): void;
|
|
39
95
|
}
|
|
96
|
+
export {};
|
package/dist/routing.js
CHANGED
|
@@ -58,33 +58,173 @@ export function resolveRouting(input) {
|
|
|
58
58
|
eligible.push(agent);
|
|
59
59
|
}
|
|
60
60
|
const selected = input.override ? eligible.find((agent) => agent.agent === input.override) : eligible[0];
|
|
61
|
+
const card = input.execution_card === undefined ? undefined : validateExecutionCard(input.execution_card);
|
|
62
|
+
if (card && !card.valid)
|
|
63
|
+
throw new Error(`Invalid execution card: ${card.failedField}`);
|
|
61
64
|
const base = selected
|
|
62
|
-
? { schema_version: 1, status: "selected", category: input.category, agent: selected.agent, reason_codes: [input.override ? "explicit_override" : selected.agent === category.profiles[0] ? "primary_available" : "fallback_selected", "capabilities_match"], alternatives, matrix_revision: revision }
|
|
63
|
-
: { schema_version: 1, status: "escalate", category: input.category, reason_codes: [input.override ? "override_unavailable" : "no_eligible_profile"], alternatives, matrix_revision: revision };
|
|
65
|
+
? { schema_version: 1, status: "selected", category: input.category, agent: selected.agent, reason_codes: [input.override ? "explicit_override" : selected.agent === category.profiles[0] ? "primary_available" : "fallback_selected", "capabilities_match"], alternatives, matrix_revision: revision, task_digest: digest(input.task ?? ""), requirements_digest: digest(input.requirements), ...(card?.valid ? { execution_card_digest: digest(card.card), execution_card_revision: card.card.revision } : {}) }
|
|
66
|
+
: { schema_version: 1, status: "escalate", category: input.category, reason_codes: [input.override ? "override_unavailable" : "no_eligible_profile"], alternatives, matrix_revision: revision, task_digest: digest(input.task ?? ""), requirements_digest: digest(input.requirements), ...(card?.valid ? { execution_card_digest: digest(card.card), execution_card_revision: card.card.revision } : {}) };
|
|
64
67
|
return { ...base, decision_digest: digest(base) };
|
|
65
68
|
}
|
|
69
|
+
function isNonEmptyStringArray(value) {
|
|
70
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
|
|
71
|
+
}
|
|
72
|
+
export function validateExecutionCard(card) {
|
|
73
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) {
|
|
74
|
+
return { valid: false, failedField: "root" };
|
|
75
|
+
}
|
|
76
|
+
const c = card;
|
|
77
|
+
const expectedFields = [
|
|
78
|
+
"status", "card_id", "revision", "objective", "changed_behavior", "risks",
|
|
79
|
+
"write_set", "control_markers", "decisions", "steps", "acceptance_criteria",
|
|
80
|
+
"checks", "boundaries",
|
|
81
|
+
];
|
|
82
|
+
if (Object.keys(c).sort().join(",") !== expectedFields.slice().sort().join(","))
|
|
83
|
+
return { valid: false, failedField: "schema" };
|
|
84
|
+
if (c.status !== "READY")
|
|
85
|
+
return { valid: false, failedField: "status" };
|
|
86
|
+
if (typeof c.card_id !== "string" || !c.card_id)
|
|
87
|
+
return { valid: false, failedField: "card_id" };
|
|
88
|
+
if (typeof c.revision !== "number" || !Number.isInteger(c.revision) || c.revision < 1)
|
|
89
|
+
return { valid: false, failedField: "revision" };
|
|
90
|
+
if (typeof c.objective !== "string" || !c.objective)
|
|
91
|
+
return { valid: false, failedField: "objective" };
|
|
92
|
+
if (!isNonEmptyStringArray(c.changed_behavior))
|
|
93
|
+
return { valid: false, failedField: "changed_behavior" };
|
|
94
|
+
if (!isNonEmptyStringArray(c.risks))
|
|
95
|
+
return { valid: false, failedField: "risks" };
|
|
96
|
+
if (!isNonEmptyStringArray(c.write_set))
|
|
97
|
+
return { valid: false, failedField: "write_set" };
|
|
98
|
+
if (new Set(c.write_set).size !== c.write_set.length)
|
|
99
|
+
return { valid: false, failedField: "write_set" };
|
|
100
|
+
if (!Array.isArray(c.control_markers) || c.control_markers.length === 0)
|
|
101
|
+
return { valid: false, failedField: "control_markers" };
|
|
102
|
+
for (const marker of c.control_markers) {
|
|
103
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker))
|
|
104
|
+
return { valid: false, failedField: "control_markers" };
|
|
105
|
+
const m = marker;
|
|
106
|
+
if (typeof m.path !== "string" || !m.path)
|
|
107
|
+
return { valid: false, failedField: "control_markers" };
|
|
108
|
+
const hasExpected = typeof m.expected === "string" && m.expected.length > 0;
|
|
109
|
+
const hasExpectedAbsent = m.expected_absent === true;
|
|
110
|
+
if (hasExpected === hasExpectedAbsent)
|
|
111
|
+
return { valid: false, failedField: "control_markers" };
|
|
112
|
+
if (Object.keys(m).some((k) => k !== "path" && k !== "expected" && k !== "expected_absent"))
|
|
113
|
+
return { valid: false, failedField: "control_markers" };
|
|
114
|
+
}
|
|
115
|
+
if (!isNonEmptyStringArray(c.decisions))
|
|
116
|
+
return { valid: false, failedField: "decisions" };
|
|
117
|
+
if (!Array.isArray(c.steps) || c.steps.length === 0)
|
|
118
|
+
return { valid: false, failedField: "steps" };
|
|
119
|
+
for (const step of c.steps) {
|
|
120
|
+
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
121
|
+
return { valid: false, failedField: "steps" };
|
|
122
|
+
const s = step;
|
|
123
|
+
if (typeof s.path !== "string" || !s.path)
|
|
124
|
+
return { valid: false, failedField: "steps" };
|
|
125
|
+
if (typeof s.operation !== "string" || !s.operation)
|
|
126
|
+
return { valid: false, failedField: "steps" };
|
|
127
|
+
if (Object.keys(s).some((k) => k !== "path" && k !== "operation"))
|
|
128
|
+
return { valid: false, failedField: "steps" };
|
|
129
|
+
}
|
|
130
|
+
if (!isNonEmptyStringArray(c.acceptance_criteria))
|
|
131
|
+
return { valid: false, failedField: "acceptance_criteria" };
|
|
132
|
+
if (!isNonEmptyStringArray(c.checks))
|
|
133
|
+
return { valid: false, failedField: "checks" };
|
|
134
|
+
if (!c.boundaries || typeof c.boundaries !== "object" || Array.isArray(c.boundaries))
|
|
135
|
+
return { valid: false, failedField: "boundaries" };
|
|
136
|
+
const b = c.boundaries;
|
|
137
|
+
if (!isNonEmptyStringArray(b.forbidden_paths))
|
|
138
|
+
return { valid: false, failedField: "boundaries" };
|
|
139
|
+
if (Object.keys(b).some((k) => k !== "forbidden_paths"))
|
|
140
|
+
return { valid: false, failedField: "boundaries" };
|
|
141
|
+
const writeSet = c.write_set;
|
|
142
|
+
const forbiddenPaths = b.forbidden_paths;
|
|
143
|
+
const stepPaths = c.steps.map((s) => s.path);
|
|
144
|
+
const markerPaths = c.control_markers.map((m) => m.path);
|
|
145
|
+
const referenced = new Set([...stepPaths, ...markerPaths]);
|
|
146
|
+
if (![...referenced].every((p) => writeSet.includes(p)))
|
|
147
|
+
return { valid: false, failedField: "steps" };
|
|
148
|
+
if (writeSet.some((p) => forbiddenPaths.includes(p)))
|
|
149
|
+
return { valid: false, failedField: "boundaries" };
|
|
150
|
+
return { valid: true, card: c };
|
|
151
|
+
}
|
|
152
|
+
export class ExecutionCardLifecycle {
|
|
153
|
+
#status = "READY";
|
|
154
|
+
#cardID;
|
|
155
|
+
#revision;
|
|
156
|
+
constructor(card) {
|
|
157
|
+
const result = validateExecutionCard(card);
|
|
158
|
+
if (!result.valid)
|
|
159
|
+
throw new Error(`Invalid execution card: ${result.failedField}`);
|
|
160
|
+
this.#cardID = result.card.card_id;
|
|
161
|
+
this.#revision = result.card.revision;
|
|
162
|
+
}
|
|
163
|
+
get status() { return this.#status; }
|
|
164
|
+
get card_id() { return this.#cardID; }
|
|
165
|
+
get revision() { return this.#revision; }
|
|
166
|
+
transition(status, identity) {
|
|
167
|
+
if (identity.card_id !== this.#cardID || identity.revision !== this.#revision)
|
|
168
|
+
throw new Error("Execution card identity is stale");
|
|
169
|
+
const allowed = {
|
|
170
|
+
READY: ["RUNNING", "CANCELLED"],
|
|
171
|
+
RUNNING: ["COMPLETED", "BLOCKED", "FAILED", "REJECTED_PLAN", "CANCELLED"],
|
|
172
|
+
COMPLETED: ["APPROVED", "CHANGES_REQUIRED"],
|
|
173
|
+
BLOCKED: [], FAILED: [], REJECTED_PLAN: [], APPROVED: [], CHANGES_REQUIRED: [], CANCELLED: [],
|
|
174
|
+
};
|
|
175
|
+
if (!allowed[this.#status].includes(status))
|
|
176
|
+
throw new Error(`Invalid execution-card transition: ${this.#status} -> ${status}`);
|
|
177
|
+
this.#status = status;
|
|
178
|
+
return this.#status;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
66
181
|
export class RoutingGate {
|
|
67
182
|
#receipts = new Map();
|
|
183
|
+
#ttlMs = 10 * 60 * 1000;
|
|
68
184
|
preview(input) {
|
|
69
185
|
return resolveRouting(input);
|
|
70
186
|
}
|
|
71
187
|
dispatch(input, provided) {
|
|
72
188
|
const decision = resolveRouting(input);
|
|
73
|
-
if (!provided || typeof provided !== "object" || provided
|
|
189
|
+
if (!provided || typeof provided !== "object" || stable(provided) !== stable(decision))
|
|
74
190
|
throw new Error("Routing decision is stale; request a new preview");
|
|
75
191
|
if (decision.status !== "selected" || !decision.agent)
|
|
76
192
|
throw new Error("Routing escalated; no agent was dispatched");
|
|
77
193
|
return decision;
|
|
78
194
|
}
|
|
79
|
-
grant(sessionID, decision) {
|
|
80
|
-
|
|
195
|
+
grant(sessionID, decision, context) {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const taskDigest = context?.task === undefined ? decision.task_digest : digest(context.task);
|
|
198
|
+
const requirementsDigest = context?.requirements === undefined ? decision.requirements_digest : digest(context.requirements);
|
|
199
|
+
const cardDigest = context?.card === undefined ? decision.execution_card_digest : digest(context.card);
|
|
200
|
+
this.#receipts.set(sessionID, {
|
|
201
|
+
decision,
|
|
202
|
+
taskDigest,
|
|
203
|
+
requirementsDigest,
|
|
204
|
+
cardDigest,
|
|
205
|
+
expiresAt: now + this.#ttlMs,
|
|
206
|
+
});
|
|
81
207
|
}
|
|
82
|
-
|
|
208
|
+
cancel(sessionID) { this.#receipts.delete(sessionID); }
|
|
209
|
+
consume(sessionID, agent, context) {
|
|
83
210
|
const receipt = this.#receipts.get(sessionID);
|
|
84
211
|
if (!receipt)
|
|
85
212
|
throw new Error("Native Task requires an active routing receipt; use the route tool");
|
|
86
|
-
if (receipt.agent !== agent)
|
|
213
|
+
if (receipt.decision.agent !== agent)
|
|
87
214
|
throw new Error("Native Task agent does not match the active routing receipt");
|
|
215
|
+
if (Date.now() > receipt.expiresAt)
|
|
216
|
+
throw new Error("Routing receipt has expired; request a new preview");
|
|
217
|
+
if (context) {
|
|
218
|
+
const taskDigest = digest(context.task ?? "");
|
|
219
|
+
if (taskDigest !== receipt.taskDigest)
|
|
220
|
+
throw new Error("Routing receipt task does not match the active routing receipt");
|
|
221
|
+
const requirementsDigest = digest(context.requirements ?? []);
|
|
222
|
+
if (requirementsDigest !== receipt.requirementsDigest)
|
|
223
|
+
throw new Error("Routing receipt requirements do not match the active routing receipt");
|
|
224
|
+
const cardDigest = context.card !== undefined ? digest(context.card) : undefined;
|
|
225
|
+
if (cardDigest !== receipt.cardDigest)
|
|
226
|
+
throw new Error("Routing receipt execution card does not match the active routing receipt");
|
|
227
|
+
}
|
|
88
228
|
this.#receipts.delete(sessionID);
|
|
89
229
|
}
|
|
90
230
|
}
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare function readState<T>(path: string, boundary: string): Promise<T
|
|
|
4
4
|
export declare function listState(boundary: string, suffix?: string): Promise<string[]>;
|
|
5
5
|
export declare function writeState(path: string, boundary: string, value: unknown): Promise<void>;
|
|
6
6
|
export declare function appendState(path: string, boundary: string, value: unknown): Promise<void>;
|
|
7
|
+
export declare function withStateLock<T>(boundary: string, callback: () => Promise<T>): Promise<T>;
|
|
7
8
|
export declare function digest(value: unknown): string;
|
package/dist/runtime/state.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { appendPrivate, writeAtomic } from "../lifecycle.js";
|
|
5
|
+
import { appendPrivate, withLifecycleLock, writeAtomic } from "../lifecycle.js";
|
|
6
6
|
function home(value = process.env.HOME ?? homedir()) {
|
|
7
7
|
if (!isAbsolute(value) || value.split(sep).includes(".."))
|
|
8
8
|
throw new Error("HOME must be an absolute safe path");
|
|
@@ -94,14 +94,21 @@ export async function listState(boundary, suffix = ".json") {
|
|
|
94
94
|
export async function writeState(path, boundary, value) {
|
|
95
95
|
if (!inside(boundary, path))
|
|
96
96
|
throw new Error("state path escapes its boundary");
|
|
97
|
-
await
|
|
98
|
-
|
|
97
|
+
await withLifecycleLock(boundary, async () => {
|
|
98
|
+
await safeDirectory(dirname(path), boundary, true);
|
|
99
|
+
await writeAtomic(path, Buffer.from(`${JSON.stringify(value)}\n`), 0o600);
|
|
100
|
+
});
|
|
99
101
|
}
|
|
100
102
|
export async function appendState(path, boundary, value) {
|
|
101
103
|
if (!inside(boundary, path))
|
|
102
104
|
throw new Error("state path escapes its boundary");
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
+
await withLifecycleLock(boundary, async () => {
|
|
106
|
+
await safeDirectory(dirname(path), boundary, true);
|
|
107
|
+
await appendPrivate(path, value);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function withStateLock(boundary, callback) {
|
|
111
|
+
return withLifecycleLock(boundary, callback);
|
|
105
112
|
}
|
|
106
113
|
export function digest(value) {
|
|
107
114
|
return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function selectOption(label: string, options: readonly string[], stdin?: NodeJS.ReadStream, stderr?: NodeJS.WriteStream, initial?: number): Promise<number | null>;
|
|
2
|
+
export declare function promptText(label: string, stdin?: NodeJS.ReadStream, stderr?: NodeJS.WriteStream): Promise<string | null>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { InstallerError } from "./installer.js";
|
|
2
|
+
function parseKey(buffer) {
|
|
3
|
+
const seq = buffer.toString();
|
|
4
|
+
if (seq === "\r" || seq === "\n")
|
|
5
|
+
return { name: "return", sequence: seq };
|
|
6
|
+
if (seq === "\x03")
|
|
7
|
+
return { name: "ctrl+c", sequence: seq };
|
|
8
|
+
if (seq === "\x04")
|
|
9
|
+
return { name: "ctrl+d", sequence: seq };
|
|
10
|
+
if (seq === "\x1b" || seq === "\x1b\x1b" || seq === "\x1b\x1b\x1b")
|
|
11
|
+
return { name: "escape", sequence: seq };
|
|
12
|
+
if (seq === "\x1b[A" || seq === "\x1b[OA")
|
|
13
|
+
return { name: "up", sequence: seq };
|
|
14
|
+
if (seq === "\x1b[B" || seq === "\x1b[OB")
|
|
15
|
+
return { name: "down", sequence: seq };
|
|
16
|
+
if (seq === "\x1b[C" || seq === "\x1b[OC")
|
|
17
|
+
return { name: "right", sequence: seq };
|
|
18
|
+
if (seq === "\x1b[D" || seq === "\x1b[OD")
|
|
19
|
+
return { name: "left", sequence: seq };
|
|
20
|
+
return { name: "unknown", sequence: seq };
|
|
21
|
+
}
|
|
22
|
+
function clearLines(stderr, count) {
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
stderr.write("\x1b[A\x1b[K");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function selectOption(label, options, stdin = process.stdin, stderr = process.stderr, initial = 0) {
|
|
28
|
+
if (!stdin.isTTY || !stderr.isTTY) {
|
|
29
|
+
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
30
|
+
}
|
|
31
|
+
let selected = Math.max(0, Math.min(initial, options.length - 1));
|
|
32
|
+
const lineCount = options.length + 1; // label + options
|
|
33
|
+
function render() {
|
|
34
|
+
stderr.write(`${label}\n`);
|
|
35
|
+
for (let i = 0; i < options.length; i++) {
|
|
36
|
+
const cursor = i === selected ? "> " : " ";
|
|
37
|
+
stderr.write(`${cursor}${options[i]}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
render();
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
stdin.setRawMode(true);
|
|
43
|
+
stdin.resume();
|
|
44
|
+
function cleanup() {
|
|
45
|
+
stdin.setRawMode(false);
|
|
46
|
+
stdin.pause();
|
|
47
|
+
stdin.removeListener("data", onData);
|
|
48
|
+
}
|
|
49
|
+
function onData(data) {
|
|
50
|
+
let remaining = data.toString();
|
|
51
|
+
while (remaining) {
|
|
52
|
+
const sequence = remaining.startsWith("\x1b[") ? remaining.slice(0, 3) : remaining[0];
|
|
53
|
+
remaining = remaining.slice(sequence.length);
|
|
54
|
+
const key = parseKey(Buffer.from(sequence));
|
|
55
|
+
if (key.name === "ctrl+c" || key.name === "ctrl+d" || key.name === "escape") {
|
|
56
|
+
cleanup();
|
|
57
|
+
clearLines(stderr, lineCount);
|
|
58
|
+
resolve(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key.name === "return") {
|
|
62
|
+
cleanup();
|
|
63
|
+
clearLines(stderr, lineCount);
|
|
64
|
+
resolve(selected);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.name === "up") {
|
|
68
|
+
clearLines(stderr, lineCount);
|
|
69
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
70
|
+
render();
|
|
71
|
+
}
|
|
72
|
+
else if (key.name === "down") {
|
|
73
|
+
clearLines(stderr, lineCount);
|
|
74
|
+
selected = (selected + 1) % options.length;
|
|
75
|
+
render();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
stdin.on("data", onData);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function promptText(label, stdin = process.stdin, stderr = process.stderr) {
|
|
83
|
+
if (!stdin.isTTY || !stderr.isTTY) {
|
|
84
|
+
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
85
|
+
}
|
|
86
|
+
stderr.write(`${label} `);
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let buffer = "";
|
|
89
|
+
stdin.setRawMode(true);
|
|
90
|
+
stdin.resume();
|
|
91
|
+
function cleanup() {
|
|
92
|
+
stdin.setRawMode(false);
|
|
93
|
+
stdin.pause();
|
|
94
|
+
stdin.removeListener("data", onData);
|
|
95
|
+
}
|
|
96
|
+
function onData(data) {
|
|
97
|
+
for (const seq of data.toString()) {
|
|
98
|
+
if (seq === "\r" || seq === "\n") {
|
|
99
|
+
cleanup();
|
|
100
|
+
stderr.write("\n");
|
|
101
|
+
resolve(buffer.trim() || null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (seq === "\x03" || seq === "\x04" || seq === "\x1b") {
|
|
105
|
+
cleanup();
|
|
106
|
+
stderr.write("\n");
|
|
107
|
+
resolve(null);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (seq === "\x7f" || seq === "\b") {
|
|
111
|
+
if (buffer.length > 0) {
|
|
112
|
+
buffer = buffer.slice(0, -1);
|
|
113
|
+
stderr.write("\x1b[D \x1b[D");
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (seq >= " ") {
|
|
118
|
+
buffer += seq;
|
|
119
|
+
stderr.write(seq);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
stdin.on("data", onData);
|
|
124
|
+
});
|
|
125
|
+
}
|