@penpair/core 0.1.3 → 0.3.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.
Files changed (149) hide show
  1. package/dist/adapters.d.ts +16 -1
  2. package/dist/adapters.js +62 -19
  3. package/dist/approvals.d.ts +32 -0
  4. package/dist/approvals.js +213 -0
  5. package/dist/atomic-output.d.ts +5 -0
  6. package/dist/atomic-output.js +33 -0
  7. package/dist/audit-rules.d.ts +60 -0
  8. package/dist/audit-rules.js +443 -0
  9. package/dist/backup-note.d.ts +4 -0
  10. package/dist/backup-note.js +49 -0
  11. package/dist/baseline-coverage.d.ts +25 -0
  12. package/dist/baseline-coverage.js +248 -0
  13. package/dist/baselines-config.d.ts +25 -0
  14. package/dist/baselines-config.js +150 -11
  15. package/dist/baselines.d.ts +37 -8
  16. package/dist/baselines.js +362 -59
  17. package/dist/cli-arguments.d.ts +7 -0
  18. package/dist/cli-arguments.js +47 -0
  19. package/dist/cli-main.d.ts +13 -1
  20. package/dist/cli-main.js +2107 -247
  21. package/dist/client.d.ts +30 -6
  22. package/dist/client.js +160 -35
  23. package/dist/command-result.d.ts +159 -0
  24. package/dist/command-result.js +468 -0
  25. package/dist/coordination.d.ts +15 -0
  26. package/dist/coordination.js +184 -0
  27. package/dist/credentials.d.ts +21 -0
  28. package/dist/credentials.js +105 -0
  29. package/dist/deep-check.js +57 -9
  30. package/dist/design-spec.d.ts +55 -0
  31. package/dist/design-spec.js +275 -0
  32. package/dist/endpoint.d.ts +8 -1
  33. package/dist/endpoint.js +133 -21
  34. package/dist/errors.d.ts +2 -1
  35. package/dist/errors.js +8 -4
  36. package/dist/export.d.ts +18 -0
  37. package/dist/export.js +113 -10
  38. package/dist/file-archive.d.ts +16 -0
  39. package/dist/file-archive.js +58 -0
  40. package/dist/file-guard.d.ts +8 -0
  41. package/dist/file-guard.js +58 -0
  42. package/dist/formats.d.ts +4 -0
  43. package/dist/formats.js +229 -0
  44. package/dist/index.d.ts +34 -1
  45. package/dist/index.js +34 -1
  46. package/dist/inspection.d.ts +106 -0
  47. package/dist/inspection.js +657 -0
  48. package/dist/instance/config-schema.d.ts +422 -0
  49. package/dist/instance/config-schema.js +435 -0
  50. package/dist/instance/config.d.ts +29 -0
  51. package/dist/instance/config.js +191 -29
  52. package/dist/instance/connect.d.ts +10 -1
  53. package/dist/instance/connect.js +178 -30
  54. package/dist/instance/connection.d.ts +39 -4
  55. package/dist/instance/connection.js +198 -14
  56. package/dist/instance/gitignore.js +5 -2
  57. package/dist/instance/init.js +32 -7
  58. package/dist/instance/runner.js +32 -10
  59. package/dist/instance/setup.js +46 -13
  60. package/dist/instance/tty.js +28 -8
  61. package/dist/journal.d.ts +87 -0
  62. package/dist/journal.js +423 -0
  63. package/dist/kit-smoke.d.ts +31 -0
  64. package/dist/kit-smoke.js +145 -0
  65. package/dist/kit-source.d.ts +2 -0
  66. package/dist/kit-source.js +279 -0
  67. package/dist/kit.d.ts +25 -0
  68. package/dist/kit.js +158 -0
  69. package/dist/layout-policy.d.ts +2 -0
  70. package/dist/layout-policy.js +86 -0
  71. package/dist/layout.d.ts +21 -0
  72. package/dist/layout.js +36 -0
  73. package/dist/live-smoke.d.ts +8 -0
  74. package/dist/live-smoke.js +23 -0
  75. package/dist/local-profile.js +2 -1
  76. package/dist/mcp-server.d.ts +12 -0
  77. package/dist/mcp-server.js +415 -0
  78. package/dist/native-api.d.ts +15 -0
  79. package/dist/native-api.js +78 -0
  80. package/dist/onboard.d.ts +10 -0
  81. package/dist/onboard.js +41 -0
  82. package/dist/operation.d.ts +22 -3
  83. package/dist/operation.js +341 -31
  84. package/dist/page-session.d.ts +23 -0
  85. package/dist/page-session.js +142 -0
  86. package/dist/png.d.ts +5 -0
  87. package/dist/png.js +22 -0
  88. package/dist/portable-tokens.d.ts +22 -0
  89. package/dist/portable-tokens.js +331 -0
  90. package/dist/probe.d.ts +6 -2
  91. package/dist/probe.js +64 -11
  92. package/dist/project-input.d.ts +6 -0
  93. package/dist/project-input.js +65 -0
  94. package/dist/project-snippets.js +29 -12
  95. package/dist/project-status.d.ts +24 -0
  96. package/dist/project-status.js +188 -0
  97. package/dist/registry.d.ts +2 -1
  98. package/dist/registry.js +224 -53
  99. package/dist/run-file.d.ts +26 -0
  100. package/dist/run-file.js +199 -0
  101. package/dist/shots.d.ts +43 -0
  102. package/dist/shots.js +172 -0
  103. package/dist/skill-instructions.d.ts +13 -0
  104. package/dist/skill-instructions.js +144 -0
  105. package/dist/skills.d.ts +7 -0
  106. package/dist/skills.js +163 -46
  107. package/dist/snippet.d.ts +3 -2
  108. package/dist/snippet.js +33 -9
  109. package/dist/snippets/archive.d.ts +9 -0
  110. package/dist/snippets/archive.js +10 -4
  111. package/dist/snippets/read.d.ts +1 -1
  112. package/dist/snippets/read.js +78 -41
  113. package/dist/structure-snapshot.d.ts +85 -0
  114. package/dist/structure-snapshot.js +525 -0
  115. package/dist/token-edit.d.ts +53 -0
  116. package/dist/token-edit.js +352 -0
  117. package/dist/token-projection.d.ts +56 -0
  118. package/dist/token-projection.js +286 -0
  119. package/dist/tokens-diff.d.ts +19 -6
  120. package/dist/tokens-diff.js +182 -18
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +1 -1
  124. package/skills/penpair-design/SKILL.md +20 -45
  125. package/skills/penpair-design/references/penpot-plugin-gotchas.md +58 -2
  126. package/skills/penpair-design/references/project-facts.md +3 -1
  127. package/skills/penpair-design/references/write-protocol.md +15 -3
  128. package/skills/penpair-handoff/SKILL.md +16 -0
  129. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  130. package/skills/penpair-handoff/references/handoff-flow.md +13 -0
  131. package/skills/penpair-recipes/SKILL.md +21 -0
  132. package/skills/penpair-recipes/references/build-flow.md +11 -0
  133. package/skills/penpair-setup/SKILL.md +8 -2
  134. package/dist/cli.d.ts +0 -2
  135. package/dist/cli.js +0 -3
  136. package/dist/docker/lifecycle.d.ts +0 -96
  137. package/dist/docker/lifecycle.js +0 -273
  138. package/dist/docker/secrets.d.ts +0 -11
  139. package/dist/docker/secrets.js +0 -38
  140. package/dist/docker/template.d.ts +0 -8
  141. package/dist/docker/template.js +0 -158
  142. package/dist/docker-profile.d.ts +0 -114
  143. package/dist/docker-profile.js +0 -18
  144. package/dist/instance/lifecycle.d.ts +0 -129
  145. package/dist/instance/lifecycle.js +0 -336
  146. package/dist/instance/secrets.d.ts +0 -11
  147. package/dist/instance/secrets.js +0 -38
  148. package/dist/instance/template.d.ts +0 -8
  149. package/dist/instance/template.js +0 -158
package/dist/client.d.ts CHANGED
@@ -1,22 +1,38 @@
1
1
  import { type Endpoint } from "./endpoint.js";
2
- import { type PenpairErrorCode } from "./errors.js";
2
+ import { PenpairError, type PenpairErrorCode } from "./errors.js";
3
+ export interface ExecutionMetrics {
4
+ requestSnippetBytes: number;
5
+ responsePayloadBytes: number;
6
+ }
3
7
  export interface ToolOutcome {
8
+ metrics?: ExecutionMetrics;
4
9
  text: string;
10
+ structuredContent?: unknown;
5
11
  isError: boolean;
6
12
  }
7
13
  /** The only Penpot surface penpair needs: one tool call at a time on an already connected editor. */
14
+ export interface ToolCallOptions {
15
+ timeoutMs?: number;
16
+ }
8
17
  export interface PenpotToolClient {
9
- callTool(name: string, args: Record<string, unknown>): Promise<ToolOutcome>;
18
+ callTool(name: string, args: Record<string, unknown>, options?: ToolCallOptions): Promise<ToolOutcome>;
10
19
  /** The names of the tools the server offers. A hosted Penpot may offer fewer than a local one. */
11
20
  listTools?(): Promise<string[]>;
12
21
  close(): Promise<void>;
13
22
  }
14
23
  /** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
15
24
  export declare const maxSnippetBytes: number;
16
- export declare function connect(endpoint: Endpoint): Promise<PenpotToolClient>;
25
+ export declare function connect(endpoint: Endpoint, options?: {
26
+ timeoutMs?: number;
27
+ signal?: AbortSignal;
28
+ }): Promise<PenpotToolClient>;
17
29
  /** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
18
30
  export declare function classifyFailure(text: string): PenpairErrorCode;
19
31
  export interface ExecuteOptions {
32
+ /** Per-request budget; adapters/guards must forward it. */
33
+ timeoutMs?: number;
34
+ /** Only known read-only executions may be retried. Unclassified source is treated as a possible write. */
35
+ effect?: "read" | "write" | "unknown";
20
36
  /** How many times to wait and retry a suspended tab. */
21
37
  retries?: number;
22
38
  sleep?: (milliseconds: number) => Promise<void>;
@@ -25,13 +41,21 @@ export interface ExecuteOptions {
25
41
  redact?: (text: string) => string;
26
42
  }
27
43
  export interface ExecuteResult {
44
+ metrics?: ExecutionMetrics;
45
+ runId: string;
46
+ outcome: "ran";
28
47
  /** What the snippet returned, parsed when it was JSON. */
29
48
  result: unknown;
30
49
  /** What the snippet printed. */
31
50
  log: string;
32
51
  }
52
+ export declare function executionEnvelope(outcome: ToolOutcome): Pick<ExecuteResult, "result" | "log"> | undefined;
33
53
  /**
34
- * Runs a snippet inside the connected Penpot editor. A suspended tab is waited out and retried, because the snippet may
35
- * already have run; the caller reads the page before repeating a write.
54
+ * Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
36
55
  */
37
- export declare function executeCode(client: PenpotToolClient, code: string, { retries, sleep, onWait, redact }?: ExecuteOptions): Promise<ExecuteResult>;
56
+ export declare function executeCode(client: PenpotToolClient, code: string, { timeoutMs, effect, retries: requestedRetries, sleep, onWait, redact, }?: ExecuteOptions): Promise<ExecuteResult>;
57
+ export declare class UncertainExecutionError extends PenpairError {
58
+ readonly runId: string;
59
+ readonly outcome = "uncertain";
60
+ constructor(runId: string, message: string);
61
+ }
package/dist/client.js CHANGED
@@ -1,29 +1,81 @@
1
+ import { commandSignal, commandSleep } from "./command-result.js";
1
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
5
  import { redactEndpoint } from "./endpoint.js";
4
6
  import { PenpairError } from "./errors.js";
5
7
  /** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
6
8
  export const maxSnippetBytes = 90 * 1024;
7
- export async function connect(endpoint) {
8
- const client = new Client({ name: "penpair", version: "0" });
9
+ export async function connect(endpoint, options = {}) {
10
+ commandSignal()?.throwIfAborted();
11
+ const signal = options.signal
12
+ ? AbortSignal.any([
13
+ options.signal,
14
+ ...(commandSignal()
15
+ ? [
16
+ commandSignal(),
17
+ ]
18
+ : []),
19
+ ])
20
+ : commandSignal();
21
+ const client = new Client({
22
+ name: "penpair",
23
+ version: "0",
24
+ });
9
25
  const transport = new StreamableHTTPClientTransport(new URL(endpoint.url));
10
26
  try {
11
- await client.connect(transport);
27
+ await client.connect(transport, {
28
+ signal,
29
+ timeout: options.timeoutMs ?? 120_000,
30
+ });
12
31
  }
13
32
  catch (error) {
33
+ await client.close().catch(() => undefined);
14
34
  throw transportFailure(error, endpoint);
15
35
  }
16
36
  return {
17
- async callTool(name, args) {
37
+ async callTool(name, args, options) {
18
38
  try {
19
- const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 120_000 });
39
+ const result = await client.callTool({
40
+ name,
41
+ arguments: args,
42
+ }, undefined, {
43
+ timeout: options?.timeoutMs ?? 120_000,
44
+ signal: signal && commandSignal()
45
+ ? AbortSignal.any([
46
+ signal,
47
+ commandSignal(),
48
+ ])
49
+ : (signal ?? commandSignal()),
50
+ });
20
51
  const content = Array.isArray(result.content) ? result.content : [];
21
52
  const text = content
22
- .flatMap((item) => typeof item === "object" && item !== null && "type" in item && item.type === "text" && "text" in item
23
- ? [String(item.text)]
53
+ .flatMap((item) => typeof item === "object" &&
54
+ item !== null &&
55
+ "type" in item &&
56
+ item.type === "text" &&
57
+ "text" in item
58
+ ? [
59
+ String(item.text),
60
+ ]
24
61
  : [])
25
62
  .join("\n");
26
- return { text, isError: result.isError === true };
63
+ return {
64
+ text,
65
+ metrics: {
66
+ requestSnippetBytes: typeof args.code === "string" ? Buffer.byteLength(args.code, "utf8") : 0,
67
+ responsePayloadBytes: Buffer.byteLength(text, "utf8") +
68
+ (result.structuredContent === undefined
69
+ ? 0
70
+ : Buffer.byteLength(JSON.stringify(result.structuredContent), "utf8")),
71
+ },
72
+ ...(result.structuredContent !== undefined
73
+ ? {
74
+ structuredContent: result.structuredContent,
75
+ }
76
+ : {}),
77
+ isError: result.isError === true,
78
+ };
27
79
  }
28
80
  catch (error) {
29
81
  throw transportFailure(error, endpoint);
@@ -31,7 +83,10 @@ export async function connect(endpoint) {
31
83
  },
32
84
  async listTools() {
33
85
  try {
34
- return (await client.listTools()).tools.map((tool) => tool.name);
86
+ return (await client.listTools(undefined, {
87
+ signal: commandSignal(),
88
+ timeout: 120_000,
89
+ })).tools.map((tool) => tool.name);
35
90
  }
36
91
  catch (error) {
37
92
  throw transportFailure(error, endpoint);
@@ -43,58 +98,96 @@ export async function connect(endpoint) {
43
98
  };
44
99
  }
45
100
  function transportFailure(error, endpoint) {
101
+ if (commandSignal()?.aborted) {
102
+ return commandSignal().reason;
103
+ }
46
104
  const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
47
105
  const message = redactEndpoint(raw, endpoint);
48
- if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403))
106
+ if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
49
107
  return new PenpairError("auth-rejected", message);
108
+ }
50
109
  const code = classifyFailure(message);
51
110
  return new PenpairError(code === "tool-failed" ? "mcp-unavailable" : code, message);
52
111
  }
53
112
  /** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
54
113
  export function classifyFailure(text) {
55
- if (/suspended|no heartbeat/i.test(text))
114
+ if (/suspended|no heartbeat/i.test(text)) {
56
115
  return "plugin-suspended";
57
- if (/not connected|no (?:penpot )?plugin|no client|plugin (?:is )?(?:not|disconnected)|connect the plugin/i.test(text))
116
+ }
117
+ if (/not connected|no (?:penpot )?plugin|no client|plugin (?:is )?(?:not|disconnected)|connect the plugin/i.test(text)) {
58
118
  return "plugin-disconnected";
59
- if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text))
119
+ }
120
+ if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text)) {
60
121
  return "auth-rejected";
61
- if (/ECONNREFUSED|ENOTFOUND|fetch failed|EHOSTUNREACH|socket hang up|ECONNRESET|timed? ?out/i.test(text))
122
+ }
123
+ if (/ECONNREFUSED|ENOTFOUND|fetch failed|EHOSTUNREACH|socket hang up|ECONNRESET|timed? ?out/i.test(text)) {
62
124
  return "mcp-unavailable";
125
+ }
63
126
  return "tool-failed";
64
127
  }
65
- const defaultSleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
66
- function unpack(text) {
67
- let envelope;
68
- try {
69
- envelope = JSON.parse(text);
128
+ const defaultSleep = commandSleep;
129
+ export function executionEnvelope(outcome) {
130
+ let envelope = outcome.structuredContent;
131
+ if (envelope === undefined) {
132
+ try {
133
+ envelope = JSON.parse(outcome.text);
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
70
138
  }
71
- catch {
139
+ if (typeof envelope !== "object" ||
140
+ envelope === null ||
141
+ Array.isArray(envelope) ||
142
+ !("result" in envelope) ||
143
+ ("log" in envelope && typeof envelope.log !== "string")) {
72
144
  return undefined;
73
145
  }
74
- if (typeof envelope !== "object" || envelope === null || !("result" in envelope))
75
- return undefined;
76
146
  const log = "log" in envelope && typeof envelope.log === "string" ? envelope.log : "";
77
147
  const raw = envelope.result;
78
- if (typeof raw !== "string")
79
- return { result: raw, log };
148
+ if (typeof raw !== "string") {
149
+ return {
150
+ result: raw,
151
+ log,
152
+ };
153
+ }
80
154
  try {
81
- return { result: JSON.parse(raw), log };
155
+ return {
156
+ result: JSON.parse(raw),
157
+ log,
158
+ };
82
159
  }
83
160
  catch {
84
- return { result: raw, log };
161
+ return {
162
+ result: raw,
163
+ log,
164
+ };
85
165
  }
86
166
  }
87
167
  /**
88
- * Runs a snippet inside the connected Penpot editor. A suspended tab is waited out and retried, because the snippet may
89
- * already have run; the caller reads the page before repeating a write.
168
+ * Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
90
169
  */
91
- export async function executeCode(client, code, { retries = 6, sleep = defaultSleep, onWait, redact = (text) => text } = {}) {
92
- if (Buffer.byteLength(code, "utf8") > maxSnippetBytes)
170
+ export async function executeCode(client, code, { timeoutMs = 120_000, effect = "unknown", retries: requestedRetries = 6, sleep = defaultSleep, onWait, redact = (text) => text, } = {}) {
171
+ commandSignal()?.throwIfAborted();
172
+ if (Buffer.byteLength(code, "utf8") > maxSnippetBytes) {
93
173
  throw new PenpairError("snippet-too-large", `The snippet is ${Buffer.byteLength(code, "utf8")} bytes`);
174
+ }
175
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
176
+ throw new Error("Execution timeout must be from 1 to 120000 ms");
177
+ }
178
+ const retries = effect === "read" ? requestedRetries : 0;
179
+ const runId = randomUUID();
180
+ const uncertain = (message) => new UncertainExecutionError(runId, redact(message));
94
181
  for (let attempt = 0;; attempt++) {
95
182
  let outcome;
96
183
  try {
97
- outcome = await client.callTool("execute_code", { code });
184
+ commandSignal()?.throwIfAborted();
185
+ outcome = await client.callTool("execute_code", {
186
+ code,
187
+ }, {
188
+ timeoutMs,
189
+ });
190
+ commandSignal()?.throwIfAborted();
98
191
  }
99
192
  catch (error) {
100
193
  if (error instanceof PenpairError && error.code === "plugin-suspended" && attempt < retries) {
@@ -102,18 +195,50 @@ export async function executeCode(client, code, { retries = 6, sleep = defaultSl
102
195
  await sleep(5_000 * (attempt + 1));
103
196
  continue;
104
197
  }
198
+ if (effect !== "read" &&
199
+ (!(error instanceof PenpairError) ||
200
+ ![
201
+ "file-mismatch",
202
+ "snippet-too-large",
203
+ "auth-rejected",
204
+ ].includes(error.code))) {
205
+ throw uncertain(error instanceof Error ? error.message : String(error));
206
+ }
105
207
  throw error;
106
208
  }
107
- const unpacked = unpack(outcome.text);
209
+ const unpacked = executionEnvelope(outcome);
108
210
  const failed = outcome.isError || (!unpacked && /^Tool execution failed:/i.test(outcome.text.trim()));
109
- if (!failed && unpacked)
110
- return unpacked;
211
+ if (!failed && unpacked) {
212
+ return {
213
+ ...unpacked,
214
+ runId,
215
+ outcome: "ran",
216
+ metrics: outcome.metrics ?? {
217
+ requestSnippetBytes: Buffer.byteLength(code, "utf8"),
218
+ responsePayloadBytes: Buffer.byteLength(outcome.text, "utf8") +
219
+ (outcome.structuredContent === undefined
220
+ ? 0
221
+ : Buffer.byteLength(JSON.stringify(outcome.structuredContent), "utf8")),
222
+ },
223
+ };
224
+ }
111
225
  const code_ = classifyFailure(outcome.text);
112
226
  if (code_ === "plugin-suspended" && attempt < retries) {
113
227
  onWait?.(`Penpot tab is suspended; bring it to the foreground (retry ${attempt + 1}/${retries})`);
114
228
  await sleep(5_000 * (attempt + 1));
115
229
  continue;
116
230
  }
231
+ if (effect !== "read" && code_ !== "auth-rejected") {
232
+ throw uncertain(outcome.text.slice(0, 500));
233
+ }
117
234
  throw new PenpairError(code_, redact(outcome.text).slice(0, 500));
118
235
  }
119
236
  }
237
+ export class UncertainExecutionError extends PenpairError {
238
+ runId;
239
+ outcome = "uncertain";
240
+ constructor(runId, message) {
241
+ super("write-uncertain", `Run ${runId} has an uncertain outcome: ${message}`);
242
+ this.runId = runId;
243
+ }
244
+ }
@@ -0,0 +1,159 @@
1
+ export interface CommandFailure {
2
+ code: string;
3
+ message: string;
4
+ remedy: string;
5
+ runId?: string;
6
+ }
7
+ export interface CommandResult {
8
+ schemaVersion: 1;
9
+ redacted?: boolean;
10
+ command: string;
11
+ outcome: "success" | "drift" | "failed" | "incomplete" | "uncertain" | "cancelled" | "deadline";
12
+ phase: string;
13
+ exitCode: number;
14
+ target: {
15
+ fileId?: string;
16
+ pageId?: string;
17
+ } | null;
18
+ runId: string | null;
19
+ data: unknown;
20
+ messages: string[];
21
+ warnings: string[];
22
+ errors: CommandFailure[];
23
+ completeness: {
24
+ complete: boolean;
25
+ reason?: string;
26
+ };
27
+ timings: {
28
+ startedAt: string;
29
+ durationMs: number;
30
+ };
31
+ }
32
+ export interface CommandOutput {
33
+ log(...values: unknown[]): void;
34
+ error(...values: unknown[]): void;
35
+ result(value: unknown): void;
36
+ }
37
+ export declare const commandOutput: CommandOutput;
38
+ export declare function commandRedactor(redact: (value: string) => string): void;
39
+ export declare function commandFailure(error: unknown): void;
40
+ export declare function withCommandSignal<T>(signal: AbortSignal, work: () => Promise<T>): Promise<T>;
41
+ /** Bounded explicit restoration work may finish after the main command is cancelled. */
42
+ export declare function withCleanupDeadline<T>(work: () => Promise<T>, milliseconds?: number): Promise<T>;
43
+ export declare function commandSignal(): AbortSignal | undefined;
44
+ export declare function commandSleep(milliseconds: number): Promise<void>;
45
+ export declare class CommandInterruptedError extends Error {
46
+ readonly code: "cancelled" | "deadline";
47
+ constructor(code: "cancelled" | "deadline");
48
+ }
49
+ /** Shared service boundary for CLI and future tool surfaces; no stdout or global console interception. */
50
+ export declare function collectCommand(command: string, work: () => Promise<number>, options?: {
51
+ signal?: AbortSignal;
52
+ }): Promise<CommandResult>;
53
+ /** Outer transport contract. Command-specific data remains owned by the command service. */
54
+ export declare const commandResultSchema: {
55
+ readonly type: "object";
56
+ readonly additionalProperties: false;
57
+ readonly required: readonly ["schemaVersion", "command", "outcome", "phase", "exitCode", "target", "runId", "data", "messages", "warnings", "errors", "completeness", "timings"];
58
+ readonly properties: {
59
+ readonly schemaVersion: {
60
+ readonly const: 1;
61
+ };
62
+ readonly redacted: {
63
+ readonly type: "boolean";
64
+ };
65
+ readonly command: {
66
+ readonly type: "string";
67
+ };
68
+ readonly outcome: {
69
+ readonly enum: readonly ["success", "drift", "failed", "incomplete", "uncertain", "cancelled", "deadline"];
70
+ };
71
+ readonly phase: {
72
+ readonly type: "string";
73
+ };
74
+ readonly exitCode: {
75
+ readonly enum: readonly [0, 1, 2, 3, 4, 124, 130];
76
+ };
77
+ readonly target: {
78
+ readonly anyOf: readonly [{
79
+ readonly type: "null";
80
+ }, {
81
+ readonly type: "object";
82
+ readonly properties: {
83
+ readonly fileId: {
84
+ readonly type: "string";
85
+ };
86
+ readonly pageId: {
87
+ readonly type: "string";
88
+ };
89
+ };
90
+ readonly additionalProperties: false;
91
+ }];
92
+ };
93
+ readonly runId: {
94
+ readonly type: readonly ["string", "null"];
95
+ };
96
+ readonly data: {};
97
+ readonly messages: {
98
+ readonly type: "array";
99
+ readonly items: {
100
+ readonly type: "string";
101
+ };
102
+ };
103
+ readonly warnings: {
104
+ readonly type: "array";
105
+ readonly items: {
106
+ readonly type: "string";
107
+ };
108
+ };
109
+ readonly errors: {
110
+ readonly type: "array";
111
+ readonly items: {
112
+ readonly type: "object";
113
+ readonly required: readonly ["code", "message", "remedy"];
114
+ readonly properties: {
115
+ readonly code: {
116
+ readonly type: "string";
117
+ };
118
+ readonly message: {
119
+ readonly type: "string";
120
+ };
121
+ readonly remedy: {
122
+ readonly type: "string";
123
+ };
124
+ readonly runId: {
125
+ readonly type: "string";
126
+ };
127
+ };
128
+ readonly additionalProperties: false;
129
+ };
130
+ };
131
+ readonly completeness: {
132
+ readonly type: "object";
133
+ readonly required: readonly ["complete"];
134
+ readonly properties: {
135
+ readonly complete: {
136
+ readonly type: "boolean";
137
+ };
138
+ readonly reason: {
139
+ readonly type: "string";
140
+ };
141
+ };
142
+ readonly additionalProperties: false;
143
+ };
144
+ readonly timings: {
145
+ readonly type: "object";
146
+ readonly required: readonly ["startedAt", "durationMs"];
147
+ readonly properties: {
148
+ readonly startedAt: {
149
+ readonly type: "string";
150
+ };
151
+ readonly durationMs: {
152
+ readonly type: "number";
153
+ readonly minimum: 0;
154
+ };
155
+ };
156
+ readonly additionalProperties: false;
157
+ };
158
+ };
159
+ };