@llblab/pi-telegram 0.52.2 → 0.53.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/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -31,7 +31,10 @@ export type CommandTemplateValue = string | CommandTemplateConfig[] | CommandTem
|
|
|
31
31
|
export type CommandTemplateConfig = string | CommandTemplateObjectConfig;
|
|
32
32
|
export interface CommandTemplateLeafConfig extends CommandTemplateObjectConfig {
|
|
33
33
|
template: string;
|
|
34
|
+
/** `when` guards of the composition nodes enclosing this leaf; every one must pass. */
|
|
35
|
+
inheritedWhen?: readonly (boolean | string)[];
|
|
34
36
|
}
|
|
37
|
+
type CommandTemplateExpansionContext = Pick<CommandTemplateLeafConfig, "args" | "defaults" | "inheritedWhen">;
|
|
35
38
|
export interface CommandTemplateInvocation {
|
|
36
39
|
command: string;
|
|
37
40
|
args: string[];
|
|
@@ -43,6 +46,8 @@ export interface CommandTemplateExecOptions {
|
|
|
43
46
|
stdin?: string;
|
|
44
47
|
killGrace?: number;
|
|
45
48
|
retry?: number;
|
|
49
|
+
/** Cleanup between a failed attempt and the next retry; a thrown error stops retries. */
|
|
50
|
+
recover?: () => Promise<void>;
|
|
46
51
|
}
|
|
47
52
|
export interface CommandTemplateExecResult {
|
|
48
53
|
stdout: string;
|
|
@@ -50,16 +55,32 @@ export interface CommandTemplateExecResult {
|
|
|
50
55
|
code: number;
|
|
51
56
|
killed: boolean;
|
|
52
57
|
}
|
|
53
|
-
export type CommandTemplateRiskLabel = "risk.shell" | "risk.eval" | "risk.broad_fs_write" | "risk.destructive_fs" | "risk.network" | "risk.external_side_effect" | "risk.long_running" | "risk.platform_specific" | "risk.secret_touching";
|
|
54
58
|
export declare function normalizeCommandTemplateConfig(config: CommandTemplateConfig): CommandTemplateObjectConfig;
|
|
55
|
-
export declare function expandCommandTemplateConfigs(config: CommandTemplateConfig, inherited?:
|
|
56
|
-
|
|
57
|
-
export declare function
|
|
59
|
+
export declare function expandCommandTemplateConfigs(config: CommandTemplateConfig, inherited?: CommandTemplateExpansionContext): CommandTemplateLeafConfig[];
|
|
60
|
+
/** Whether a node's own and enclosing `when` guards all pass for its resolved placeholder values. */
|
|
61
|
+
export declare function shouldRunCommandTemplateConfig(config: CommandTemplateConfig | CommandTemplateLeafConfig, values: Record<string, unknown>): boolean;
|
|
62
|
+
/** A node's `recover` template as one cleanup run: output ignored, failure stops retries unless a leaf opts into `continue`. */
|
|
63
|
+
export declare function createCommandTemplateRecovery(config: CommandTemplateConfig, values: Record<string, unknown>, options: {
|
|
64
|
+
cwd: string;
|
|
65
|
+
timeout?: number;
|
|
66
|
+
execCommand: (command: string, args: string[], options?: CommandTemplateExecOptions) => Promise<CommandTemplateExecResult>;
|
|
67
|
+
}): (() => Promise<void>) | undefined;
|
|
58
68
|
export declare function splitCommandTemplate(input: string): string[];
|
|
59
69
|
export declare function shouldRunCommandTemplateNode(value: boolean | string | undefined, values: Record<string, unknown>): boolean;
|
|
60
|
-
|
|
70
|
+
/** Resolve one optional non-negative numeric control field, substituting a template token when given as text. */
|
|
71
|
+
export declare function resolveCommandTemplateNumericField(value: number | string | undefined, values: Record<string, unknown>, label: string): number | undefined;
|
|
72
|
+
/** A node's own `timeout`; string leaves carry none. */
|
|
73
|
+
export declare function getCommandTemplateConfiguredTimeout(config: CommandTemplateConfig | undefined): number | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Bound one composed step by its own timeout and the handler budget left after `elapsedMs`.
|
|
76
|
+
* Without `elapsedMs` the step receives the full budget.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getCommandTemplateStepTimeout(budgetMs: number, step: CommandTemplateConfig, elapsedMs?: number): number;
|
|
79
|
+
/** Array templates expand into ordered composition steps; single-command templates have none. */
|
|
80
|
+
export declare function getCommandTemplateCompositionSteps(config: CommandTemplateObjectConfig): CommandTemplateLeafConfig[];
|
|
61
81
|
export declare function execCommandTemplate(command: string, args: string[], options?: CommandTemplateExecOptions): Promise<CommandTemplateExecResult>;
|
|
62
82
|
export declare function buildCommandTemplateInvocation(config: CommandTemplateConfig, values: Record<string, unknown>, cwd: string, options?: {
|
|
63
83
|
emptyMessage?: string;
|
|
64
84
|
missingLabel?: string;
|
|
65
85
|
}): CommandTemplateInvocation;
|
|
86
|
+
export {};
|
|
@@ -6,17 +6,6 @@
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { extname, isAbsolute, normalize, resolve } from "node:path";
|
|
9
|
-
const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
|
|
10
|
-
"risk.shell",
|
|
11
|
-
"risk.eval",
|
|
12
|
-
"risk.destructive_fs",
|
|
13
|
-
"risk.broad_fs_write",
|
|
14
|
-
"risk.external_side_effect",
|
|
15
|
-
"risk.secret_touching",
|
|
16
|
-
"risk.network",
|
|
17
|
-
"risk.long_running",
|
|
18
|
-
"risk.platform_specific",
|
|
19
|
-
];
|
|
20
9
|
function normalizeCommandTemplateArgs(value) {
|
|
21
10
|
if (!Array.isArray(value))
|
|
22
11
|
return [];
|
|
@@ -80,151 +69,6 @@ function resolveCommandTemplateRepeat(value, values = {}) {
|
|
|
80
69
|
}
|
|
81
70
|
throw new Error("Command template repeat must be a positive integer or {array.length}.");
|
|
82
71
|
}
|
|
83
|
-
function getExecutableName(command) {
|
|
84
|
-
if (!command)
|
|
85
|
-
return "";
|
|
86
|
-
return command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
|
|
87
|
-
}
|
|
88
|
-
function matchesFlag(arg, flag) {
|
|
89
|
-
if (arg === flag)
|
|
90
|
-
return true;
|
|
91
|
-
if (/^-[A-Za-z]$/.test(flag) && /^-[A-Za-z]+$/.test(arg))
|
|
92
|
-
return arg.slice(1).includes(flag.slice(1));
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
function hasAnyFlag(args, flags) {
|
|
96
|
-
return args.some((arg) => flags.some((flag) => matchesFlag(arg, flag)));
|
|
97
|
-
}
|
|
98
|
-
function hasRiskyPathArg(args) {
|
|
99
|
-
return args.some((arg) => arg === "/" ||
|
|
100
|
-
arg === "~" ||
|
|
101
|
-
arg === "./" ||
|
|
102
|
-
arg === "../" ||
|
|
103
|
-
arg.includes("{") ||
|
|
104
|
-
arg.startsWith("~/") ||
|
|
105
|
-
arg.startsWith("/"));
|
|
106
|
-
}
|
|
107
|
-
function sortRiskLabels(labels) {
|
|
108
|
-
const unique = new Set(labels);
|
|
109
|
-
return COMMAND_TEMPLATE_RISK_LABEL_ORDER.filter((label) => unique.has(label));
|
|
110
|
-
}
|
|
111
|
-
function hasAnyArg(args, values) {
|
|
112
|
-
return args.some((arg) => values.includes(arg.toLowerCase()));
|
|
113
|
-
}
|
|
114
|
-
function hasSecretTouchingText(parts) {
|
|
115
|
-
return parts.some((part) => /(^|[{}._\-\s/])(?:secret|token|password|passwd|credential|api[_-]?key|private[_-]?key|\.env|ssh[_-]?key)(?:[{}._\-\s/]|$)/i.test(part));
|
|
116
|
-
}
|
|
117
|
-
function getLeafCommandTemplateRiskLabels(config) {
|
|
118
|
-
const parts = splitCommandTemplate(config.template);
|
|
119
|
-
const command = getExecutableName(parts[0]);
|
|
120
|
-
const args = parts.slice(1);
|
|
121
|
-
const labels = new Set();
|
|
122
|
-
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
123
|
-
labels.add("risk.shell");
|
|
124
|
-
if (hasAnyFlag(args, ["-c"]))
|
|
125
|
-
labels.add("risk.eval");
|
|
126
|
-
}
|
|
127
|
-
if (["node", "deno", "bun"].includes(command) &&
|
|
128
|
-
hasAnyFlag(args, ["-e", "--eval"])) {
|
|
129
|
-
labels.add("risk.eval");
|
|
130
|
-
}
|
|
131
|
-
if (["python", "python3", "perl", "ruby"].includes(command) &&
|
|
132
|
-
hasAnyFlag(args, ["-c", "-e"])) {
|
|
133
|
-
labels.add("risk.eval");
|
|
134
|
-
}
|
|
135
|
-
if (command === "rm" &&
|
|
136
|
-
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
137
|
-
hasRiskyPathArg(args))) {
|
|
138
|
-
labels.add("risk.destructive_fs");
|
|
139
|
-
}
|
|
140
|
-
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
141
|
-
labels.add("risk.broad_fs_write");
|
|
142
|
-
}
|
|
143
|
-
if ([
|
|
144
|
-
"curl",
|
|
145
|
-
"wget",
|
|
146
|
-
"ssh",
|
|
147
|
-
"scp",
|
|
148
|
-
"sftp",
|
|
149
|
-
"rsync",
|
|
150
|
-
"nc",
|
|
151
|
-
"ncat",
|
|
152
|
-
"telnet",
|
|
153
|
-
"ftp",
|
|
154
|
-
].includes(command) ||
|
|
155
|
-
(command === "git" &&
|
|
156
|
-
hasAnyArg(args, ["clone", "fetch", "pull", "push", "ls-remote"])) ||
|
|
157
|
-
["npm", "pnpm", "yarn", "pip", "cargo"].includes(command)) {
|
|
158
|
-
labels.add("risk.network");
|
|
159
|
-
}
|
|
160
|
-
if (["gh", "glab", "hub", "kubectl", "terraform"].includes(command) ||
|
|
161
|
-
(command === "git" && hasAnyArg(args, ["push"])) ||
|
|
162
|
-
(["npm", "pnpm", "yarn"].includes(command) &&
|
|
163
|
-
hasAnyArg(args, ["publish", "login", "logout", "deprecate"]))) {
|
|
164
|
-
labels.add("risk.external_side_effect");
|
|
165
|
-
}
|
|
166
|
-
if (command === "sleep" ||
|
|
167
|
-
command === "watch" ||
|
|
168
|
-
(command === "tail" && hasAnyFlag(args, ["-f"])) ||
|
|
169
|
-
hasAnyArg(args, ["--watch", "--serve", "serve"])) {
|
|
170
|
-
labels.add("risk.long_running");
|
|
171
|
-
}
|
|
172
|
-
if ([
|
|
173
|
-
"systemctl",
|
|
174
|
-
"launchctl",
|
|
175
|
-
"osascript",
|
|
176
|
-
"open",
|
|
177
|
-
"xdg-open",
|
|
178
|
-
"powershell",
|
|
179
|
-
"pwsh",
|
|
180
|
-
"cmd.exe",
|
|
181
|
-
"apt",
|
|
182
|
-
"apt-get",
|
|
183
|
-
"dnf",
|
|
184
|
-
"yum",
|
|
185
|
-
"brew",
|
|
186
|
-
"pacman",
|
|
187
|
-
"apk",
|
|
188
|
-
"xclip",
|
|
189
|
-
"wl-copy",
|
|
190
|
-
].includes(command)) {
|
|
191
|
-
labels.add("risk.platform_specific");
|
|
192
|
-
}
|
|
193
|
-
if (["pass", "gpg", "ssh-add"].includes(command) ||
|
|
194
|
-
hasSecretTouchingText(parts)) {
|
|
195
|
-
labels.add("risk.secret_touching");
|
|
196
|
-
}
|
|
197
|
-
return sortRiskLabels(labels);
|
|
198
|
-
}
|
|
199
|
-
function getLeafCommandTemplateWarnings(config) {
|
|
200
|
-
const parts = splitCommandTemplate(config.template);
|
|
201
|
-
const command = getExecutableName(parts[0]);
|
|
202
|
-
const args = parts.slice(1);
|
|
203
|
-
const warnings = [];
|
|
204
|
-
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
205
|
-
const shellContent = hasAnyFlag(args, ["-c"])
|
|
206
|
-
? "shell command strings"
|
|
207
|
-
: "shell scripts";
|
|
208
|
-
warnings.push(`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting. Mitigation: keep scripts local, reviewed, and parameterized with explicit placeholders.`);
|
|
209
|
-
}
|
|
210
|
-
if (["node", "deno", "bun"].includes(command) &&
|
|
211
|
-
hasAnyFlag(args, ["-e", "--eval"])) {
|
|
212
|
-
warnings.push(`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`);
|
|
213
|
-
}
|
|
214
|
-
if (["python", "python3", "perl", "ruby"].includes(command) &&
|
|
215
|
-
hasAnyFlag(args, ["-c", "-e"])) {
|
|
216
|
-
warnings.push(`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`);
|
|
217
|
-
}
|
|
218
|
-
if (command === "rm" &&
|
|
219
|
-
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
220
|
-
hasRiskyPathArg(args))) {
|
|
221
|
-
warnings.push(`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands. Mitigation: constrain path placeholders and consider dry-run or explicit confirmation.`);
|
|
222
|
-
}
|
|
223
|
-
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
224
|
-
warnings.push(`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands. Mitigation: constrain path placeholders and prefer narrow source/destination paths.`);
|
|
225
|
-
}
|
|
226
|
-
return warnings;
|
|
227
|
-
}
|
|
228
72
|
function pad(value, width) {
|
|
229
73
|
return String(value).padStart(width, "0");
|
|
230
74
|
}
|
|
@@ -266,6 +110,9 @@ export function expandCommandTemplateConfigs(config, inherited = {}) {
|
|
|
266
110
|
const inheritedDefaults = normalizeCommandTemplateDefaults(inherited.defaults);
|
|
267
111
|
const ownDefaults = resolveInheritedDefaultReferences(normalizeCommandTemplateDefaults(normalizedConfig.defaults), inheritedDefaults);
|
|
268
112
|
const context = {
|
|
113
|
+
...(inherited.inheritedWhen?.length
|
|
114
|
+
? { inheritedWhen: inherited.inheritedWhen }
|
|
115
|
+
: {}),
|
|
269
116
|
...(inherited.args !== undefined ? { args: inherited.args } : {}),
|
|
270
117
|
...(inheritedDefaults ? { defaults: inheritedDefaults } : {}),
|
|
271
118
|
...(normalizedConfig.args !== undefined
|
|
@@ -279,18 +126,21 @@ export function expandCommandTemplateConfigs(config, inherited = {}) {
|
|
|
279
126
|
if (repeated) {
|
|
280
127
|
return repeated.flatMap((step) => expandCommandTemplateConfigs(step, context));
|
|
281
128
|
}
|
|
282
|
-
|
|
283
|
-
const recoverSteps = recoverConfig
|
|
284
|
-
? expandCommandTemplateConfigs(recoverConfig, context)
|
|
285
|
-
: [];
|
|
129
|
+
// `recover` is cleanup between failed retries, never a sequence step; runners attach it per leaf.
|
|
286
130
|
if (Array.isArray(normalizedConfig.template)) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
131
|
+
const childContext = normalizedConfig.when === undefined
|
|
132
|
+
? context
|
|
133
|
+
: {
|
|
134
|
+
...context,
|
|
135
|
+
inheritedWhen: [
|
|
136
|
+
...(context.inheritedWhen ?? []),
|
|
137
|
+
normalizedConfig.when,
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
return normalizedConfig.template.flatMap((step) => expandCommandTemplateConfigs(step, childContext));
|
|
291
141
|
}
|
|
292
142
|
if (typeof normalizedConfig.template !== "string")
|
|
293
|
-
return
|
|
143
|
+
return [];
|
|
294
144
|
return [
|
|
295
145
|
{
|
|
296
146
|
...normalizedConfig,
|
|
@@ -298,16 +148,45 @@ export function expandCommandTemplateConfigs(config, inherited = {}) {
|
|
|
298
148
|
template: normalizedConfig.template,
|
|
299
149
|
retry: normalizedConfig.retry,
|
|
300
150
|
},
|
|
301
|
-
...recoverSteps,
|
|
302
151
|
];
|
|
303
152
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
153
|
+
/** Whether a node's own and enclosing `when` guards all pass for its resolved placeholder values. */
|
|
154
|
+
export function shouldRunCommandTemplateConfig(config, values) {
|
|
155
|
+
if (typeof config === "string")
|
|
156
|
+
return true;
|
|
157
|
+
const inheritedWhen = "inheritedWhen" in config ? (config.inheritedWhen ?? []) : [];
|
|
158
|
+
if (config.when === undefined && inheritedWhen.length === 0)
|
|
159
|
+
return true;
|
|
160
|
+
const resolved = { ...getCommandTemplateDefaults(config), ...values };
|
|
161
|
+
return [...inheritedWhen, config.when].every((condition) => shouldRunCommandTemplateNode(condition, resolved));
|
|
308
162
|
}
|
|
309
|
-
|
|
310
|
-
|
|
163
|
+
/** A node's `recover` template as one cleanup run: output ignored, failure stops retries unless a leaf opts into `continue`. */
|
|
164
|
+
export function createCommandTemplateRecovery(config, values, options) {
|
|
165
|
+
if (typeof config === "string")
|
|
166
|
+
return undefined;
|
|
167
|
+
const recoverConfig = normalizeRecoverConfig(config.recover);
|
|
168
|
+
if (!recoverConfig)
|
|
169
|
+
return undefined;
|
|
170
|
+
const steps = expandCommandTemplateConfigs(recoverConfig, {
|
|
171
|
+
...(config.args !== undefined ? { args: config.args } : {}),
|
|
172
|
+
...(config.defaults !== undefined ? { defaults: config.defaults } : {}),
|
|
173
|
+
});
|
|
174
|
+
return async () => {
|
|
175
|
+
for (const step of steps) {
|
|
176
|
+
if (!shouldRunCommandTemplateConfig(step, values))
|
|
177
|
+
continue;
|
|
178
|
+
const invocation = buildCommandTemplateInvocation(step, values, options.cwd, { missingLabel: "command template recover" });
|
|
179
|
+
const result = await options.execCommand(invocation.command, invocation.args, {
|
|
180
|
+
cwd: options.cwd,
|
|
181
|
+
...(options.timeout !== undefined
|
|
182
|
+
? { timeout: options.timeout }
|
|
183
|
+
: {}),
|
|
184
|
+
});
|
|
185
|
+
if (result.code !== 0 && step.failure !== "continue") {
|
|
186
|
+
throw new Error(`Command template recover exited with code ${result.code}${result.killed ? " (killed)" : ""}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
311
190
|
}
|
|
312
191
|
function parseCommandTemplateArgToken(value) {
|
|
313
192
|
const separatorIndex = value.indexOf("=");
|
|
@@ -597,7 +476,43 @@ function resolveCommandTemplateValue(content, values, missingLabel, depth = 0) {
|
|
|
597
476
|
return expression;
|
|
598
477
|
return undefined;
|
|
599
478
|
}
|
|
600
|
-
|
|
479
|
+
/** Resolve one optional non-negative numeric control field, substituting a template token when given as text. */
|
|
480
|
+
export function resolveCommandTemplateNumericField(value, values, label) {
|
|
481
|
+
if (value === undefined)
|
|
482
|
+
return undefined;
|
|
483
|
+
const resolved = typeof value === "string"
|
|
484
|
+
? substituteCommandTemplateToken(value, values, label)
|
|
485
|
+
: value;
|
|
486
|
+
if (resolved === "")
|
|
487
|
+
return undefined;
|
|
488
|
+
const numeric = Number(resolved);
|
|
489
|
+
if (!Number.isFinite(numeric) || numeric < 0)
|
|
490
|
+
throw new Error(`Command template ${label} must be a non-negative number.`);
|
|
491
|
+
return numeric;
|
|
492
|
+
}
|
|
493
|
+
/** A node's own `timeout`; string leaves carry none. */
|
|
494
|
+
export function getCommandTemplateConfiguredTimeout(config) {
|
|
495
|
+
const timeout = typeof config === "string" ? undefined : config?.timeout;
|
|
496
|
+
return resolveCommandTemplateNumericField(timeout, {}, "timeout");
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Bound one composed step by its own timeout and the handler budget left after `elapsedMs`.
|
|
500
|
+
* Without `elapsedMs` the step receives the full budget.
|
|
501
|
+
*/
|
|
502
|
+
export function getCommandTemplateStepTimeout(budgetMs, step, elapsedMs) {
|
|
503
|
+
const remaining = elapsedMs === undefined ? budgetMs : Math.max(1, budgetMs - elapsedMs);
|
|
504
|
+
const stepTimeout = getCommandTemplateConfiguredTimeout(step);
|
|
505
|
+
return stepTimeout === undefined
|
|
506
|
+
? remaining
|
|
507
|
+
: Math.min(stepTimeout, remaining);
|
|
508
|
+
}
|
|
509
|
+
/** Array templates expand into ordered composition steps; single-command templates have none. */
|
|
510
|
+
export function getCommandTemplateCompositionSteps(config) {
|
|
511
|
+
return Array.isArray(config.template)
|
|
512
|
+
? expandCommandTemplateConfigs(config)
|
|
513
|
+
: [];
|
|
514
|
+
}
|
|
515
|
+
function substituteCommandTemplateToken(token, values, missingLabel = "command template", depth = 0) {
|
|
601
516
|
return token.replace(/\{([^{}]+)\}/g, (_match, content) => {
|
|
602
517
|
const resolved = resolveCommandTemplateValue(content, values, missingLabel, depth);
|
|
603
518
|
if (resolved !== undefined)
|
|
@@ -618,6 +533,8 @@ export async function execCommandTemplate(command, args, options = {}) {
|
|
|
618
533
|
if (result.code === 0)
|
|
619
534
|
return result;
|
|
620
535
|
lastResult = result;
|
|
536
|
+
if (attempt < maxAttempts)
|
|
537
|
+
await options.recover?.();
|
|
621
538
|
}
|
|
622
539
|
return lastResult;
|
|
623
540
|
}
|
|
@@ -627,7 +544,7 @@ function escapeWindowsCommand(value) {
|
|
|
627
544
|
}
|
|
628
545
|
function escapeWindowsCommandArgument(value, doubleEscapeMetaChars) {
|
|
629
546
|
let escaped = value
|
|
630
|
-
.replace(/(?=(\\+?)?)\1"/g,
|
|
547
|
+
.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"')
|
|
631
548
|
.replace(/(?=(\\+?)?)\1$/g, "$1$1");
|
|
632
549
|
escaped = `"${escaped}"`.replace(WINDOWS_COMMAND_META_CHARS, "^$1");
|
|
633
550
|
return doubleEscapeMetaChars
|
|
@@ -640,8 +557,7 @@ function resolveCommandTemplateSpawn(command, args) {
|
|
|
640
557
|
return { command, args };
|
|
641
558
|
}
|
|
642
559
|
const normalizedCommand = normalize(command);
|
|
643
|
-
const isNodeModulesShim = /[\\/]node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/iu
|
|
644
|
-
.test(normalizedCommand);
|
|
560
|
+
const isNodeModulesShim = /[\\/]node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/iu.test(normalizedCommand);
|
|
645
561
|
const shellCommand = [
|
|
646
562
|
escapeWindowsCommand(normalizedCommand),
|
|
647
563
|
...args.map((arg) => escapeWindowsCommandArgument(arg, isNodeModulesShim)),
|