@klarkxy/dsh-plugin-kit 0.1.0 → 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.
- package/README.md +24 -2
- package/docs/README.zh-CN.md +21 -2
- package/lib/cli-media.d.ts +70 -0
- package/lib/cli-media.js +249 -0
- package/lib/cli-media.js.map +1 -0
- package/lib/cli-process.d.ts +33 -0
- package/lib/cli-process.js +166 -0
- package/lib/cli-process.js.map +1 -0
- package/lib/client-utils.d.ts +7 -1
- package/lib/client-utils.js +25 -1
- package/lib/client-utils.js.map +1 -1
- package/lib/contracts.d.ts +67 -111
- package/lib/contracts.js +34 -2
- package/lib/contracts.js.map +1 -1
- package/lib/domain.d.ts +26 -0
- package/lib/domain.js +41 -0
- package/lib/domain.js.map +1 -0
- package/lib/host-rpc.d.ts +22 -3
- package/lib/host-rpc.js +87 -68
- package/lib/host-rpc.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/llm-call.d.ts +5 -1
- package/lib/llm-call.js +5 -1
- package/lib/llm-call.js.map +1 -1
- package/lib/model-menu.d.ts +3 -36
- package/lib/model-menu.js +11 -120
- package/lib/model-menu.js.map +1 -1
- package/lib/model-settings-client.d.ts +47 -0
- package/lib/model-settings-client.js +143 -0
- package/lib/model-settings-client.js.map +1 -0
- package/lib/model-settings.d.ts +55 -0
- package/lib/model-settings.js +60 -0
- package/lib/model-settings.js.map +1 -0
- package/lib/official-ui.js +1 -1
- package/lib/official-ui.js.map +1 -1
- package/package.json +57 -8
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @klarkxy/dsh-plugin-kit
|
|
2
2
|
|
|
3
|
+
<!-- release-state-note -->
|
|
4
|
+
The [repository registry](../../README.md#plugin-lifecycle) tracks Draft → Testing → Published, moving forward only. Fixes, testing and version updates keep published packages Published; each update still passes release checks. First publication explicitly selects Testing packages and records Published only after npm archive verification. See the [release guide](../../RELEASING.md).
|
|
5
|
+
|
|
3
6
|
The shared support package for the feature plugins in this repository: shared record types, the plugin-page model menu, host RPC registration, the official-UI style contract, and one native `llm` text call. It is a library, not a DSH bundle — adding it as a dependency gives a feature plugin the pieces it needs, not a new user-visible feature of its own.
|
|
4
7
|
|
|
5
8
|
[简体中文](docs/README.zh-CN.md)
|
|
@@ -15,10 +18,16 @@ The shared support package for the feature plugins in this repository: shared re
|
|
|
15
18
|
- `.` — the shared contract types, plus `registerHostRpc`, `callLlmText` and `resolveFeatureModel`.
|
|
16
19
|
- `./contracts` — browser-safe types and constants: shared memory/knowledge records, `TaskContract` / `TaskCheckpoint`, `ModelRoute`, `RpcResult`, `CHAT_EVENTS_SLOT`, `projectIdFromCwd`.
|
|
17
20
|
- `./host-rpc` — host RPC registration helper and its context type.
|
|
21
|
+
- `./model-settings` — host-only model settings contributions. Register the owning plugin's read snapshot and CAS writer with `registerModelSettings` inside `ctx.effect`; the hub discovers active owners using `collectModelSettings`. Each registration has an incarnation ID, so a stale page cannot write into a replacement instance. Reads expose only model fields and revision, with an optional independent model catalog; storage and validation stay with the owner.
|
|
18
22
|
- `./client-utils` — browser-safe helpers for native plugin-page seats: `selectedSessionId`, `useNativeSeat`, `useFeatureRefresh`. `react` is an optional peer.
|
|
23
|
+
- `./model-settings-client` — browser-safe optional invalidation subscription using `createModelSettingsSubscriber`. Model Hub mounts the native stream descriptor once; pages own their rereads and draft conflicts. Owners call host-side `notifyModelSettingsChanged` after persistence and memory state commit.
|
|
19
24
|
- `./model-menu` — plugin-page model select: catalog parsing, empty-route handling, reasoning-effort options.
|
|
20
25
|
- `./llm-call` — `callLlmText` and `resolveFeatureModel` as separate exports.
|
|
21
26
|
- `./official-ui` — the shared browser-side style contract: `officialUiCss(roots)`, the `--dsw-*` token allowlist, and the focus/elevation helpers.
|
|
27
|
+
- `./cli-process` — run a resolved executable without a command shell, with bounded output, serialized line callbacks, deadlines and owned-process-tree cancellation. Streaming mode delivers stdout through `onLine`; it does not also retain stdout.
|
|
28
|
+
- `./cli-media` — validate and stage explicit workspace/current-session media, enforce file limits, and publish native image/file attachments. Remote job authority and vendor protocols remain in each plugin; these helpers do not sandbox a vendor CLI's own file access.
|
|
29
|
+
|
|
30
|
+
Plugins passing their Cordis context to `resolveFeatureModel` must declare `agents`, `sessionProjections` and `agentDefaultModel` in `inject`. These services supply the live session selection and the host default when the plugin has no saved model.
|
|
22
31
|
|
|
23
32
|
## Plugin UI convention
|
|
24
33
|
|
|
@@ -46,12 +55,25 @@ restyling controls of its own. Concretely:
|
|
|
46
55
|
`OFFICIAL_THEME_TOKEN_NAMES` is that allowlist. Adding a token to it is a
|
|
47
56
|
deliberate act: confirm the name against the host theme first.
|
|
48
57
|
|
|
49
|
-
##
|
|
58
|
+
## Session ancestry
|
|
59
|
+
|
|
60
|
+
`isSubagentSession` uses durable session headers, never the author of a user message.
|
|
61
|
+
Explicit subagent origin or positive delegation depth wins. A seeded human fork with
|
|
62
|
+
zero or omitted depth and ordinary/free classification keeps its parent as history
|
|
63
|
+
lineage; incomplete legacy parent metadata still identifies a child session.
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
## Shared string contracts
|
|
66
|
+
|
|
67
|
+
`./contracts` is the single source for cross-plugin literals:
|
|
68
|
+
|
|
69
|
+
- `MODEL_SETTINGS_CHANGES_REMOTE` — the model-settings change stream provided by `@klarkxy/dsh-model-hub`. Subscribers degrade silently when that package is not installed.
|
|
70
|
+
- `WEB_PROVIDER_AVAILABILITY_EVENT` and `DshWebManagementDescriptor` — native web providers announce a local availability change and optional display metadata.
|
|
71
|
+
- `CREATOR_PRESET_ID` — the host Creator agent-preset id.
|
|
52
72
|
|
|
53
73
|
## Development
|
|
54
74
|
|
|
75
|
+
Model settings contributions accept `ModelSettingsText` (`string | { zh: string; en: string }`) for provider titles, field labels/descriptions, follow labels and catalog errors. Built-in display copy should include both languages; user data and third-party literal strings stay unchanged. Model Hub resolves the text at render time when the host locale changes.
|
|
76
|
+
|
|
55
77
|
```sh
|
|
56
78
|
pnpm --filter @klarkxy/dsh-plugin-kit typecheck
|
|
57
79
|
pnpm --filter @klarkxy/dsh-plugin-kit test
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @klarkxy/dsh-plugin-kit
|
|
2
2
|
|
|
3
|
+
<!-- release-state-note -->
|
|
4
|
+
插件生命周期见[仓库状态清单](../../../README.zh-CN.md#插件生命周期):草稿 → 测试中 → 已发布,只向前推进。已发布插件的修复、测试和升版保持已发布;每次更新仍须通过发布检查。首次发布须明确选择测试中的包,并在 npm 归档验证成功后登记。详见[发布规则](../../../RELEASING.zh-CN.md)。
|
|
5
|
+
|
|
3
6
|
本仓库功能插件共用的支撑包:共享记录类型、插件页模型菜单、宿主 RPC 注册、官方 UI 样式契约,以及一次原生 `llm` 文本调用。它是一个库,不是 DSH bundle——把它作为依赖装上,得到的是功能插件需要的零件,而不是一个新的用户可见功能。
|
|
4
7
|
|
|
5
8
|
[English](../README.md)
|
|
@@ -15,10 +18,16 @@
|
|
|
15
18
|
- `.` — 共享契约类型,以及 `registerHostRpc`、`callLlmText`、`resolveFeatureModel`。
|
|
16
19
|
- `./contracts` — 浏览器安全的类型与常量:共享记忆/知识记录、`TaskContract` / `TaskCheckpoint`、`ModelRoute`、`RpcResult`、`CHAT_EVENTS_SLOT`、`projectIdFromCwd`。
|
|
17
20
|
- `./host-rpc` — 宿主 RPC 注册辅助及其上下文类型。
|
|
21
|
+
- `./model-settings` — 仅供宿主使用的模型设置贡献入口。在 `ctx.effect` 中用 `registerModelSettings` 登记所属插件的模型快照和 CAS 保存接口,枢纽通过 `collectModelSettings` 发现活动入口。每次注册有独立实例标识,旧页面不能写入重新加载的实例。只公开模型字段、版本以及可选的独立模型目录;存储与校验仍归所属插件维护。
|
|
18
22
|
- `./client-utils` — 浏览器安全的原生插件页座位辅助:`selectedSessionId`、`useNativeSeat`、`useFeatureRefresh`。`react` 是可选对等依赖。
|
|
23
|
+
- `./model-settings-client` — 浏览器安全的可选模型设置订阅。`createModelSettingsSubscriber` 复用原生远程流,模型枢纽统一注册描述符;消费页处理刷新和草稿冲突。所属插件在持久化和内存更新完成后用宿主入口的 `notifyModelSettingsChanged` 发出失效提示。
|
|
19
24
|
- `./model-menu` — 插件页模型菜单:目录解析、空路由处理、思考强度选项。
|
|
20
25
|
- `./llm-call` — 单独导出 `callLlmText` 与 `resolveFeatureModel`。
|
|
21
26
|
- `./official-ui` — 浏览器端共享样式契约:`officialUiCss(roots)`、`--dsw-*` token 白名单,以及焦点环与层级辅助。
|
|
27
|
+
- `./cli-process` — 不经命令 shell 运行已解析的可执行文件,提供输出上限、串行逐行回调、超时及本次进程树的取消。流式模式通过 `onLine` 交付 stdout,不再重复保留 stdout 字符串。
|
|
28
|
+
- `./cli-media` — 校验并暂存明确指定的工作区/当前会话媒体,限制文件大小,返回原生图片和文件附件。远端任务授权与厂商协议由各插件负责;这些辅助不为厂商 CLI 自身的文件访问提供沙箱。
|
|
29
|
+
|
|
30
|
+
将 Cordis 上下文传给 `resolveFeatureModel` 的插件须在 `inject` 中声明 `agents`、`sessionProjections` 和 `agentDefaultModel`。插件未保存模型时,这三个服务提供当前会话选择和宿主默认模型。
|
|
22
31
|
|
|
23
32
|
## 插件 UI 约定
|
|
24
33
|
|
|
@@ -30,12 +39,22 @@
|
|
|
30
39
|
|
|
31
40
|
`OFFICIAL_THEME_TOKEN_NAMES` 就是这份白名单。往里加 token 是一个需要确认的动作:先对照宿主主题核实名字。
|
|
32
41
|
|
|
33
|
-
##
|
|
42
|
+
## 会话关系
|
|
43
|
+
|
|
44
|
+
`isSubagentSession` 只读取持久会话头,不根据 user 消息判断真人。明确的子代理来源或正派发深度优先;派发深度为零或由原生宿主省略、明确带 seed 的普通/自由真人分叉保留 parent 作为历史继承关系,仅有 parent 的旧会话及未知元数据仍按子线程处理。
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
## 跨插件字符串契约
|
|
47
|
+
|
|
48
|
+
`./contracts` 是插件间字面量的单一来源:
|
|
49
|
+
|
|
50
|
+
- `MODEL_SETTINGS_CHANGES_REMOTE`:由 `@klarkxy/dsh-model-hub` 提供的模型设置变更流。未安装该包时,订阅方静默降级。
|
|
51
|
+
- `WEB_PROVIDER_AVAILABILITY_EVENT` 与 `DshWebManagementDescriptor`:原生网页供应商宣布本地可用性变化,以及可选的展示元数据。
|
|
52
|
+
- `CREATOR_PRESET_ID`:宿主 Creator 预设 id。
|
|
36
53
|
|
|
37
54
|
## 开发
|
|
38
55
|
|
|
56
|
+
模型设置贡献的提供方标题、字段名称与说明、跟随模式名称及目录错误支持 `ModelSettingsText`(`string | { zh: string; en: string }`)。内置展示文案提供中英文,用户数据和第三方字符串保留原文;模型设置页在渲染时跟随宿主语言解析。
|
|
57
|
+
|
|
39
58
|
```sh
|
|
40
59
|
pnpm --filter @klarkxy/dsh-plugin-kit typecheck
|
|
41
60
|
pnpm --filter @klarkxy/dsh-plugin-kit test
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AttachmentStore, FileAttachmentRef, ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
|
|
2
|
+
//#region src/cli-media.d.ts
|
|
3
|
+
type MediaKind = 'image' | 'audio' | 'video';
|
|
4
|
+
type MediaInput = {
|
|
5
|
+
path?: string;
|
|
6
|
+
attachmentId?: string;
|
|
7
|
+
kind: MediaKind;
|
|
8
|
+
};
|
|
9
|
+
type MediaExec = {
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
agent?: {
|
|
12
|
+
session: {
|
|
13
|
+
id: unknown;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
type MediaHost = {
|
|
18
|
+
sandboxPolicy: {
|
|
19
|
+
resolve(input: {
|
|
20
|
+
session?: unknown;
|
|
21
|
+
}): {
|
|
22
|
+
workspaceRoot: string;
|
|
23
|
+
mode: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
attachments: AttachmentStore;
|
|
27
|
+
get?(name: string): unknown;
|
|
28
|
+
};
|
|
29
|
+
type PublishedMedia = {
|
|
30
|
+
path: string;
|
|
31
|
+
name: string;
|
|
32
|
+
kind: MediaKind;
|
|
33
|
+
mediaType: string;
|
|
34
|
+
bytes: number;
|
|
35
|
+
attachment: {
|
|
36
|
+
type: 'image';
|
|
37
|
+
attachment: ImageAttachmentRef;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'file';
|
|
40
|
+
attachment: FileAttachmentRef;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
declare class CliMediaError extends Error {
|
|
44
|
+
readonly code: string;
|
|
45
|
+
constructor(code: string, message: string);
|
|
46
|
+
}
|
|
47
|
+
declare const MEDIA_LIMITS: Record<MediaKind, number>;
|
|
48
|
+
declare function insideDirectory(root: string, path: string): boolean;
|
|
49
|
+
/** Resolve each path component; links are not accepted as media or receipt boundaries. */
|
|
50
|
+
declare function confinedPath(root: string, path: string): Promise<string>;
|
|
51
|
+
declare function mediaWorkspace(host: MediaHost, exec: MediaExec, write: boolean): Promise<string>;
|
|
52
|
+
declare function inspectMedia(path: string, kind: MediaKind): Promise<{
|
|
53
|
+
mediaType: string;
|
|
54
|
+
bytes: number;
|
|
55
|
+
}>;
|
|
56
|
+
declare function stageMedia(host: MediaHost, exec: MediaExec, root: string, directory: string, inputs: MediaInput[]): Promise<{
|
|
57
|
+
path: string;
|
|
58
|
+
mediaType: string;
|
|
59
|
+
bytes: number;
|
|
60
|
+
}[]>;
|
|
61
|
+
declare function jobDirectory(root: string, slug: string, id?: string): Promise<{
|
|
62
|
+
id: string;
|
|
63
|
+
directory: string;
|
|
64
|
+
}>;
|
|
65
|
+
declare function writeReceipt(directory: string, receipt: unknown): Promise<void>;
|
|
66
|
+
declare function readReceipt<T>(root: string, slug: string, id: string): Promise<T>;
|
|
67
|
+
declare function publishMedia(host: MediaHost, source: string, sourceRoot: string, destination: string, workspace: string, kind: MediaKind, signal: AbortSignal): Promise<PublishedMedia>;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { CliMediaError, MEDIA_LIMITS, MediaExec, MediaHost, MediaInput, MediaKind, PublishedMedia, confinedPath, insideDirectory, inspectMedia, jobDirectory, mediaWorkspace, publishMedia, readReceipt, stageMedia, writeReceipt };
|
|
70
|
+
//# sourceMappingURL=cli-media.d.ts.map
|
package/lib/cli-media.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region src/cli-media.ts
|
|
7
|
+
var CliMediaError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "CliMediaError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const fail = (code, message) => new CliMediaError(code, message);
|
|
16
|
+
const MEDIA_LIMITS = {
|
|
17
|
+
image: 31457280,
|
|
18
|
+
audio: 104857600,
|
|
19
|
+
video: 268435456
|
|
20
|
+
};
|
|
21
|
+
function insideDirectory(root, path) {
|
|
22
|
+
const rel = relative(root, path);
|
|
23
|
+
return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
24
|
+
}
|
|
25
|
+
/** Resolve each path component; links are not accepted as media or receipt boundaries. */
|
|
26
|
+
async function confinedPath(root, path) {
|
|
27
|
+
const base = await realpath(root), absolute = resolve(base, path);
|
|
28
|
+
if (!insideDirectory(base, absolute) || (await lstat(root)).isSymbolicLink()) throw fail("outside-workspace", "The file must stay inside the permitted directory.");
|
|
29
|
+
let part = base;
|
|
30
|
+
for (const component of relative(base, absolute).split(sep).filter(Boolean)) {
|
|
31
|
+
part = join(part, component);
|
|
32
|
+
if ((await lstat(part)).isSymbolicLink()) throw fail("outside-workspace", "Symbolic links are not accepted for media files.");
|
|
33
|
+
}
|
|
34
|
+
const canonical = await realpath(absolute);
|
|
35
|
+
if (!insideDirectory(base, canonical)) throw fail("outside-workspace", "The file must stay inside the permitted directory.");
|
|
36
|
+
return canonical;
|
|
37
|
+
}
|
|
38
|
+
async function mediaWorkspace(host, exec, write) {
|
|
39
|
+
exec.signal.throwIfAborted();
|
|
40
|
+
const policy = host.sandboxPolicy.resolve({ session: exec.agent?.session });
|
|
41
|
+
if (write && policy.mode === "read-only") throw fail("read-only", "This operation saves media and needs workspace write access.");
|
|
42
|
+
const root = await realpath(policy.workspaceRoot);
|
|
43
|
+
if (!(await lstat(root)).isDirectory()) throw fail("invalid-workspace", "The workspace is unavailable.");
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
async function inspectMedia(path, kind) {
|
|
47
|
+
const info = await lstat(path);
|
|
48
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size < 4 || info.size > MEDIA_LIMITS[kind]) throw fail("invalid-media", "The media file is empty, too large, or not a regular file.");
|
|
49
|
+
const file = await open(path, "r"), head = Buffer.alloc(64);
|
|
50
|
+
try {
|
|
51
|
+
await file.read(head, 0, head.length, 0);
|
|
52
|
+
} finally {
|
|
53
|
+
await file.close();
|
|
54
|
+
}
|
|
55
|
+
let mediaType;
|
|
56
|
+
if (head.subarray(0, 8).equals(Buffer.from([
|
|
57
|
+
137,
|
|
58
|
+
80,
|
|
59
|
+
78,
|
|
60
|
+
71,
|
|
61
|
+
13,
|
|
62
|
+
10,
|
|
63
|
+
26,
|
|
64
|
+
10
|
|
65
|
+
]))) mediaType = "image/png";
|
|
66
|
+
else if (head[0] === 255 && head[1] === 216 && head[2] === 255) mediaType = "image/jpeg";
|
|
67
|
+
else if (/^GIF8[79]a$/.test(head.toString("ascii", 0, 6))) mediaType = "image/gif";
|
|
68
|
+
else if (head.toString("ascii", 0, 4) === "RIFF" && head.toString("ascii", 8, 12) === "WEBP") mediaType = "image/webp";
|
|
69
|
+
else if (head.toString("ascii", 0, 4) === "RIFF" && head.toString("ascii", 8, 12) === "WAVE") mediaType = "audio/wav";
|
|
70
|
+
else if (head.toString("ascii", 0, 3) === "ID3" || head[0] === 255 && (head[1] & 224) === 224) mediaType = "audio/mpeg";
|
|
71
|
+
else if (head.toString("ascii", 0, 4) === "fLaC") mediaType = "audio/flac";
|
|
72
|
+
else if (head.toString("ascii", 0, 4) === "OggS") mediaType = "audio/ogg";
|
|
73
|
+
else if (head.toString("ascii", 4, 8) === "ftyp") {
|
|
74
|
+
const brand = head.toString("ascii", 8, 12);
|
|
75
|
+
mediaType = /^(?:heic|heix|hevc|hevx|mif1|msf1)$/.test(brand) ? "image/heic" : kind === "audio" ? "audio/mp4" : brand === "qt " ? "video/quicktime" : "video/mp4";
|
|
76
|
+
} else if (head.subarray(0, 4).equals(Buffer.from([
|
|
77
|
+
26,
|
|
78
|
+
69,
|
|
79
|
+
223,
|
|
80
|
+
163
|
|
81
|
+
]))) mediaType = kind === "audio" ? "audio/webm" : "video/webm";
|
|
82
|
+
if (!mediaType?.startsWith(`${kind}/`)) throw fail("invalid-media", `The file is not a supported ${kind} file.`);
|
|
83
|
+
return {
|
|
84
|
+
mediaType,
|
|
85
|
+
bytes: info.size
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function sessionAttachments(host, exec) {
|
|
89
|
+
if (!exec.agent) throw fail("no-session", "Session attachments need an active session.");
|
|
90
|
+
const query = host.get?.("sessionQuery");
|
|
91
|
+
if (!query) throw fail("no-session-query", "The host cannot read current-session attachments.");
|
|
92
|
+
const snapshot = await query.readSession(exec.agent.session.id);
|
|
93
|
+
exec.signal.throwIfAborted();
|
|
94
|
+
const result = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const raw of snapshot.events) {
|
|
96
|
+
const event = raw;
|
|
97
|
+
if (event.type !== "user/message" && event.type !== "tool/result") continue;
|
|
98
|
+
const content = event.data?.content ?? event.data?.message?.content;
|
|
99
|
+
if (!Array.isArray(content)) continue;
|
|
100
|
+
for (const block of content) if ((block?.type === "image" || block?.type === "file") && typeof block.attachment?.attachmentId === "string") result.set(block.attachment.attachmentId, block);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
async function* bounded(chunks, max, signal) {
|
|
105
|
+
let bytes = 0;
|
|
106
|
+
for await (const chunk of chunks) {
|
|
107
|
+
signal.throwIfAborted();
|
|
108
|
+
bytes += chunk.byteLength;
|
|
109
|
+
if (bytes > max) throw fail("invalid-media", "The media file exceeds its size limit.");
|
|
110
|
+
yield chunk;
|
|
111
|
+
}
|
|
112
|
+
signal.throwIfAborted();
|
|
113
|
+
}
|
|
114
|
+
async function stageMedia(host, exec, root, directory, inputs) {
|
|
115
|
+
if (inputs.length > 40) throw fail("bad-request", "At most 40 input files are allowed.");
|
|
116
|
+
const refs = inputs.some((input) => input.attachmentId) ? await sessionAttachments(host, exec) : /* @__PURE__ */ new Map();
|
|
117
|
+
const staged = [];
|
|
118
|
+
for (const [index, input] of inputs.entries()) {
|
|
119
|
+
exec.signal.throwIfAborted();
|
|
120
|
+
if (!MEDIA_LIMITS[input.kind] || Boolean(input.path) === Boolean(input.attachmentId)) throw fail("bad-request", "Provide exactly one workspace path or current-session attachment ID per input.");
|
|
121
|
+
const ref = input.attachmentId ? refs.get(input.attachmentId) : void 0;
|
|
122
|
+
if (input.attachmentId && !ref) throw fail("unknown-attachment", "The attachment does not belong to the current session.");
|
|
123
|
+
const source = input.path ? await confinedPath(root, input.path) : void 0;
|
|
124
|
+
if (source) await inspectMedia(source, input.kind);
|
|
125
|
+
const rawName = source ? basename(source) : ref.attachment.name || "attachment";
|
|
126
|
+
const extension = /^\.[a-z0-9]{1,8}$/i.test(extname(rawName)) ? extname(rawName).toLowerCase() : ref?.type === "image" ? "." + ref.attachment.mediaType.split("/")[1] : ".bin";
|
|
127
|
+
const target = join(directory, `input-${index}${extension}`);
|
|
128
|
+
if (source) await pipeline(createReadStream(source), createWriteStream(target, { flags: "wx" }), { signal: exec.signal });
|
|
129
|
+
else if (ref.type === "image") {
|
|
130
|
+
if (input.kind !== "image") throw fail("invalid-media", "An image attachment cannot be used as audio or video.");
|
|
131
|
+
const stored = await host.attachments.readImage(ref.attachment, exec.signal);
|
|
132
|
+
if (stored.data.byteLength > MEDIA_LIMITS.image) throw fail("invalid-media", "The image is too large.");
|
|
133
|
+
await writeFile(target, stored.data, {
|
|
134
|
+
flag: "wx",
|
|
135
|
+
signal: exec.signal
|
|
136
|
+
});
|
|
137
|
+
} else await pipeline(bounded(host.attachments.readFileStream(ref.attachment, exec.signal), MEDIA_LIMITS[input.kind], exec.signal), createWriteStream(target, { flags: "wx" }), { signal: exec.signal });
|
|
138
|
+
const info = await inspectMedia(target, input.kind);
|
|
139
|
+
staged.push({
|
|
140
|
+
path: target,
|
|
141
|
+
...info
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return staged;
|
|
145
|
+
}
|
|
146
|
+
async function jobDirectory(root, slug, id = randomUUID()) {
|
|
147
|
+
if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail("bad-request", "Invalid media job identifier.");
|
|
148
|
+
const parent = join(root, `.${slug}`);
|
|
149
|
+
await mkdir(parent, { recursive: true });
|
|
150
|
+
await confinedPath(root, parent);
|
|
151
|
+
const directory = join(parent, id);
|
|
152
|
+
await mkdir(directory, { recursive: true });
|
|
153
|
+
await confinedPath(root, directory);
|
|
154
|
+
return {
|
|
155
|
+
id,
|
|
156
|
+
directory
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function writeReceipt(directory, receipt) {
|
|
160
|
+
const target = join(directory, "receipt.json"), temporary = join(directory, `receipt-${randomUUID()}.tmp`);
|
|
161
|
+
await confinedPath(directory, directory);
|
|
162
|
+
await writeFile(temporary, JSON.stringify(receipt, null, 2) + "\n", {
|
|
163
|
+
flag: "wx",
|
|
164
|
+
mode: 384
|
|
165
|
+
});
|
|
166
|
+
await rename(temporary, target);
|
|
167
|
+
}
|
|
168
|
+
async function readReceipt(root, slug, id) {
|
|
169
|
+
if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail("bad-request", "Invalid media job identifier.");
|
|
170
|
+
const path = await confinedPath(root, join(root, `.${slug}`, id, "receipt.json"));
|
|
171
|
+
if ((await lstat(path)).size > 1048576) throw fail("invalid-receipt", "The media receipt is too large.");
|
|
172
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
173
|
+
}
|
|
174
|
+
async function publishMedia(host, source, sourceRoot, destination, workspace, kind, signal) {
|
|
175
|
+
signal.throwIfAborted();
|
|
176
|
+
const path = await confinedPath(sourceRoot, source), info = await inspectMedia(path, kind);
|
|
177
|
+
if (kind === "image" && ![
|
|
178
|
+
"image/png",
|
|
179
|
+
"image/jpeg",
|
|
180
|
+
"image/gif",
|
|
181
|
+
"image/webp"
|
|
182
|
+
].includes(info.mediaType)) throw fail("unsupported-media", "The host cannot attach this image format. Use PNG, JPEG, GIF or WebP.");
|
|
183
|
+
await confinedPath(workspace, destination);
|
|
184
|
+
const extension = {
|
|
185
|
+
"image/png": ".png",
|
|
186
|
+
"image/jpeg": ".jpg",
|
|
187
|
+
"image/gif": ".gif",
|
|
188
|
+
"image/webp": ".webp",
|
|
189
|
+
"audio/mpeg": ".mp3",
|
|
190
|
+
"audio/wav": ".wav",
|
|
191
|
+
"audio/flac": ".flac",
|
|
192
|
+
"audio/ogg": ".ogg",
|
|
193
|
+
"audio/mp4": ".m4a",
|
|
194
|
+
"video/mp4": ".mp4",
|
|
195
|
+
"video/quicktime": ".mov",
|
|
196
|
+
"video/webm": ".webm",
|
|
197
|
+
"audio/webm": ".webm"
|
|
198
|
+
}[info.mediaType];
|
|
199
|
+
const rawName = basename(path).replace(/[^\w.\-]/g, "_").slice(-100) || kind;
|
|
200
|
+
const name = extension ? rawName.replace(/\.[^.]*$/, "") + extension : rawName;
|
|
201
|
+
const reuse = insideDirectory(destination, path) && await realpath(sourceRoot) === await realpath(workspace);
|
|
202
|
+
const target = reuse ? path : join(destination, `${randomUUID().slice(0, 8)}-${name}`);
|
|
203
|
+
if (kind === "image") {
|
|
204
|
+
const input = {
|
|
205
|
+
data: await readFile(path),
|
|
206
|
+
mediaType: info.mediaType,
|
|
207
|
+
name
|
|
208
|
+
};
|
|
209
|
+
await host.attachments.validateImage(input);
|
|
210
|
+
signal.throwIfAborted();
|
|
211
|
+
if (!reuse) await writeFile(target, input.data, {
|
|
212
|
+
flag: "wx",
|
|
213
|
+
signal
|
|
214
|
+
});
|
|
215
|
+
const [attachment] = await host.attachments.saveImages([input]);
|
|
216
|
+
signal.throwIfAborted();
|
|
217
|
+
return {
|
|
218
|
+
path: relative(workspace, target).split(sep).join("/"),
|
|
219
|
+
name,
|
|
220
|
+
kind,
|
|
221
|
+
...info,
|
|
222
|
+
attachment: {
|
|
223
|
+
type: "image",
|
|
224
|
+
attachment
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (!reuse) await pipeline(createReadStream(path), createWriteStream(target, { flags: "wx" }), { signal });
|
|
229
|
+
const attachment = await host.attachments.saveFileStream({
|
|
230
|
+
data: bounded(createReadStream(target), MEDIA_LIMITS[kind], signal),
|
|
231
|
+
name,
|
|
232
|
+
signal
|
|
233
|
+
});
|
|
234
|
+
signal.throwIfAborted();
|
|
235
|
+
return {
|
|
236
|
+
path: relative(workspace, target).split(sep).join("/"),
|
|
237
|
+
name,
|
|
238
|
+
kind,
|
|
239
|
+
...info,
|
|
240
|
+
attachment: {
|
|
241
|
+
type: "file",
|
|
242
|
+
attachment
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
export { CliMediaError, MEDIA_LIMITS, confinedPath, insideDirectory, inspectMedia, jobDirectory, mediaWorkspace, publishMedia, readReceipt, stageMedia, writeReceipt };
|
|
248
|
+
|
|
249
|
+
//# sourceMappingURL=cli-media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-media.js","names":[],"sources":["../src/cli-media.ts"],"sourcesContent":["import { createReadStream, createWriteStream } from 'node:fs'\nimport { lstat, realpath, mkdir, open, readFile, writeFile, rename } from 'node:fs/promises'\nimport { pipeline } from 'node:stream/promises'\nimport { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { AttachmentStore, ImageAttachmentRef, FileAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'\n\nexport type MediaKind = 'image' | 'audio' | 'video'\nexport type MediaInput = { path?: string; attachmentId?: string; kind: MediaKind }\nexport type MediaExec = { signal: AbortSignal; agent?: { session: { id: unknown } } }\nexport type MediaHost = {\n sandboxPolicy: { resolve(input: { session?: unknown }): { workspaceRoot: string; mode: string } };\n attachments: AttachmentStore;\n get?(name: string): unknown;\n}\nexport type PublishedMedia = {\n path: string; name: string; kind: MediaKind; mediaType: string; bytes: number;\n attachment: { type: 'image'; attachment: ImageAttachmentRef } | { type: 'file'; attachment: FileAttachmentRef };\n}\nexport class CliMediaError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliMediaError' }\n}\nconst fail = (code: string, message: string) => new CliMediaError(code, message)\nexport const MEDIA_LIMITS: Record<MediaKind, number> = { image: 30 * 1024 * 1024, audio: 100 * 1024 * 1024, video: 256 * 1024 * 1024 }\nexport function insideDirectory(root: string, path: string): boolean {\n const rel = relative(root, path)\n return !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)\n}\n/** Resolve each path component; links are not accepted as media or receipt boundaries. */\nexport async function confinedPath(root: string, path: string): Promise<string> {\n const base = await realpath(root), absolute = resolve(base, path)\n if (!insideDirectory(base, absolute) || (await lstat(root)).isSymbolicLink()) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n let part = base\n for (const component of relative(base, absolute).split(sep).filter(Boolean)) {\n part = join(part, component)\n if ((await lstat(part)).isSymbolicLink()) throw fail('outside-workspace', 'Symbolic links are not accepted for media files.')\n }\n const canonical = await realpath(absolute)\n if (!insideDirectory(base, canonical)) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n return canonical\n}\nexport async function mediaWorkspace(host: MediaHost, exec: MediaExec, write: boolean): Promise<string> {\n exec.signal.throwIfAborted()\n const policy = host.sandboxPolicy.resolve({ session: exec.agent?.session })\n if (write && policy.mode === 'read-only') throw fail('read-only', 'This operation saves media and needs workspace write access.')\n const root = await realpath(policy.workspaceRoot)\n if (!(await lstat(root)).isDirectory()) throw fail('invalid-workspace', 'The workspace is unavailable.')\n return root\n}\nexport async function inspectMedia(path: string, kind: MediaKind): Promise<{ mediaType: string; bytes: number }> {\n const info = await lstat(path)\n if (!info.isFile() || info.isSymbolicLink() || info.size < 4 || info.size > MEDIA_LIMITS[kind]) throw fail('invalid-media', 'The media file is empty, too large, or not a regular file.')\n const file = await open(path, 'r'), head = Buffer.alloc(64)\n try { await file.read(head, 0, head.length, 0) } finally { await file.close() }\n let mediaType: string | undefined\n if (head.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) mediaType = 'image/png'\n else if (head[0] === 255 && head[1] === 216 && head[2] === 255) mediaType = 'image/jpeg'\n else if (/^GIF8[79]a$/.test(head.toString('ascii', 0, 6))) mediaType = 'image/gif'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP') mediaType = 'image/webp'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WAVE') mediaType = 'audio/wav'\n else if (head.toString('ascii', 0, 3) === 'ID3' || (head[0] === 255 && (head[1] & 224) === 224)) mediaType = 'audio/mpeg'\n else if (head.toString('ascii', 0, 4) === 'fLaC') mediaType = 'audio/flac'\n else if (head.toString('ascii', 0, 4) === 'OggS') mediaType = 'audio/ogg'\n else if (head.toString('ascii', 4, 8) === 'ftyp') {\n const brand = head.toString('ascii', 8, 12)\n mediaType = /^(?:heic|heix|hevc|hevx|mif1|msf1)$/.test(brand) ? 'image/heic' : kind === 'audio' ? 'audio/mp4' : brand === 'qt ' ? 'video/quicktime' : 'video/mp4'\n } else if (head.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]))) mediaType = kind === 'audio' ? 'audio/webm' : 'video/webm'\n if (!mediaType?.startsWith(`${kind}/`)) throw fail('invalid-media', `The file is not a supported ${kind} file.`)\n return { mediaType, bytes: info.size }\n}\nasync function sessionAttachments(host: MediaHost, exec: MediaExec): Promise<Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>> {\n if (!exec.agent) throw fail('no-session', 'Session attachments need an active session.')\n const query = host.get?.('sessionQuery') as { readSession(id: unknown): Promise<{ events: unknown[] }> } | undefined\n if (!query) throw fail('no-session-query', 'The host cannot read current-session attachments.')\n const snapshot = await query.readSession(exec.agent.session.id)\n exec.signal.throwIfAborted()\n const result = new Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>()\n for (const raw of snapshot.events) {\n const event = raw as { type?: string; data?: { content?: unknown; message?: { content?: unknown } } }\n if (event.type !== 'user/message' && event.type !== 'tool/result') continue\n const content = event.data?.content ?? event.data?.message?.content\n if (!Array.isArray(content)) continue\n for (const block of content) {\n if ((block?.type === 'image' || block?.type === 'file') && typeof block.attachment?.attachmentId === 'string') result.set(block.attachment.attachmentId, block)\n }\n }\n return result\n}\nasync function* bounded(chunks: AsyncIterable<Uint8Array>, max: number, signal: AbortSignal): AsyncIterable<Uint8Array> {\n let bytes = 0\n for await (const chunk of chunks) {\n signal.throwIfAborted(); bytes += chunk.byteLength\n if (bytes > max) throw fail('invalid-media', 'The media file exceeds its size limit.')\n yield chunk\n }\n signal.throwIfAborted()\n}\nexport async function stageMedia(host: MediaHost, exec: MediaExec, root: string, directory: string, inputs: MediaInput[]): Promise<{ path: string; mediaType: string; bytes: number }[]> {\n if (inputs.length > 40) throw fail('bad-request', 'At most 40 input files are allowed.')\n const refs = inputs.some(input => input.attachmentId) ? await sessionAttachments(host, exec) : new Map()\n const staged: { path: string; mediaType: string; bytes: number }[] = []\n for (const [index, input] of inputs.entries()) {\n exec.signal.throwIfAborted()\n if (!MEDIA_LIMITS[input.kind] || Boolean(input.path) === Boolean(input.attachmentId)) throw fail('bad-request', 'Provide exactly one workspace path or current-session attachment ID per input.')\n const ref = input.attachmentId ? refs.get(input.attachmentId) : undefined\n if (input.attachmentId && !ref) throw fail('unknown-attachment', 'The attachment does not belong to the current session.')\n const source = input.path ? await confinedPath(root, input.path) : undefined\n if (source) await inspectMedia(source, input.kind)\n const rawName = source ? basename(source) : ref!.attachment.name || 'attachment'\n const extension = /^\\.[a-z0-9]{1,8}$/i.test(extname(rawName)) ? extname(rawName).toLowerCase() : ref?.type === 'image' ? '.' + (ref.attachment as ImageAttachmentRef).mediaType.split('/')[1] : '.bin'\n const target = join(directory, `input-${index}${extension}`)\n if (source) await pipeline(createReadStream(source), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n else if (ref!.type === 'image') {\n if (input.kind !== 'image') throw fail('invalid-media', 'An image attachment cannot be used as audio or video.')\n const stored = await host.attachments.readImage(ref!.attachment as ImageAttachmentRef, exec.signal)\n if (stored.data.byteLength > MEDIA_LIMITS.image) throw fail('invalid-media', 'The image is too large.')\n await writeFile(target, stored.data, { flag: 'wx', signal: exec.signal })\n } else await pipeline(bounded(host.attachments.readFileStream(ref!.attachment as FileAttachmentRef, exec.signal), MEDIA_LIMITS[input.kind], exec.signal), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n const info = await inspectMedia(target, input.kind)\n staged.push({ path: target, ...info })\n }\n return staged\n}\nexport async function jobDirectory(root: string, slug: string, id: string = randomUUID()): Promise<{ id: string; directory: string }> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const parent = join(root, `.${slug}`)\n await mkdir(parent, { recursive: true }); await confinedPath(root, parent)\n const directory = join(parent, id)\n await mkdir(directory, { recursive: true }); await confinedPath(root, directory)\n return { id, directory }\n}\nexport async function writeReceipt(directory: string, receipt: unknown): Promise<void> {\n const target = join(directory, 'receipt.json'), temporary = join(directory, `receipt-${randomUUID()}.tmp`)\n await confinedPath(directory, directory)\n await writeFile(temporary, JSON.stringify(receipt, null, 2) + '\\n', { flag: 'wx', mode: 0o600 })\n // Replacement is atomic in this directory; credentials and signed URLs do not belong in receipts.\n await rename(temporary, target)\n}\nexport async function readReceipt<T>(root: string, slug: string, id: string): Promise<T> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const path = await confinedPath(root, join(root, `.${slug}`, id, 'receipt.json'))\n if ((await lstat(path)).size > 1024 * 1024) throw fail('invalid-receipt', 'The media receipt is too large.')\n return JSON.parse(await readFile(path, 'utf8')) as T\n}\nexport async function publishMedia(host: MediaHost, source: string, sourceRoot: string, destination: string, workspace: string, kind: MediaKind, signal: AbortSignal): Promise<PublishedMedia> {\n signal.throwIfAborted()\n const path = await confinedPath(sourceRoot, source), info = await inspectMedia(path, kind)\n if (kind === 'image' && !['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(info.mediaType)) throw fail('unsupported-media', 'The host cannot attach this image format. Use PNG, JPEG, GIF or WebP.')\n await confinedPath(workspace, destination)\n const extension = ({ 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'audio/mpeg': '.mp3', 'audio/wav': '.wav', 'audio/flac': '.flac', 'audio/ogg': '.ogg', 'audio/mp4': '.m4a', 'video/mp4': '.mp4', 'video/quicktime': '.mov', 'video/webm': '.webm', 'audio/webm': '.webm' } as Record<string, string>)[info.mediaType]\n const rawName = basename(path).replace(/[^\\w.\\-]/g, '_').slice(-100) || kind\n const name = extension ? rawName.replace(/\\.[^.]*$/, '') + extension : rawName\n const reuse = insideDirectory(destination, path) && await realpath(sourceRoot) === await realpath(workspace)\n const target = reuse ? path : join(destination, `${randomUUID().slice(0, 8)}-${name}`)\n if (kind === 'image') {\n const input = { data: await readFile(path), mediaType: info.mediaType as ImageMediaType, name }\n await host.attachments.validateImage(input)\n signal.throwIfAborted()\n if (!reuse) await writeFile(target, input.data, { flag: 'wx', signal })\n const [attachment] = await host.attachments.saveImages([input])\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'image', attachment } }\n }\n if (!reuse) await pipeline(createReadStream(path), createWriteStream(target, { flags: 'wx' }), { signal })\n const attachment = await host.attachments.saveFileStream({ data: bounded(createReadStream(target), MEDIA_LIMITS[kind], signal), name, signal })\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'file', attachment } }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
//#region src/cli-process.d.ts
|
|
3
|
+
/** A resolved executable, never a shell command or an argument string. */
|
|
4
|
+
type CliExecutable = {
|
|
5
|
+
command: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
displayPath: string;
|
|
9
|
+
};
|
|
10
|
+
type CliOutput = {
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
};
|
|
15
|
+
declare class CliProcessError extends Error {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
constructor(code: string, message: string);
|
|
18
|
+
}
|
|
19
|
+
type CliOptions = {
|
|
20
|
+
cwd: string;
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
input?: string;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
maxOutputBytes?: number;
|
|
25
|
+
maxLineBytes?: number;
|
|
26
|
+
onLine?: (line: string) => void | Promise<void>;
|
|
27
|
+
spawnChild?: typeof spawn;
|
|
28
|
+
};
|
|
29
|
+
/** Own one process tree until it exits, including caller cancellation and protocol failure. */
|
|
30
|
+
declare function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { CliExecutable, CliOptions, CliOutput, CliProcessError, runCli };
|
|
33
|
+
//# sourceMappingURL=cli-process.d.ts.map
|