@actiondock/core 2.11.0 → 2.13.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.
@@ -88,11 +88,22 @@ export declare function buildActionDescribePayload(spec: {
88
88
  * 扁平输入通用语法速查行列表。
89
89
  */
90
90
  export declare const ACTION_DESCRIBE_SYNTAX_REFERENCE: readonly string[];
91
+ /**
92
+ * 格式化 Action 详情的可选配置项。
93
+ */
94
+ export interface FormatActionDetailOptions {
95
+ /**
96
+ * 是否支持显式与声明式正文选择运行选项(普通 CLI 为 true,目录型 Standalone 默认为 false)。
97
+ * @default true
98
+ */
99
+ supportsTextField?: boolean;
100
+ }
91
101
  /**
92
102
  * 格式化渲染 Action 详情人类可读文本。
93
103
  * 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
94
104
  *
95
105
  * @param input Action 描述载荷或 Action 规范
106
+ * @param options 可选格式化配置项
96
107
  * @returns 格式化后的说明文本
97
108
  */
98
109
  export declare function formatActionDetail(input: ActionDescribePayload | (Partial<ActionSpec> & {
@@ -109,4 +120,4 @@ export declare function formatActionDetail(input: ActionDescribePayload | (Parti
109
120
  uses?: string[];
110
121
  entry?: string;
111
122
  [key: string]: any;
112
- }): string;
123
+ }, options?: FormatActionDetailOptions): string;
@@ -1,5 +1,6 @@
1
1
  import { buildCliInputAdviceV1 } from "./advice.js";
2
2
  import { isForbiddenActionInputPropertyName } from "./flat-predicates.js";
3
+ import { analyzeActionOutputContract } from "./output-contract.js";
3
4
  /**
4
5
  * 计算 Action 输入建议。
5
6
  *
@@ -134,9 +135,10 @@ export const ACTION_DESCRIBE_SYNTAX_REFERENCE = [
134
135
  * 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
135
136
  *
136
137
  * @param input Action 描述载荷或 Action 规范
138
+ * @param options 可选格式化配置项
137
139
  * @returns 格式化后的说明文本
138
140
  */
139
- export function formatActionDetail(input) {
141
+ export function formatActionDetail(input, options) {
140
142
  const payload = "inputAdvice" in input && input.inputAdvice
141
143
  ? input
142
144
  : buildActionDescribePayload(input);
@@ -197,5 +199,57 @@ export function formatActionDetail(input) {
197
199
  lines.push(` - ${issue.path}: ${issue.code}`);
198
200
  }
199
201
  }
202
+ const supportsTextField = options?.supportsTextField ?? true;
203
+ const outputContract = analyzeActionOutputContract({
204
+ annotations: payload.annotations,
205
+ outputSchema: payload.outputSchema,
206
+ });
207
+ const isAnnotationInvalid = outputContract.diagnostics.some((d) => d.code === "INVALID_ANNOTATION");
208
+ lines.push("");
209
+ lines.push("Output Selection:");
210
+ if (supportsTextField) {
211
+ if (isAnnotationInvalid) {
212
+ lines.push(` Invalid annotation: ${outputContract.errors.join("; ")}`);
213
+ lines.push(" Default run: Synchronous execution will be rejected before invocation due to invalid annotation");
214
+ lines.push(" Full result: Pass '--json' to bypass default annotation and receive the complete structured envelope");
215
+ lines.push(" Explicit text field: Pass '--text-field <field>' to bypass default annotation and select a specific field");
216
+ }
217
+ else if (outputContract.declared && outputContract.textField) {
218
+ lines.push(` Default text field: ${outputContract.textField}`);
219
+ if (outputContract.errors.length > 0) {
220
+ lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
221
+ }
222
+ lines.push(` stdout: Raw text content of '${outputContract.textField}' (synchronous execution only)`);
223
+ lines.push(" stderr: Remaining fields as JSON metadata, diagnostics, and logs");
224
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
225
+ lines.push(" Override: Pass '--text-field <field>' to select another top-level string field");
226
+ }
227
+ else {
228
+ lines.push(" Default: Raw string for string results; formatted JSON for objects and arrays");
229
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
230
+ lines.push(" Custom field: Pass '--text-field <field>' to extract a specific top-level string field");
231
+ }
232
+ }
233
+ else {
234
+ if (isAnnotationInvalid) {
235
+ lines.push(` Invalid annotation: ${outputContract.errors.join("; ")} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
236
+ lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
237
+ lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
238
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
239
+ }
240
+ else if (outputContract.declared && outputContract.textField) {
241
+ lines.push(` Default text field: ${outputContract.textField} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
242
+ if (outputContract.errors.length > 0) {
243
+ lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
244
+ }
245
+ lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
246
+ lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
247
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
248
+ }
249
+ else {
250
+ lines.push(" Default: Raw string for scalars; extracts 'content', 'text', or 'message' from objects (with metadata on stderr), falling back to formatted JSON");
251
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
252
+ }
253
+ }
200
254
  return lines.join("\n");
201
255
  }
@@ -16,6 +16,30 @@ export type NodeState = (typeof NodeState)[keyof typeof NodeState];
16
16
  export interface FlatMaterializerOptions {
17
17
  maxMaterializedSizeBytes?: number;
18
18
  }
19
+ /**
20
+ * 断言顶层字符串属性写入不与既有字段结构冲突。
21
+ *
22
+ * 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
23
+ * - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
24
+ * - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
25
+ * - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
26
+ *
27
+ * @param target 目标对象(仅检测自有属性,不触及原型链)
28
+ * @param field 待写入的顶层属性名
29
+ */
30
+ export declare function assertTopLevelLeafAvailable(target: object, field: string): void;
31
+ /**
32
+ * 校验物化后的最终入参值:结构合法性与实体化总大小上限。
33
+ *
34
+ * 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
35
+ * - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
36
+ * - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
37
+ * FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
38
+ *
39
+ * @param result 待校验的最终入参对象
40
+ * @param options 物化配置选项(可选 maxMaterializedSizeBytes)
41
+ */
42
+ export declare function assertMaterializedInputWithinLimits(result: JsonValue, options?: FlatMaterializerOptions): void;
19
43
  /**
20
44
  * 将扁平赋值表达式列表物化为标准 JSON 值对象。
21
45
  *
@@ -11,6 +11,56 @@ export const NodeState = {
11
11
  ARRAY: "ARRAY",
12
12
  VALUE: "VALUE",
13
13
  };
14
+ /**
15
+ * 断言顶层字符串属性写入不与既有字段结构冲突。
16
+ *
17
+ * 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
18
+ * - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
19
+ * - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
20
+ * - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
21
+ *
22
+ * @param target 目标对象(仅检测自有属性,不触及原型链)
23
+ * @param field 待写入的顶层属性名
24
+ */
25
+ export function assertTopLevelLeafAvailable(target, field) {
26
+ if (!Object.hasOwn(target, field))
27
+ return;
28
+ const existing = target[field];
29
+ if (Array.isArray(existing)) {
30
+ throw inputPathConflict(`Path conflict at "${field}": expected object but found array`, { path: field, reason: "OBJECT_ARRAY_CONFLICT" });
31
+ }
32
+ if (existing !== null && typeof existing === "object") {
33
+ throw inputPathConflict(`Path conflict at "${field}": cannot assign value to existing container (object)`, { path: field, reason: "LEAF_CONTAINER_CONFLICT" });
34
+ }
35
+ throw inputPathConflict(`Duplicate assignment to leaf path "${field}"`, { path: field, reason: "DUPLICATE_ASSIGNMENT" });
36
+ }
37
+ /**
38
+ * 校验物化后的最终入参值:结构合法性与实体化总大小上限。
39
+ *
40
+ * 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
41
+ * - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
42
+ * - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
43
+ * FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
44
+ *
45
+ * @param result 待校验的最终入参对象
46
+ * @param options 物化配置选项(可选 maxMaterializedSizeBytes)
47
+ */
48
+ export function assertMaterializedInputWithinLimits(result, options) {
49
+ const check = validateJsonValue(result);
50
+ if (!check.valid) {
51
+ throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
52
+ reason: check.code === "MAX_JSON_DEPTH"
53
+ ? "MAX_MATERIALIZED_JSON_DEPTH"
54
+ : "INVALID_JSON_VALUE",
55
+ });
56
+ }
57
+ const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
58
+ const serialized = JSON.stringify(result);
59
+ const byteLength = Buffer.byteLength(serialized, "utf8");
60
+ if (byteLength > maxMaterializedSizeBytes) {
61
+ throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
62
+ }
63
+ }
14
64
  function createNode(pathStr, state = NodeState.UNSET) {
15
65
  return {
16
66
  state,
@@ -159,19 +209,6 @@ export function materializeFlatInput(assignments, options) {
159
209
  }
160
210
  }
161
211
  const result = materializeNode(root);
162
- const check = validateJsonValue(result);
163
- if (!check.valid) {
164
- throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
165
- reason: check.code === "MAX_JSON_DEPTH"
166
- ? "MAX_MATERIALIZED_JSON_DEPTH"
167
- : "INVALID_JSON_VALUE",
168
- });
169
- }
170
- const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
171
- const serialized = JSON.stringify(result);
172
- const byteLength = Buffer.byteLength(serialized, "utf8");
173
- if (byteLength > maxMaterializedSizeBytes) {
174
- throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
175
- }
212
+ assertMaterializedInputWithinLimits(result, options);
176
213
  return result;
177
214
  }
@@ -10,4 +10,5 @@ export * from "./file-input.js";
10
10
  export * from "./stdin-input.js";
11
11
  export * from "./input-resolver.js";
12
12
  export * from "./advice.js";
13
+ export * from "./output-contract.js";
13
14
  export * from "./describe.js";
@@ -10,4 +10,5 @@ export * from "./file-input.js";
10
10
  export * from "./stdin-input.js";
11
11
  export * from "./input-resolver.js";
12
12
  export * from "./advice.js";
13
+ export * from "./output-contract.js";
13
14
  export * from "./describe.js";
@@ -16,6 +16,8 @@ export interface InputResolutionPolicy {
16
16
  sanitizeInputErrors?: boolean;
17
17
  /** 是否仅允许字节流输入(默认 false) */
18
18
  byteStreamOnly?: boolean;
19
+ /** 原始 stdin 文本模式:保留开头 BOM,UTF-8 编码失败视为读取失败(默认 false,仅作用于 stdinField) */
20
+ stdinRawText?: boolean;
19
21
  }
20
22
  /**
21
23
  * 从标准输入流中完整读取全部数据并转换为 UTF-8 字符串(保持历史接口兼容)。
@@ -30,6 +32,8 @@ export interface ResolveActionInputOptions {
30
32
  input?: string;
31
33
  inputFile?: string;
32
34
  flatArgs?: string[];
35
+ /** 将 stdin 完整原始文本绑定为指定顶层入参字段的字符串值(与 input / inputFile 互斥,可与 flatArgs 组合) */
36
+ stdinField?: string;
33
37
  stdin?: NodeJS.ReadableStream;
34
38
  flatOptions?: FlatParserOptions & FlatMaterializerOptions;
35
39
  policy?: InputResolutionPolicy;
@@ -51,11 +55,15 @@ export declare function parseJson(text: string, source: string, policy?: InputRe
51
55
  * - effectiveFlat: flatArgs 存在且 length > 0
52
56
  * - hasInlineJson: options.input !== undefined(即使为 "")
53
57
  * - hasFileJson: options.inputFile !== undefined(包含 "-")
54
- * - 最多一个为 true;若超过一个,抛出 INPUT_CONFLICT(reason: "MULTIPLE_INPUT_MODES")。
55
- * - 若三个均为 false,返回默认 {}。
58
+ * - hasStdinField: options.stdinField !== undefined
59
+ * - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
60
+ * - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
61
+ * 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
62
+ * - 若四者均为 false,返回默认 {}。
56
63
  * - flatArgs = [] 视为未指定。
57
64
  * - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
58
65
  * - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
66
+ * - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
59
67
  *
60
68
  * @param options 输入解析选项
61
69
  * @returns 解析后的 JSON 对象
@@ -1,5 +1,7 @@
1
- import { inputConflict, inputLimitExceeded, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
1
+ import { inputConflict, inputLimitExceeded, inputPathConflict, invalidFlatArgument, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
2
+ import { isFlatPathPropertyName, isForbiddenActionInputPropertyName, } from "./flat-predicates.js";
2
3
  import { decodeFlatInput } from "./flat-decode.js";
4
+ import { assertTopLevelLeafAvailable, assertMaterializedInputWithinLimits, } from "./flat-materializer.js";
3
5
  import { validateJsonValue, DEFAULT_MAX_JSON_DEPTH, } from "../value-validator.js";
4
6
  import { scanJsonDepth } from "./json-depth-scanner.js";
5
7
  import { decodeUtf8Strict, stripBom } from "./utf8.js";
@@ -103,6 +105,62 @@ function sanitizeError(err, source) {
103
105
  }
104
106
  return err;
105
107
  }
108
+ /**
109
+ * 检测单个扁平 token 是否占用 stdin 绑定目标字段。
110
+ *
111
+ * 判定范围(在读 stdin 与执行业务之前):
112
+ * - 目标字段本身:重复赋值(DUPLICATE_ASSIGNMENT);
113
+ * - 目标字段的任意嵌套路径:叶/容器冲突(LEAF_CONTAINER_CONFLICT)。
114
+ *
115
+ * @param token 扁平赋值 token
116
+ * @param field stdin 绑定的目标字段名
117
+ */
118
+ function detectFlatTokenFieldConflict(token, field) {
119
+ const colonEqIndex = token.indexOf(":=");
120
+ const eqIndex = token.indexOf("=");
121
+ let opIndex = -1;
122
+ if (colonEqIndex !== -1 && (eqIndex === -1 || colonEqIndex < eqIndex)) {
123
+ opIndex = colonEqIndex;
124
+ }
125
+ else if (eqIndex !== -1) {
126
+ opIndex = eqIndex;
127
+ }
128
+ const rawPath = opIndex === -1 ? token : token.slice(0, opIndex);
129
+ if (rawPath !== field && !rawPath.startsWith(`${field}.`))
130
+ return null;
131
+ return inputPathConflict(`Input path conflict at "${rawPath}": field "${field}" is already bound to stdin text via --stdin-field`, { path: rawPath, reason: rawPath === field ? "DUPLICATE_ASSIGNMENT" : "LEAF_CONTAINER_CONFLICT" });
132
+ }
133
+ /**
134
+ * 校验 stdin 字段绑定名是否为合法安全顶层属性名。
135
+ *
136
+ * 拒绝空、纯空白、危险属性(`__proto__`、`constructor`、`prototype`)与
137
+ * 不符合扁平命名段规范(含点分嵌套路径)的表达,错误为 INVALID_FLAT_ARGUMENT。
138
+ *
139
+ * @param field 用户提供的字段名
140
+ */
141
+ function assertStdinFieldName(field) {
142
+ const problem = (() => {
143
+ if (field.length === 0)
144
+ return "empty";
145
+ if (field.trim() !== field || field.trim().length === 0)
146
+ return "whitespace";
147
+ if (isForbiddenActionInputPropertyName(field))
148
+ return "forbidden";
149
+ if (field.includes("."))
150
+ return "dotted";
151
+ if (!isFlatPathPropertyName(field))
152
+ return "invalid";
153
+ return null;
154
+ })();
155
+ if (problem === null)
156
+ return;
157
+ const reason = problem === "forbidden"
158
+ ? "FORBIDDEN_PROPERTY"
159
+ : problem === "dotted"
160
+ ? "INVALID_DOT_NOTATION"
161
+ : "INVALID_SEGMENT";
162
+ throw invalidFlatArgument(`Invalid --stdin-field name (length: ${field.length}): must be a non-empty top-level property name without dot notation`, { length: field.length, reason });
163
+ }
106
164
  /**
107
165
  * 解析完整 JSON 文档字符串。
108
166
  * 若解析失败抛出 INVALID_JSON 异常。
@@ -152,11 +210,15 @@ export function parseJson(text, source, policy) {
152
210
  * - effectiveFlat: flatArgs 存在且 length > 0
153
211
  * - hasInlineJson: options.input !== undefined(即使为 "")
154
212
  * - hasFileJson: options.inputFile !== undefined(包含 "-")
155
- * - 最多一个为 true;若超过一个,抛出 INPUT_CONFLICT(reason: "MULTIPLE_INPUT_MODES")。
156
- * - 若三个均为 false,返回默认 {}。
213
+ * - hasStdinField: options.stdinField !== undefined
214
+ * - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
215
+ * - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
216
+ * 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
217
+ * - 若四者均为 false,返回默认 {}。
157
218
  * - flatArgs = [] 视为未指定。
158
219
  * - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
159
220
  * - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
221
+ * - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
160
222
  *
161
223
  * @param options 输入解析选项
162
224
  * @returns 解析后的 JSON 对象
@@ -165,6 +227,22 @@ export async function resolveActionInput(options) {
165
227
  const effectiveFlat = (options.flatArgs?.length ?? 0) > 0;
166
228
  const hasInlineJson = options.input !== undefined;
167
229
  const hasFileJson = options.inputFile !== undefined;
230
+ const hasStdinField = options.stdinField !== undefined;
231
+ // stdin 字段绑定与完整 JSON 来源(内联 / 文件 / stdin JSON)严格互斥
232
+ if (hasStdinField && (hasInlineJson || hasFileJson)) {
233
+ const err = inputConflict("Input options conflict: --stdin-field binds raw stdin text to an action input field and cannot be combined with inline JSON (-i/--input) or file input (-f/--input-file).", {
234
+ reason: "MULTIPLE_INPUT_MODES",
235
+ hasFlatArgs: effectiveFlat,
236
+ hasInput: hasInlineJson,
237
+ hasInputFile: hasFileJson,
238
+ hasStdinField,
239
+ });
240
+ if (options.policy?.sanitizeInputErrors) {
241
+ throw sanitizeError(err, "stdin");
242
+ }
243
+ throw err;
244
+ }
245
+ // 原有三种完整输入模式之间的严格互斥保持不变
168
246
  let modeCount = 0;
169
247
  if (effectiveFlat)
170
248
  modeCount++;
@@ -184,11 +262,52 @@ export async function resolveActionInput(options) {
184
262
  }
185
263
  throw err;
186
264
  }
187
- if (modeCount === 0) {
265
+ if (modeCount === 0 && !hasStdinField) {
188
266
  return {};
189
267
  }
190
268
  const policy = options.policy;
191
269
  const maxBytes = policy?.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES;
270
+ // 0. stdin 原始文本字段绑定模式(可与 flatArgs 组合)
271
+ if (hasStdinField) {
272
+ const field = options.stdinField;
273
+ try {
274
+ assertStdinFieldName(field);
275
+ // 冲突检测与扁平完整解码均先于 stdin 读取:
276
+ // 目标字段占用冲突与任何已知的 flat 语法/路径/字面量错误在读 stdin 前拒绝,
277
+ // 不因等待上游 EOF 而挂起;解码结果复用,读取后不再重复解码。
278
+ let result = {};
279
+ if (effectiveFlat) {
280
+ for (const token of options.flatArgs) {
281
+ const conflict = detectFlatTokenFieldConflict(token, field);
282
+ if (conflict) {
283
+ throw conflict;
284
+ }
285
+ }
286
+ result = decodeFlatInput(options.flatArgs, options.flatOptions);
287
+ assertTopLevelLeafAvailable(result, field);
288
+ }
289
+ const text = await readStdinBounded(options.stdin || process.stdin, {
290
+ maxInputBytes: maxBytes,
291
+ signal: options.signal,
292
+ byteStreamOnly: policy?.byteStreamOnly,
293
+ strictUtf8: policy?.strictUtf8 ?? true,
294
+ rawText: policy?.stdinRawText ?? true,
295
+ });
296
+ // 绑定后对最终入参执行既有实体化结构/总大小校验:
297
+ // stdin 源字节上限(maxInputBytes)与最终对象序列化上限
298
+ // (flatOptions.maxMaterializedSizeBytes,默认 10MiB)各自独立生效,
299
+ // 正文中的引号/反斜杠转义放大亦被最终上限拦截。
300
+ result[field] = text;
301
+ assertMaterializedInputWithinLimits(result, options.flatOptions);
302
+ return result;
303
+ }
304
+ catch (err) {
305
+ if (policy?.sanitizeInputErrors) {
306
+ throw sanitizeError(err, "stdin");
307
+ }
308
+ throw err;
309
+ }
310
+ }
192
311
  // 1. 扁平入参模式
193
312
  if (effectiveFlat) {
194
313
  try {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Action 输出正文字段与 Schema 契约静态分析工具。
3
+ *
4
+ * 核心设计原则:
5
+ * - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
6
+ * - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
7
+ * - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
8
+ */
9
+ /**
10
+ * 契约诊断项。
11
+ */
12
+ export interface OutputContractDiagnostic {
13
+ type: "error" | "warning";
14
+ code: string;
15
+ message: string;
16
+ }
17
+ /**
18
+ * 注解解析与检验结果。
19
+ */
20
+ export interface AnnotationInspectionResult {
21
+ declared: boolean;
22
+ valid: boolean;
23
+ textField?: string;
24
+ error?: string;
25
+ }
26
+ /**
27
+ * 输出契约分析输入项。
28
+ */
29
+ export interface ActionOutputContractInput {
30
+ annotations?: unknown;
31
+ outputSchema?: unknown;
32
+ }
33
+ /**
34
+ * 输出契约全量分析结果。
35
+ */
36
+ export interface ActionOutputContractAnalysis {
37
+ declared: boolean;
38
+ valid: boolean;
39
+ textField?: string;
40
+ errors: string[];
41
+ warnings: string[];
42
+ diagnostics: OutputContractDiagnostic[];
43
+ }
44
+ /**
45
+ * 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
46
+ *
47
+ * 契约规范:
48
+ * - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
49
+ * - 正文字段键名为 textField,值必须为非空字符串。
50
+ * - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
51
+ * - 注解存在但类型非法时返回 valid: false 与对应错误描述。
52
+ */
53
+ export declare function inspectActionTextFieldAnnotation(annotations?: unknown): AnnotationInspectionResult;
54
+ /**
55
+ * 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
56
+ *
57
+ * 错误判断(确定矛盾):
58
+ * - 注解结构非法或 textField 不是非空字符串;
59
+ * - Schema 明确拒绝所有结果(boolean false);
60
+ * - Schema 明确只允许非对象根类型;
61
+ * - Schema 明确禁止目标属性(property schema 为 false);
62
+ * - Schema 明确要求目标属性为非字符串类型;
63
+ * - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
64
+ *
65
+ * 风险提示(无法完全静态证明安全):
66
+ * - 目标字段未声明为 required;
67
+ * - 目标字段允许 null 等非字符串类型;
68
+ * - outputSchema 缺失或为布尔 true;
69
+ * - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
70
+ * - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
71
+ */
72
+ export declare function analyzeActionOutputContract(input: ActionOutputContractInput): ActionOutputContractAnalysis;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Action 输出正文字段与 Schema 契约静态分析工具。
3
+ *
4
+ * 核心设计原则:
5
+ * - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
6
+ * - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
7
+ * - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
8
+ */
9
+ /**
10
+ * 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
11
+ *
12
+ * 契约规范:
13
+ * - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
14
+ * - 正文字段键名为 textField,值必须为非空字符串。
15
+ * - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
16
+ * - 注解存在但类型非法时返回 valid: false 与对应错误描述。
17
+ */
18
+ export function inspectActionTextFieldAnnotation(annotations) {
19
+ if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
20
+ return { declared: false, valid: true };
21
+ }
22
+ const record = annotations;
23
+ if (!Object.hasOwn(record, "actiondock.cli")) {
24
+ return { declared: false, valid: true };
25
+ }
26
+ const cli = record["actiondock.cli"];
27
+ if (cli === undefined) {
28
+ return { declared: false, valid: true };
29
+ }
30
+ if (typeof cli !== "object" || cli === null || Array.isArray(cli)) {
31
+ return {
32
+ declared: true,
33
+ valid: false,
34
+ error: `Invalid 'actiondock.cli' annotation: expected an object, but received ${cli === null ? "null" : Array.isArray(cli) ? "an array" : typeof cli}.`,
35
+ };
36
+ }
37
+ const cliRecord = cli;
38
+ if (!Object.hasOwn(cliRecord, "textField") || cliRecord.textField === undefined) {
39
+ return { declared: false, valid: true };
40
+ }
41
+ const textField = cliRecord.textField;
42
+ if (typeof textField !== "string") {
43
+ return {
44
+ declared: true,
45
+ valid: false,
46
+ error: `Invalid 'actiondock.cli.textField' annotation: expected a string, but received ${textField === null ? "null" : Array.isArray(textField) ? "an array" : typeof textField}.`,
47
+ };
48
+ }
49
+ if (textField.trim() === "") {
50
+ return {
51
+ declared: true,
52
+ valid: false,
53
+ error: "Invalid 'actiondock.cli.textField' annotation: cannot be an empty string.",
54
+ };
55
+ }
56
+ return {
57
+ declared: true,
58
+ valid: true,
59
+ textField,
60
+ };
61
+ }
62
+ /**
63
+ * 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
64
+ *
65
+ * 错误判断(确定矛盾):
66
+ * - 注解结构非法或 textField 不是非空字符串;
67
+ * - Schema 明确拒绝所有结果(boolean false);
68
+ * - Schema 明确只允许非对象根类型;
69
+ * - Schema 明确禁止目标属性(property schema 为 false);
70
+ * - Schema 明确要求目标属性为非字符串类型;
71
+ * - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
72
+ *
73
+ * 风险提示(无法完全静态证明安全):
74
+ * - 目标字段未声明为 required;
75
+ * - 目标字段允许 null 等非字符串类型;
76
+ * - outputSchema 缺失或为布尔 true;
77
+ * - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
78
+ * - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
79
+ */
80
+ export function analyzeActionOutputContract(input) {
81
+ const inspection = inspectActionTextFieldAnnotation(input.annotations);
82
+ if (!inspection.valid) {
83
+ const errorMsg = inspection.error;
84
+ return {
85
+ declared: true,
86
+ valid: false,
87
+ errors: [errorMsg],
88
+ warnings: [],
89
+ diagnostics: [
90
+ {
91
+ type: "error",
92
+ code: "INVALID_ANNOTATION",
93
+ message: errorMsg,
94
+ },
95
+ ],
96
+ };
97
+ }
98
+ if (!inspection.declared || !inspection.textField) {
99
+ return {
100
+ declared: false,
101
+ valid: true,
102
+ errors: [],
103
+ warnings: [],
104
+ diagnostics: [],
105
+ };
106
+ }
107
+ const textField = inspection.textField;
108
+ const errors = [];
109
+ const warnings = [];
110
+ const diagnostics = [];
111
+ function addError(code, message) {
112
+ errors.push(message);
113
+ diagnostics.push({ type: "error", code, message });
114
+ }
115
+ function addWarning(code, message) {
116
+ warnings.push(message);
117
+ diagnostics.push({ type: "warning", code, message });
118
+ }
119
+ const schema = input.outputSchema;
120
+ if (schema === undefined) {
121
+ addWarning("OUTPUT_SCHEMA_ABSENT", `Output schema is absent; cannot statically verify text field '${textField}'. Runtime validation will apply.`);
122
+ return {
123
+ declared: true,
124
+ valid: true,
125
+ textField,
126
+ errors,
127
+ warnings,
128
+ diagnostics,
129
+ };
130
+ }
131
+ if (schema === false) {
132
+ addError("OUTPUT_SCHEMA_REJECTS_ALL", `Output schema explicitly rejects all outputs (schema is false); cannot produce object containing text field '${textField}'.`);
133
+ return {
134
+ declared: true,
135
+ valid: false,
136
+ textField,
137
+ errors,
138
+ warnings,
139
+ diagnostics,
140
+ };
141
+ }
142
+ if (schema === true) {
143
+ addWarning("OUTPUT_SCHEMA_ARBITRARY", `Output schema allows arbitrary values (schema is true); text field '${textField}' is not guaranteed to exist or be a string.`);
144
+ return {
145
+ declared: true,
146
+ valid: true,
147
+ textField,
148
+ errors,
149
+ warnings,
150
+ diagnostics,
151
+ };
152
+ }
153
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
154
+ addError("INVALID_OUTPUT_SCHEMA", "Invalid outputSchema: expected a schema object or boolean.");
155
+ return {
156
+ declared: true,
157
+ valid: false,
158
+ textField,
159
+ errors,
160
+ warnings,
161
+ diagnostics,
162
+ };
163
+ }
164
+ const s = schema;
165
+ // 1. 检查根节点类型约束
166
+ if (s.type === undefined) {
167
+ addWarning("ROOT_TYPE_UNSPECIFIED", `Output schema does not explicitly specify root type as 'object'; non-object outputs cannot satisfy text field '${textField}'.`);
168
+ }
169
+ else {
170
+ const rootTypes = Array.isArray(s.type) ? s.type : [s.type];
171
+ if (!rootTypes.includes("object")) {
172
+ addError("ROOT_TYPE_NOT_OBJECT", `Output schema root type is '${String(s.type)}', which does not allow an object required for text field '${textField}'.`);
173
+ return {
174
+ declared: true,
175
+ valid: false,
176
+ textField,
177
+ errors,
178
+ warnings,
179
+ diagnostics,
180
+ };
181
+ }
182
+ const nonObjectTypes = rootTypes.filter((t) => t !== "object");
183
+ if (nonObjectTypes.length > 0) {
184
+ addWarning("ROOT_TYPE_ALLOWS_NON_OBJECT", `Output schema root type allows non-object types (${nonObjectTypes.join(", ")}); text output requires an object at runtime.`);
185
+ }
186
+ }
187
+ // 2. 检查复杂组合关键字(oneOf, anyOf, allOf, $ref)
188
+ if (s.oneOf || s.anyOf || s.allOf || s.$ref) {
189
+ addWarning("COMPLEX_SCHEMA_COMPOSITION", `Output schema contains complex composition keywords (oneOf/anyOf/allOf/$ref); text field '${textField}' cannot be fully verified statically.`);
190
+ }
191
+ const hasPatternProps = Boolean(s.patternProperties &&
192
+ typeof s.patternProperties === "object" &&
193
+ !Array.isArray(s.patternProperties) &&
194
+ Object.keys(s.patternProperties).length > 0);
195
+ // 3. 检查属性定义与目标字段
196
+ if (s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) {
197
+ const props = s.properties;
198
+ if (Object.hasOwn(props, textField)) {
199
+ const fieldSchema = props[textField];
200
+ if (fieldSchema === false) {
201
+ addError("TEXT_FIELD_FORBIDDEN", `Output schema property '${textField}' is explicitly forbidden (property schema is false).`);
202
+ }
203
+ else if (fieldSchema === true) {
204
+ addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
205
+ }
206
+ else if (typeof fieldSchema === "object" &&
207
+ fieldSchema !== null &&
208
+ !Array.isArray(fieldSchema)) {
209
+ const propObj = fieldSchema;
210
+ if (propObj.oneOf || propObj.anyOf || propObj.allOf || propObj.$ref) {
211
+ addWarning("TEXT_FIELD_COMPLEX_COMPOSITION", `Property '${textField}' contains complex composition keywords (oneOf/anyOf/allOf/$ref); text output cannot be fully verified statically.`);
212
+ }
213
+ if (propObj.type === undefined) {
214
+ addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
215
+ }
216
+ else {
217
+ const fieldTypes = Array.isArray(propObj.type) ? propObj.type : [propObj.type];
218
+ if (!fieldTypes.includes("string")) {
219
+ addError("TEXT_FIELD_NOT_STRING", `Output schema property '${textField}' has type '${String(propObj.type)}', which does not allow string.`);
220
+ }
221
+ else {
222
+ const nonStringTypes = fieldTypes.filter((t) => t !== "string");
223
+ if (nonStringTypes.length > 0) {
224
+ addWarning("TEXT_FIELD_ALLOWS_NON_STRING", `Property '${textField}' allows non-string types (${nonStringTypes.join(", ")}); text output requires a string at runtime.`);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ else {
231
+ // 字段未在 properties 中明确定义
232
+ if (s.additionalProperties === false) {
233
+ if (hasPatternProps) {
234
+ addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
235
+ }
236
+ else {
237
+ addError("TEXT_FIELD_DISALLOWED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties and additionalProperties is false.`);
238
+ }
239
+ }
240
+ else {
241
+ addWarning("TEXT_FIELD_UNDECLARED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties (allowed via additionalProperties); existence and type cannot be statically verified.`);
242
+ }
243
+ }
244
+ }
245
+ else {
246
+ // 未定义 properties
247
+ if (s.additionalProperties === false) {
248
+ if (hasPatternProps) {
249
+ addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
250
+ }
251
+ else {
252
+ addError("TEXT_FIELD_DISALLOWED_NO_PROPERTIES", `Output schema has no properties declared and additionalProperties is false; text field '${textField}' cannot exist.`);
253
+ }
254
+ }
255
+ else {
256
+ addWarning("TEXT_FIELD_NO_PROPERTIES", `Output schema does not define properties; text field '${textField}' existence cannot be statically verified.`);
257
+ }
258
+ }
259
+ // 4. 检查 required 约束(若无确定阻止字段存在的错误)
260
+ if (errors.length === 0) {
261
+ const isRequired = Array.isArray(s.required) && s.required.includes(textField);
262
+ if (!isRequired) {
263
+ addWarning("TEXT_FIELD_NOT_REQUIRED", `Text field '${textField}' is not marked as required in outputSchema; if absent at runtime, text extraction will fail with OUTPUT_FORMAT_ERROR (business execution remains successful in run records).`);
264
+ }
265
+ }
266
+ return {
267
+ declared: true,
268
+ valid: errors.length === 0,
269
+ textField,
270
+ errors,
271
+ warnings,
272
+ diagnostics,
273
+ };
274
+ }
@@ -10,6 +10,15 @@ export interface ReadStdinBoundedOptions {
10
10
  byteStreamOnly?: boolean;
11
11
  /** 是否执行严格 UTF-8 校验与解码(默认 true) */
12
12
  strictUtf8?: boolean;
13
+ /**
14
+ * 原始文本模式(默认 false)。
15
+ *
16
+ * 语义差异(仅在 strictUtf8 下生效):
17
+ * - 保留开头 BOM(U+FEFF)作为正文一部分,不执行剥离;
18
+ * - 编码失败映射为 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")读取失败,
19
+ * 而不是完整 JSON 场景的 INVALID_JSON 解析失败。
20
+ */
21
+ rawText?: boolean;
13
22
  }
14
23
  /**
15
24
  * 以有界大小限制从标准输入流中安全读取全部内容。
@@ -17,7 +26,8 @@ export interface ReadStdinBoundedOptions {
17
26
  * 核心契约:
18
27
  * - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
19
28
  * - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
20
- * - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常。
29
+ * - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
30
+ * rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
21
31
  *
22
32
  * @param stream 标准输入可读流,默认为 process.stdin
23
33
  * @param options 读取配置选项
@@ -7,7 +7,8 @@ import { DEFAULT_MAX_INPUT_BYTES } from "./file-input.js";
7
7
  * 核心契约:
8
8
  * - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
9
9
  * - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
10
- * - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常。
10
+ * - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
11
+ * rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
11
12
  *
12
13
  * @param stream 标准输入可读流,默认为 process.stdin
13
14
  * @param options 读取配置选项
@@ -18,6 +19,7 @@ export async function readStdinBounded(stream = process.stdin, options) {
18
19
  const signal = options?.signal;
19
20
  const byteStreamOnly = options?.byteStreamOnly ?? false;
20
21
  const strictUtf8 = options?.strictUtf8 ?? true;
22
+ const rawText = options?.rawText ?? false;
21
23
  // 启动前检查 abort
22
24
  if (signal?.aborted) {
23
25
  throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
@@ -101,8 +103,18 @@ export async function readStdinBounded(stream = process.stdin, options) {
101
103
  try {
102
104
  const fullBuffer = Buffer.concat(chunks);
103
105
  if (strictUtf8) {
104
- const text = decodeUtf8Strict(fullBuffer, "full-json-stdin");
105
- resolve(text);
106
+ if (rawText) {
107
+ // 原始文本模式:保留开头 BOM,编码失败视为读取失败而非 JSON 解析失败
108
+ try {
109
+ resolve(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fullBuffer));
110
+ }
111
+ catch (err) {
112
+ reject(inputFileReadFailed("stdin", new Error(`Invalid UTF-8 encoding: ${err instanceof Error ? err.message : String(err)}`), { reason: "INVALID_UTF8" }));
113
+ }
114
+ }
115
+ else {
116
+ resolve(decodeUtf8Strict(fullBuffer, "full-json-stdin"));
117
+ }
106
118
  }
107
119
  else {
108
120
  resolve(fullBuffer.toString("utf8"));
@@ -9,7 +9,7 @@ export * from "./types-generator.js";
9
9
  export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, type TraverseDirectoryEntry, type TraverseDirectoryOptions, } from "../utils/index.js";
10
10
  export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
11
11
  export { runDoctorChecks, } from "../doctor/doctor.js";
12
- export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
12
+ export { resolveActionInput, buildActionDescribePayload, formatActionDetail, type FormatActionDetailOptions, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, type AnnotationInspectionResult, type ActionOutputContractInput, type ActionOutputContractAnalysis, type OutputContractDiagnostic, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
13
13
  export { validateActionInputValue } from "../value-validator.js";
14
14
  export { parseJson } from "../input/input-resolver.js";
15
15
  export { InputError, FlatInputError, } from "../input/flat-errors.js";
@@ -9,8 +9,8 @@ export * from "./types-generator.js";
9
9
  export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, } from "../utils/index.js";
10
10
  export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
11
11
  export { runDoctorChecks, } from "../doctor/doctor.js";
12
- // 动作入参解析、校验与格式化(含 BOM 剥离与标准输入有界读取的单一事实源转引)
13
- export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
12
+ // 动作入参解析、输出契约分析、校验与格式化(含 BOM 剥离与标准输入有界读取的单一事实源转引)
13
+ export { resolveActionInput, buildActionDescribePayload, formatActionDetail, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
14
14
  export { validateActionInputValue } from "../value-validator.js";
15
15
  export { parseJson } from "../input/input-resolver.js";
16
16
  export { InputError, FlatInputError, } from "../input/flat-errors.js";
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
86
86
  node: ">=24.12.0",
87
87
  },
88
88
  dependencies: {
89
- "@actiondock/sdk": "^2.11.0",
89
+ "@actiondock/sdk": "^2.13.0",
90
90
  },
91
91
  devDependencies: {
92
- "@actiondock/testing": "^2.11.0",
92
+ "@actiondock/testing": "^2.13.0",
93
93
  "@types/node": "^22.13.0",
94
94
  "tsx": "^4.19.0",
95
95
  "typescript": "^5.7.0",
@@ -22,6 +22,7 @@ export function normalizeActionCollection(rawActions) {
22
22
  description: v.description,
23
23
  inputSchema: v.inputSchema,
24
24
  outputSchema: v.outputSchema,
25
+ annotations: v.annotations,
25
26
  };
26
27
  }
27
28
  return { actionsMap, actionSpecs };
@@ -37,6 +38,7 @@ export function normalizeActionCollection(rawActions) {
37
38
  description: item.description ?? act.description,
38
39
  inputSchema: item.inputSchema ?? act.inputSchema,
39
40
  outputSchema: item.outputSchema ?? act.outputSchema,
41
+ annotations: item.annotations ?? act.annotations,
40
42
  };
41
43
  }
42
44
  }
@@ -50,6 +52,7 @@ export function normalizeActionCollection(rawActions) {
50
52
  description: v.description,
51
53
  inputSchema: v.inputSchema,
52
54
  outputSchema: v.outputSchema,
55
+ annotations: v.annotations,
53
56
  };
54
57
  }
55
58
  }
@@ -72,7 +72,7 @@ export async function handleDescribe(ctx, subArgs) {
72
72
  ctx.writeOut(JSON.stringify(payload, null, 2));
73
73
  }
74
74
  else {
75
- ctx.writeOut(formatActionDetail(payload));
75
+ ctx.writeOut(formatActionDetail(payload, { supportsTextField: false }));
76
76
  }
77
77
  return ExitCode.SUCCESS;
78
78
  }
@@ -84,6 +84,7 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
84
84
  }
85
85
  let inputStr;
86
86
  let inputFile;
87
+ let stdinField;
87
88
  let timeoutMs;
88
89
  const recognizedFlags = new Set(["--json", "--async"]);
89
90
  for (let i = 0; i < subArgs.length; i++) {
@@ -131,6 +132,12 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
131
132
  else if (arg.startsWith("-f=")) {
132
133
  inputFile = arg.slice(3);
133
134
  }
135
+ else if (arg === "--stdin-field" && i + 1 < subArgs.length) {
136
+ stdinField = subArgs[++i];
137
+ }
138
+ else if (arg.startsWith("--stdin-field=")) {
139
+ stdinField = arg.slice(14);
140
+ }
134
141
  else if (arg.startsWith("-")) {
135
142
  const hint = "Hint: Separate action inputs from CLI options using '--', e.g.: ad run <id> [options] -- <param>=<val> or <param>:=<json>.";
136
143
  return ctx.emitError(isJson, "INVALID_ARGUMENT", `error: unknown option '${arg}'`, ExitCode.INVALID_ARGUMENT, {
@@ -144,8 +151,10 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
144
151
  input = await resolveActionInput({
145
152
  input: inputStr,
146
153
  inputFile,
154
+ stdinField,
147
155
  flatArgs: actionArgs.length > 0 ? actionArgs : undefined,
148
156
  stdin: process.stdin,
157
+ signal,
149
158
  });
150
159
  const check = validateActionInputValue(input);
151
160
  if (!check.valid) {
@@ -42,6 +42,8 @@ export function printHelp(writeOut, options) {
42
42
  writeOut(" <cmd> describe <id> [--json] Show action details and schemas");
43
43
  writeOut(" <cmd> run <id> [--input '<json>'] Execute action (raw text output by default)");
44
44
  writeOut(" <cmd> run <id> [--json] Output standard execution result in JSON format");
45
+ writeOut(" <cmd> run <id> [--stdin-field <field>] [-- <assignments...>]");
46
+ writeOut(" Bind raw stdin text to an input field");
45
47
  writeOut(" <cmd> config list/get/set/delete Manage package configuration");
46
48
  writeOut(" <cmd> state list/get/set/delete Manage shared state store");
47
49
  writeOut("\nGlobal options:");
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * ActionDock 核心版本号单一事实源。
3
3
  */
4
- export declare const ACTIONDOCK_VERSION = "2.11.0";
4
+ export declare const ACTIONDOCK_VERSION = "2.13.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * ActionDock 核心版本号单一事实源。
3
3
  */
4
- export const ACTIONDOCK_VERSION = "2.11.0";
4
+ export const ACTIONDOCK_VERSION = "2.13.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/core",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "test": "node ../../scripts/run-tests.ts"
65
65
  },
66
66
  "dependencies": {
67
- "@actiondock/sdk": "^2.11.0",
67
+ "@actiondock/sdk": "^2.13.0",
68
68
  "ajv": "^8.17.1",
69
69
  "ajv-formats": "^3.0.1",
70
70
  "undici": "^7.29.1"