@alphacoder-v0/pi-loops 0.22.8 → 0.24.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/CHANGELOG.md +664 -0
- package/CONTRACT.md +7 -7
- package/README.md +91 -63
- package/README.zh-CN.md +59 -39
- package/docs/cli.md +272 -83
- package/docs/configuration.md +86 -24
- package/docs/design.md +395 -56
- package/docs/downstream.md +2 -2
- package/docs/goal.md +12 -3
- package/docs/hooks.md +6 -7
- package/docs/loops.md +288 -59
- package/docs/mcp.md +22 -14
- package/docs/recipes.md +6 -0
- package/docs/screenshot.png +0 -0
- package/docs/triggers.md +120 -26
- package/docs/troubleshooting.md +37 -26
- package/docs/web-ui-parity.md +691 -65
- package/package.json +4 -3
- package/recipes/autoresearch/recipe.toml +1 -0
- package/recipes/changelog-draft/recipe.toml +1 -0
- package/recipes/ci-sweeper/recipe.toml +1 -0
- package/recipes/daily-digest/recipe.toml +1 -0
- package/recipes/deps-sweeper/recipe.toml +1 -0
- package/recipes/ecosystem/recipe.toml +1 -0
- package/recipes/issue-loop/recipe.toml +1 -0
- package/recipes/pr-watch/recipe.toml +1 -0
- package/skills/pi-loops/SKILL.md +6 -4
- package/src/activity.ts +199 -0
- package/src/archive.ts +21 -9
- package/src/args.ts +6 -2
- package/src/cli-entry.mjs +12 -3
- package/src/cli.ts +746 -125
- package/src/config.ts +53 -5
- package/src/conversation-rows.ts +469 -0
- package/src/danger.ts +227 -6
- package/src/engine.ts +1240 -0
- package/src/goal.ts +71 -0
- package/src/held-read.ts +27 -0
- package/src/hooks.ts +49 -18
- package/src/host-control-channel.ts +455 -40
- package/src/host-control.ts +602 -111
- package/src/host-entry.mjs +5 -3
- package/src/host-runtime.ts +455 -42
- package/src/host.ts +659 -123
- package/src/inbox-card.ts +278 -0
- package/src/inbox.ts +139 -62
- package/src/job-edit.ts +7 -1
- package/src/job-health.ts +14 -2
- package/src/job-lines.ts +201 -13
- package/src/job-owner.ts +74 -13
- package/src/job-tune.ts +110 -0
- package/src/lease.ts +537 -0
- package/src/lock.ts +7 -2
- package/src/mcp-pool.ts +66 -14
- package/src/mcp.ts +1 -1
- package/src/open-file.ts +54 -0
- package/src/page-children.ts +1507 -0
- package/src/page-devices.ts +192 -0
- package/src/page-server.ts +3150 -0
- package/src/path-precheck.ts +215 -0
- package/src/paths.ts +24 -0
- package/src/pi-floor.ts +10 -0
- package/src/pi-loops.ts +1301 -450
- package/src/pi-resolver.d.mts +1 -0
- package/src/pi-resolver.mjs +110 -10
- package/src/presence.ts +12 -63
- package/src/protocol.ts +10 -1
- package/src/recipe.ts +43 -1
- package/src/requests.ts +354 -0
- package/src/rule-lines.ts +68 -0
- package/src/run-card.ts +105 -0
- package/src/runner.ts +18 -1
- package/src/schedule.ts +1 -2
- package/src/scheduler.ts +595 -245
- package/src/sdk-runner.ts +321 -27
- package/src/seen.ts +278 -0
- package/src/session-head.ts +21 -0
- package/src/share.ts +12 -2
- package/src/snapshot.ts +3 -2
- package/src/store.ts +81 -8
- package/src/tools.ts +196 -65
- package/src/trigger-runtime.ts +164 -127
- package/src/triggers.ts +39 -7
- package/src/trust.ts +46 -2
- package/src/ui-prefs.ts +16 -5
- package/src/watch-plan.ts +174 -0
- package/src/web.d.mts +3 -0
- package/src/web.mjs +2260 -2340
- package/src/window-executor.ts +459 -0
- package/src/window-guard.ts +158 -0
- package/docs/agents/issue-tracker.md +0 -34
package/src/trust.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import { ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { withinProject } from "./
|
|
12
|
+
import { ProjectTrustStore, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { withinProject } from "./paths.ts";
|
|
14
14
|
|
|
15
15
|
/** pi's own normalisation: resolved, then through symlinks where the path exists. */
|
|
16
16
|
export function canonicalDir(dir: string): string {
|
|
@@ -22,6 +22,50 @@ export function canonicalDir(dir: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* pi's own saved decision for `cwd`: the nearest entry, this directory's or a parent's, so a
|
|
27
|
+
* directory distrusted inside a trusted parent stays distrusted. A held turn uses it, because its
|
|
28
|
+
* directory is the one the person opened pi in; a loop's may be one a model chose, and uses
|
|
29
|
+
* `isExactlyTrusted`. "This session only" is never saved, so it never counts.
|
|
30
|
+
*/
|
|
31
|
+
export function isTrustedByPi(agentDir: string, cwd: string): boolean {
|
|
32
|
+
if (!cwd) return false;
|
|
33
|
+
return new ProjectTrustStore(agentDir).get(cwd) === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `cwd` has anything that trust would change: what pi itself asks about (extensions,
|
|
38
|
+
* settings, skills), or pi-loops' own `.pi/mcp.toml` and `.pi/hooks.toml`. A directory with none of
|
|
39
|
+
* them loses nothing untrusted, and is not worth a warning.
|
|
40
|
+
*/
|
|
41
|
+
export function projectNeedsTrust(cwd: string): boolean {
|
|
42
|
+
if (!cwd) return false;
|
|
43
|
+
try {
|
|
44
|
+
if (hasTrustRequiringProjectResources(cwd)) return true;
|
|
45
|
+
} catch {
|
|
46
|
+
/* unreadable: fall through to our own files */
|
|
47
|
+
}
|
|
48
|
+
return fs.existsSync(path.join(cwd, ".pi", "mcp.toml")) || fs.existsSync(path.join(cwd, ".pi", "hooks.toml"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the window's project may load pi-loops' own project files (`.pi/mcp.toml`). pi answers
|
|
53
|
+
* `isProjectTrusted()` with yes in a directory that has nothing of pi's own to protect, without
|
|
54
|
+
* asking, so a cloned repository with only a `.pi/mcp.toml` would have its server command started
|
|
55
|
+
* on open. There a saved decision, or `pi --approve` on this command line, is required; where pi
|
|
56
|
+
* asked, its answer stands, and a no is no.
|
|
57
|
+
*/
|
|
58
|
+
export function windowTrusted(agentDir: string, cwd: string, piTrusted: boolean, approved = false): boolean {
|
|
59
|
+
if (!piTrusted || !cwd) return false;
|
|
60
|
+
let piAsked = false;
|
|
61
|
+
try {
|
|
62
|
+
piAsked = hasTrustRequiringProjectResources(cwd);
|
|
63
|
+
} catch {
|
|
64
|
+
/* unreadable: pi could not have asked either */
|
|
65
|
+
}
|
|
66
|
+
return piAsked || approved || isTrustedByPi(agentDir, cwd);
|
|
67
|
+
}
|
|
68
|
+
|
|
25
69
|
/** True only when the user trusted this very directory, not an ancestor of it. */
|
|
26
70
|
export function isExactlyTrusted(agentDir: string, cwd: string): boolean {
|
|
27
71
|
if (!cwd) return false;
|
package/src/ui-prefs.ts
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
* read-modify-write: the key is merged into whatever the file already holds, including keys this
|
|
10
10
|
* module has never heard of.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* open and read — and unknown keys are left exactly as they were found.
|
|
12
|
+
* The browser page's server (src/page-server.ts) imports this module too, so every writer of the
|
|
13
|
+
* file goes through the one merge here. It is JSON with a two-space indent and a trailing newline,
|
|
14
|
+
* kept because this is a file people open and read, and unknown keys are left exactly as they were
|
|
15
|
+
* found.
|
|
17
16
|
*/
|
|
18
17
|
import * as fs from "node:fs";
|
|
19
18
|
import * as path from "node:path";
|
|
@@ -30,6 +29,14 @@ export interface UiPrefs {
|
|
|
30
29
|
* made once and not in every window. `/pi-loops install-launcher` clears it when it writes one.
|
|
31
30
|
*/
|
|
32
31
|
launcher?: string;
|
|
32
|
+
/** `"stopped"` once a person stopped pi-loops by hand: no window starts it again until `pi-loops start`. */
|
|
33
|
+
host?: string;
|
|
34
|
+
/** `"shown"` once a session start has said that closed conversations' plain jobs now run in the host. */
|
|
35
|
+
heldJobsNotice?: string;
|
|
36
|
+
/** `"shown"` once a session start has named the plain jobs whose `--tools` now cap their turns. */
|
|
37
|
+
toolsCapNotice?: string;
|
|
38
|
+
/** The conversation the browser page last opened: what `GET /` opens. */
|
|
39
|
+
lastPage?: string;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const fileIn = (dir: string): string => path.join(dir, "ui.json");
|
|
@@ -58,6 +65,10 @@ export function readUiPrefs(dir: string): UiPrefs {
|
|
|
58
65
|
if (typeof doc.model === "string") prefs.model = doc.model;
|
|
59
66
|
if (typeof doc.thinking === "string") prefs.thinking = doc.thinking;
|
|
60
67
|
if (typeof doc.launcher === "string") prefs.launcher = doc.launcher;
|
|
68
|
+
if (typeof doc.host === "string") prefs.host = doc.host;
|
|
69
|
+
if (typeof doc.heldJobsNotice === "string") prefs.heldJobsNotice = doc.heldJobsNotice;
|
|
70
|
+
if (typeof doc.toolsCapNotice === "string") prefs.toolsCapNotice = doc.toolsCapNotice;
|
|
71
|
+
if (typeof doc.lastPage === "string") prefs.lastPage = doc.lastPage;
|
|
61
72
|
return prefs;
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A watch from one sentence, as a question: what `cron_create` and `new_trigger` ask the person
|
|
3
|
+
* through pi's own `select` before they create anything, in the person's own turn on every surface
|
|
4
|
+
* (docs/design.md decision 24). The plan, the card it is shown as, the dialog's title and options,
|
|
5
|
+
* and the one place an answer becomes the stores' fields (`watchChoice`). Pure, beside tools.ts,
|
|
6
|
+
* the job-edit.ts pattern: the tool asks, maps the answer and creates.
|
|
7
|
+
*/
|
|
8
|
+
import { dangerousCommandReason } from "./danger.ts";
|
|
9
|
+
import { redact } from "./redact.ts";
|
|
10
|
+
import { computeNext, formatDuration, formatLocal, formatSchedule, stamp } from "./schedule.ts";
|
|
11
|
+
import type { LoopJob } from "./store.ts";
|
|
12
|
+
import type { CreateJobInput } from "./tools.ts";
|
|
13
|
+
|
|
14
|
+
export type WatchWhere = "chat" | "inbox";
|
|
15
|
+
/** What the person cannot change on the card. `stateful` and `verify` follow the answer, so nothing holds the same choice twice; `verify` here is only what the model asked for, the first option's default. */
|
|
16
|
+
export type JobDraft = Omit<CreateJobInput, "stateful" | "recipe">;
|
|
17
|
+
export interface RuleDraft {
|
|
18
|
+
condition: string;
|
|
19
|
+
action: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
thinking?: string;
|
|
24
|
+
}
|
|
25
|
+
export type WatchPlan =
|
|
26
|
+
| { kind: "schedule"; job: JobDraft; where: WatchWhere }
|
|
27
|
+
| { kind: "rule"; rule: RuleDraft; where: WatchWhere; once: boolean };
|
|
28
|
+
|
|
29
|
+
/** What the card shows, computed once here: the page parses no schedule. */
|
|
30
|
+
export interface WatchCard {
|
|
31
|
+
kind: WatchPlan["kind"];
|
|
32
|
+
/** "every day at 09:00" for a schedule; the condition for a rule. */
|
|
33
|
+
when: string;
|
|
34
|
+
/** A schedule's next run, in local time. */
|
|
35
|
+
next?: string;
|
|
36
|
+
/** A rule: how often the background looks (`[triggers] poll_interval_secs`). */
|
|
37
|
+
looksEverySecs?: number;
|
|
38
|
+
/** The action, whole, redacted: the card is the consent, so it shows all of what would run. */
|
|
39
|
+
does: string;
|
|
40
|
+
/** A loop that asks for a checker: each finding is reviewed before it enters the inbox. */
|
|
41
|
+
verify?: boolean;
|
|
42
|
+
/** Why the unattended policy would refuse the action's command, when it would. */
|
|
43
|
+
danger?: string;
|
|
44
|
+
/** The terminal line that would do the same, whole and redacted. */
|
|
45
|
+
command: string;
|
|
46
|
+
}
|
|
47
|
+
/** A schedule in words, for the card's first row; an expression with no short reading is shown as it is. */
|
|
48
|
+
export function scheduleWords(schedule: LoopJob["schedule"]): string {
|
|
49
|
+
if (schedule.kind !== "cron") return formatSchedule(schedule);
|
|
50
|
+
const f = schedule.expr.trim().split(/\s+/);
|
|
51
|
+
if (f.length !== 5) return schedule.expr;
|
|
52
|
+
const [minute, hour, dom, month, dow] = f;
|
|
53
|
+
const plain = (v: string) => /^\d{1,2}$/.test(v);
|
|
54
|
+
const hhmm = () => `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
|
|
55
|
+
if (plain(minute) && plain(hour) && dom === "*" && month === "*") {
|
|
56
|
+
if (dow === "*") return `every day at ${hhmm()}`;
|
|
57
|
+
if (dow === "1-5" || dow === "mon-fri") return `weekdays at ${hhmm()}`;
|
|
58
|
+
if (dow === "1" || dow === "mon") return `every Monday at ${hhmm()}`;
|
|
59
|
+
}
|
|
60
|
+
const step = /^\*\/(\d+)$/.exec(minute);
|
|
61
|
+
if (step && hour === "*" && dom === "*" && month === "*" && dow === "*") return `every ${step[1]} minutes`;
|
|
62
|
+
if (minute === "0" && hour === "*" && dom === "*" && month === "*" && dow === "*") return "every hour";
|
|
63
|
+
return schedule.expr;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const BACKTICK = String.fromCharCode(96);
|
|
67
|
+
const CODE_SPAN = new RegExp(`${BACKTICK}([^${BACKTICK}]+)${BACKTICK}`, "g");
|
|
68
|
+
|
|
69
|
+
/** The unattended policy's reason over the action and each `code span` in it, when there is one. */
|
|
70
|
+
export function actionDanger(action: string, home?: string): string | undefined {
|
|
71
|
+
const candidates = [action, ...[...action.matchAll(CODE_SPAN)].map((m) => m[1])];
|
|
72
|
+
for (const c of candidates) {
|
|
73
|
+
const reason = dangerousCommandReason(c, home);
|
|
74
|
+
if (reason) return reason;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The terminal line that would do the same, whole and redacted: one line, since that is how it would be typed. */
|
|
80
|
+
const oneLine = (text: string) => redact(text).replace(/\s+/g, " ").trim();
|
|
81
|
+
|
|
82
|
+
/** What the card says, from the plan: the schedule in words and its next run, or the condition and how often the background looks. */
|
|
83
|
+
export function planCard(plan: WatchPlan, ctx: { now: number; pollSecs: number; home?: string }): WatchCard {
|
|
84
|
+
if (plan.kind === "schedule") {
|
|
85
|
+
const next = computeNext({ schedule: plan.job.schedule, createdAt: ctx.now }, ctx.now);
|
|
86
|
+
const spec = plan.job.schedule.kind === "cron" ? plan.job.schedule.expr : plan.job.schedule.kind === "every" ? `every ${formatDuration(plan.job.schedule.ms)}` : `at ${stamp(plan.job.schedule.at).slice(0, 16)}`;
|
|
87
|
+
return {
|
|
88
|
+
kind: "schedule",
|
|
89
|
+
when: scheduleWords(plan.job.schedule),
|
|
90
|
+
next: next === undefined ? undefined : formatLocal(next),
|
|
91
|
+
does: redact(plan.job.prompt),
|
|
92
|
+
verify: plan.job.verify || undefined,
|
|
93
|
+
danger: actionDanger(plan.job.prompt, ctx.home),
|
|
94
|
+
command: oneLine(`/cron add${plan.where === "inbox" ? " --stateful" : ""}${plan.job.name ? ` --name ${plan.job.name}` : ""} "${spec}" ${plan.job.prompt}`),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
kind: "rule",
|
|
99
|
+
when: redact(plan.rule.condition),
|
|
100
|
+
looksEverySecs: ctx.pollSecs,
|
|
101
|
+
does: redact(plan.rule.action),
|
|
102
|
+
danger: actionDanger(plan.rule.action, ctx.home),
|
|
103
|
+
// A condition the model wrote as "when …" already has its word.
|
|
104
|
+
command: oneLine(`/new-trigger when ${plan.rule.condition.replace(/^\s*when\s+/i, "")}, ${plan.rule.action}`),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The dialog's first line: how the page tells a watch question from any other select. */
|
|
109
|
+
export const WATCH_ASK_HEAD = "Set up this watch?";
|
|
110
|
+
/** How long the question waits for the person before pi cancels it and the tool says so. */
|
|
111
|
+
export const WATCH_ASK_MS = 30 * 60_000;
|
|
112
|
+
export const WATCH_CANCEL = "Cancel";
|
|
113
|
+
|
|
114
|
+
/** "every 10 minutes": how often the background looks, as the card says it. */
|
|
115
|
+
function looksWords(secs: number): string {
|
|
116
|
+
return secs >= 120 ? `every ${Math.round(secs / 60)} minutes` : secs === 60 ? "every minute" : `every ${secs} seconds`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The dialog's title: the head line, then the card's rows as `Label: value`, the same text a
|
|
121
|
+
* terminal shows and the page draws its card from. A value's further lines are indented two spaces,
|
|
122
|
+
* so a line inside an action never reads as a row of its own.
|
|
123
|
+
*/
|
|
124
|
+
export function askTitle(card: WatchCard): string {
|
|
125
|
+
const rows: [string, string][] = [];
|
|
126
|
+
if (card.kind === "rule") {
|
|
127
|
+
rows.push(["Waits for", card.when], ["Looks", looksWords(card.looksEverySecs ?? 600)]);
|
|
128
|
+
rows.push(["Then", card.does]);
|
|
129
|
+
} else {
|
|
130
|
+
rows.push(["Runs", `${card.when}${card.next ? ` · next ${card.next}` : ""}`], ["Does", card.does]);
|
|
131
|
+
}
|
|
132
|
+
if (card.danger) rows.push(["Careful", `When this runs nobody is asked: pi-loops refuses ${card.danger} and does the safe part.`]);
|
|
133
|
+
rows.push(["Command", card.command]);
|
|
134
|
+
return [WATCH_ASK_HEAD, ...rows.map(([label, value]) => `${label}: ${value.replace(/\r?\n/g, "\n ")}`)].join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A yes, as the card's choices: where the result goes, whether a rule fires once, whether a loop's findings are checked. */
|
|
138
|
+
export type WatchChoice = { verdict: "cancel" } | { verdict: "set"; where: WatchWhere; once: boolean; checks: boolean };
|
|
139
|
+
type SetChoice = Extract<WatchChoice, { verdict: "set" }>;
|
|
140
|
+
|
|
141
|
+
const WHERE_WORDS: Record<WatchWhere, string> = { chat: "this conversation", inbox: "inbox" };
|
|
142
|
+
const SCHEDULE_CHOICES: SetChoice[] = [
|
|
143
|
+
{ verdict: "set", where: "inbox", once: true, checks: false },
|
|
144
|
+
{ verdict: "set", where: "inbox", once: true, checks: true },
|
|
145
|
+
{ verdict: "set", where: "chat", once: true, checks: false },
|
|
146
|
+
];
|
|
147
|
+
const RULE_CHOICES: SetChoice[] = [
|
|
148
|
+
{ verdict: "set", where: "chat", once: true, checks: false },
|
|
149
|
+
{ verdict: "set", where: "chat", once: false, checks: false },
|
|
150
|
+
{ verdict: "set", where: "inbox", once: true, checks: false },
|
|
151
|
+
{ verdict: "set", where: "inbox", once: false, checks: false },
|
|
152
|
+
];
|
|
153
|
+
function optionText(kind: WatchPlan["kind"], c: SetChoice): string {
|
|
154
|
+
const extra = kind === "rule" ? (c.once ? " · once" : " · every time") : c.checks ? " · with checks" : "";
|
|
155
|
+
return `Set it up · ${WHERE_WORDS[c.where]}${extra}`;
|
|
156
|
+
}
|
|
157
|
+
/** Every answer that sets a watch up, and what it means: the whole vocabulary, so nothing else parses. */
|
|
158
|
+
const VOCABULARY = new Map<string, SetChoice>([...SCHEDULE_CHOICES.map((c) => [optionText("schedule", c), c] as const), ...RULE_CHOICES.map((c) => [optionText("rule", c), c] as const)]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The dialog's options: one per combination of the card's choices, so the page's chips are the axes
|
|
162
|
+
* of this set and Set it up sends the one option they name. The same strings every time; the plan's
|
|
163
|
+
* own defaults come first, which is what a terminal's selector highlights and the page's chips start on.
|
|
164
|
+
*/
|
|
165
|
+
export function watchOptions(plan: WatchPlan): string[] {
|
|
166
|
+
const all = plan.kind === "rule" ? RULE_CHOICES : SCHEDULE_CHOICES;
|
|
167
|
+
const first = plan.kind === "rule" ? all.find((c) => c.where === plan.where && c.once === plan.once)! : all.find((c) => c.where === plan.where && c.checks === (plan.where === "inbox" && plan.job.verify === true))!;
|
|
168
|
+
return [first, ...all.filter((c) => c !== first)].map((c) => optionText(plan.kind, c)).concat(WATCH_CANCEL);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The answer pi's select gave (`undefined` for Esc, an abort or the timeout), as the card's choices. Anything outside the vocabulary is a cancel. */
|
|
172
|
+
export function watchChoice(answer: string | undefined): WatchChoice {
|
|
173
|
+
return (answer !== undefined && VOCABULARY.get(answer)) || { verdict: "cancel" };
|
|
174
|
+
}
|
package/src/web.d.mts
ADDED