dsh-plugin-lcu 0.2.9 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +174 -104
- package/cordis.patch.yml +11 -6
- package/docs/README.zh.md +154 -168
- package/helper/sky-service.mjs +484 -0
- package/lib/app.js +284 -0
- package/lib/app.js.map +1 -0
- package/lib/approval.js +45 -4
- package/lib/approval.js.map +1 -1
- package/lib/connection.js +82 -35
- package/lib/connection.js.map +1 -1
- package/lib/control.js +214 -0
- package/lib/control.js.map +1 -0
- package/lib/diag.js +1 -1
- package/lib/host-guard.js +2 -2
- package/lib/index.js +100 -30
- package/lib/index.js.map +1 -1
- package/lib/session.js +137 -0
- package/lib/session.js.map +1 -0
- package/lib/tool.js +7 -4
- package/lib/tool.js.map +1 -1
- package/lib/types/app.d.ts +118 -0
- package/lib/types/app.d.ts.map +1 -0
- package/lib/types/approval.d.ts +28 -3
- package/lib/types/approval.d.ts.map +1 -1
- package/lib/types/connection.d.ts +27 -13
- package/lib/types/connection.d.ts.map +1 -1
- package/lib/types/control.d.ts +54 -0
- package/lib/types/control.d.ts.map +1 -0
- package/lib/types/diag.d.ts +1 -1
- package/lib/types/host-guard.d.ts +2 -2
- package/lib/types/index.d.ts +26 -6
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/session.d.ts +89 -0
- package/lib/types/session.d.ts.map +1 -0
- package/lib/types/tool.d.ts +6 -5
- package/lib/types/tool.d.ts.map +1 -1
- package/package.json +8 -6
- package/scripts/probe-lcu.mjs +40 -18
- package/scripts/gen-presets.mjs +0 -266
package/lib/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `dsh-plugin-lcu` — drive the desktop and Chrome from DeepSeek Harness.
|
|
3
3
|
*
|
|
4
4
|
* The plugin is one root-scoped row. It does not open anything at load time:
|
|
5
|
-
*
|
|
5
|
+
* The runtime starts only for an Agent whose preset is
|
|
6
6
|
* listed in `presets`, and stops when that Agent is disposed. That keeps a
|
|
7
7
|
* heavy, permission-bearing capability off every session that does not need it.
|
|
8
8
|
*
|
|
@@ -19,11 +19,23 @@ export declare const name = "lcu";
|
|
|
19
19
|
export declare const inject: string[];
|
|
20
20
|
/** Plugin configuration. */
|
|
21
21
|
export interface Config {
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The ChatGPT application whose runtime provides computer use.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to the standard installation. The plugin launches the runtime from
|
|
26
|
+
* inside this application directly — it does not go through a wrapper.
|
|
27
|
+
*/
|
|
28
|
+
app?: string;
|
|
29
|
+
/**
|
|
30
|
+
* An explicit executable replacing the computed launch, for an unusual install.
|
|
31
|
+
*
|
|
32
|
+
* Set this only when the application is somewhere the built-in resolution
|
|
33
|
+
* cannot reach; it bypasses the runtime's own environment setup.
|
|
34
|
+
*/
|
|
23
35
|
command?: string;
|
|
24
|
-
/** Enable the
|
|
36
|
+
/** Enable the runtime's browser surface; needs the official browser extension and site approvals. */
|
|
25
37
|
chrome?: boolean;
|
|
26
|
-
/** Enable the
|
|
38
|
+
/** Enable the runtime's computer-audio recording API. */
|
|
27
39
|
audio?: boolean;
|
|
28
40
|
/**
|
|
29
41
|
* Agent preset ids whose sessions get the tools.
|
|
@@ -35,11 +47,19 @@ export interface Config {
|
|
|
35
47
|
presets?: string[];
|
|
36
48
|
/** Exact HTTP(S) origins pre-approved for browser access, never widened. */
|
|
37
49
|
allowedOrigins?: string[];
|
|
38
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Bundle identifiers computer use may use without asking.
|
|
52
|
+
*
|
|
53
|
+
* The runtime asks before it first uses each application, and an unanswered
|
|
54
|
+
* question is a refusal, so an unattended run needs its targets listed here.
|
|
55
|
+
* The application hosting the agent can never be admitted this way.
|
|
56
|
+
*/
|
|
57
|
+
allowedApps?: string[];
|
|
58
|
+
/** Prompt section order for the injected runtime instructions. */
|
|
39
59
|
sectionOrder?: number;
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
42
|
-
* Mount the
|
|
62
|
+
* Mount the computer-use capability.
|
|
43
63
|
*
|
|
44
64
|
* @param ctx - the host context this row mounts into.
|
|
45
65
|
* @param config - the row's configuration.
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD,OAAO,EAC+C,kBAAkB,EAEvE,MAAM,iBAAiB,CAAA;AAUxB,eAAO,MAAM,IAAI,QAAQ,CAAA;AACzB,eAAO,MAAM,MAAM,UAA4B,CAAA;AAyB/C,4BAA4B;AAC5B,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qGAAqG;IACrG,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AA8BD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAyWxD;AAYD,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connection an Agent's tools talk to.
|
|
3
|
+
*
|
|
4
|
+
* The runtime is executed from files inside the ChatGPT application, and the
|
|
5
|
+
* application replaces those files when it updates. A session that stays
|
|
6
|
+
* connected across an update therefore runs a mix of two generations — the
|
|
7
|
+
* runtime it started with and the modules a later `import` resolves from the new
|
|
8
|
+
* bundle — and the failure that follows is not obvious from the outside.
|
|
9
|
+
*
|
|
10
|
+
* This is a thin facade rather than a fix inside the connection: the tools close
|
|
11
|
+
* over one object for the life of the Agent, so that object has to be able to
|
|
12
|
+
* become a different connection. Before every call it compares the application's
|
|
13
|
+
* fingerprint, and when it changed it opens a new connection and retires the old
|
|
14
|
+
* one, so all later calls come from a single generation.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-plugin-lcu/session
|
|
17
|
+
*/
|
|
18
|
+
import type { LcuConnection } from './connection.ts';
|
|
19
|
+
/**
|
|
20
|
+
* What the tools need from a connection.
|
|
21
|
+
*
|
|
22
|
+
* Written as indexed access types rather than restated signatures, so a change to
|
|
23
|
+
* the connection cannot leave this out of date.
|
|
24
|
+
*/
|
|
25
|
+
export interface RuntimeConnection {
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
modelTools: LcuConnection['modelTools'];
|
|
28
|
+
readonly hasHostControl: boolean;
|
|
29
|
+
callTool: LcuConnection['callTool'];
|
|
30
|
+
turnEnded: LcuConnection['turnEnded'];
|
|
31
|
+
controlStatus: LcuConnection['controlStatus'];
|
|
32
|
+
controlStop: LcuConnection['controlStop'];
|
|
33
|
+
close: LcuConnection['close'];
|
|
34
|
+
}
|
|
35
|
+
/** How the session opens a connection, and how it notices the application moved. */
|
|
36
|
+
export interface ManagedSessionOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Open a fresh connection.
|
|
39
|
+
*
|
|
40
|
+
* @param reason - whether this is the first connection or a replacement.
|
|
41
|
+
*/
|
|
42
|
+
connect: (reason: 'initial' | 'regenerated') => Promise<LcuConnection>;
|
|
43
|
+
/**
|
|
44
|
+
* The application's current fingerprint.
|
|
45
|
+
*
|
|
46
|
+
* Called before every call, so it must stay cheap: it reads two file
|
|
47
|
+
* timestamps. It may throw, which is treated as "the application moved" and
|
|
48
|
+
* lets the reconnect report the real error.
|
|
49
|
+
*/
|
|
50
|
+
generation: () => string;
|
|
51
|
+
/** Called after a successful replacement, for the diagnostic log. */
|
|
52
|
+
onRegenerated?: (transition: {
|
|
53
|
+
readonly from: string;
|
|
54
|
+
readonly to: string;
|
|
55
|
+
}) => void;
|
|
56
|
+
/** Called when a replacement fails. The previous connection stays in use. */
|
|
57
|
+
onRegenerateFailed?: (error: unknown) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Called when the fingerprint cannot be read at all.
|
|
60
|
+
*
|
|
61
|
+
* A fingerprint that cannot be read is **not** evidence that anything changed,
|
|
62
|
+
* and opening a second runtime because of a transient read failure is worse
|
|
63
|
+
* than waiting: the next call checks again.
|
|
64
|
+
*/
|
|
65
|
+
onGenerationUnreadable?: (error: unknown) => void;
|
|
66
|
+
}
|
|
67
|
+
/** A connection that follows the application across an update. */
|
|
68
|
+
export declare class ManagedSession implements RuntimeConnection {
|
|
69
|
+
#private;
|
|
70
|
+
private constructor();
|
|
71
|
+
/**
|
|
72
|
+
* Open the first connection.
|
|
73
|
+
*
|
|
74
|
+
* @param options - how to connect and how to notice a change.
|
|
75
|
+
* @returns the session, already connected.
|
|
76
|
+
*/
|
|
77
|
+
static open(options: ManagedSessionOptions): Promise<ManagedSession>;
|
|
78
|
+
get sessionId(): string | undefined;
|
|
79
|
+
set sessionId(value: string | undefined);
|
|
80
|
+
get hasHostControl(): boolean;
|
|
81
|
+
modelTools(): ReturnType<LcuConnection['modelTools']>;
|
|
82
|
+
callTool(...args: Parameters<LcuConnection['callTool']>): ReturnType<LcuConnection['callTool']>;
|
|
83
|
+
turnEnded(...args: Parameters<LcuConnection['turnEnded']>): ReturnType<LcuConnection['turnEnded']>;
|
|
84
|
+
controlStatus(...args: Parameters<LcuConnection['controlStatus']>): ReturnType<LcuConnection['controlStatus']>;
|
|
85
|
+
controlStop(...args: Parameters<LcuConnection['controlStop']>): ReturnType<LcuConnection['controlStop']>;
|
|
86
|
+
/** Retire the connection. Later calls fail rather than opening a new runtime. */
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACvC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;IAChC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;IACnC,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC,aAAa,EAAE,aAAa,CAAC,eAAe,CAAC,CAAA;IAC7C,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IACzC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,KAAK,OAAO,CAAC,aAAa,CAAC,CAAA;IACtE;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,MAAM,CAAA;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACpF,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAClD;AAED,kEAAkE;AAClE,qBAAa,cAAe,YAAW,iBAAiB;;IAgBtD,OAAO;IAMP;;;;;OAKG;WACU,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IAc1E,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IAED,IAAI,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAGtC;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,UAAU,IAAI,UAAU,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IA2C/C,QAAQ,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IAK/F,SAAS,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,GAAG,UAAU,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;IAKlG,aAAa,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,GAAG,UAAU,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;IAK9G,WAAW,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IAK9G,iFAAiF;IAC3E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B"}
|
package/lib/types/tool.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
15
|
-
import type {
|
|
15
|
+
import type { LcuContentBlock, LcuTool } from './connection.ts';
|
|
16
|
+
import type { RuntimeConnection } from './session.ts';
|
|
16
17
|
interface ImageBlock {
|
|
17
18
|
readonly type: 'image';
|
|
18
19
|
readonly data: string;
|
|
@@ -96,7 +97,7 @@ export declare function stuckStopHint(text: string): string | undefined;
|
|
|
96
97
|
* @param options - host hooks around the call.
|
|
97
98
|
* @returns a registrable tool definition.
|
|
98
99
|
*/
|
|
99
|
-
export declare function createLcuTool(descriptor: LcuTool, connection:
|
|
100
|
+
export declare function createLcuTool(descriptor: LcuTool, connection: RuntimeConnection, ctx: Context, options?: LcuToolOptions): ToolDefinition;
|
|
100
101
|
/**
|
|
101
102
|
* Build every model-facing tool for one agent's connection.
|
|
102
103
|
*
|
|
@@ -105,7 +106,7 @@ export declare function createLcuTool(descriptor: LcuTool, connection: LcuConnec
|
|
|
105
106
|
* @returns tool definitions for `js` and `js_reset`, in server order.
|
|
106
107
|
* @throws {LcuError} when the server does not expose the model tools.
|
|
107
108
|
*/
|
|
108
|
-
export declare function buildLcuTools(connection:
|
|
109
|
+
export declare function buildLcuTools(connection: RuntimeConnection, ctx: Context, options?: LcuToolOptions): ToolDefinition[];
|
|
109
110
|
/**
|
|
110
111
|
* The explicit per-app release, which LCU exposes to a host rather than to the
|
|
111
112
|
* model.
|
|
@@ -119,7 +120,7 @@ export declare function buildLcuTools(connection: LcuConnection, ctx: Context, o
|
|
|
119
120
|
* @param currentTurn - resolves the agent's active turn, or `undefined` outside one.
|
|
120
121
|
* @returns a registrable tool definition.
|
|
121
122
|
*/
|
|
122
|
-
export declare function createComputerUseStopTool(connection:
|
|
123
|
+
export declare function createComputerUseStopTool(connection: RuntimeConnection, currentTurn: () => string | undefined): ToolDefinition;
|
|
123
124
|
/**
|
|
124
125
|
* Register the model-facing tools in one agent's scope.
|
|
125
126
|
*
|
|
@@ -132,6 +133,6 @@ export declare function createComputerUseStopTool(connection: LcuConnection, cur
|
|
|
132
133
|
* @param ctx - the plugin context.
|
|
133
134
|
* @returns a disposer that unregisters every tool.
|
|
134
135
|
*/
|
|
135
|
-
export declare function registerLcuTools(agentCtx: Context, connection:
|
|
136
|
+
export declare function registerLcuTools(agentCtx: Context, connection: RuntimeConnection, ctx: Context, options?: LcuToolOptions): () => void;
|
|
136
137
|
export {};
|
|
137
138
|
//# sourceMappingURL=tool.d.ts.map
|
package/lib/types/tool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,cAAc,EAAsD,MAAM,wBAAwB,CAAA;AAEhH,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,cAAc,EAAsD,MAAM,wBAAwB,CAAA;AAEhH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAUrD,UAAU,UAAU;IAClB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAQlF;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtE;AA2GD,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;CAChD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAO5E;AAWD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWtE;AAcD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,iBAAiB,EAC7B,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,cAAmB,GAC3B,cAAc,CA+DhB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,iBAAiB,EAC7B,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,cAAmB,GAC3B,cAAc,EAAE,CAOlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,iBAAiB,EAC7B,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,GACpC,cAAc,CAsEhB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,iBAAiB,EAC7B,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,cAAmB,GAC3B,MAAM,IAAI,CASZ"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lcu",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Drive the desktop and Chrome from DeepSeek Harness
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Drive the desktop and Chrome from DeepSeek Harness using the computer-use runtime inside the ChatGPT desktop application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
7
7
|
"deepseek-harness",
|
|
8
8
|
"dsh",
|
|
9
9
|
"computer-use",
|
|
10
10
|
"browser-use",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
11
|
+
"cua",
|
|
12
|
+
"chatgpt",
|
|
13
|
+
"desktop-automation",
|
|
14
|
+
"macos"
|
|
14
15
|
],
|
|
15
16
|
"license": "MIT",
|
|
16
17
|
"author": {
|
|
@@ -36,7 +37,9 @@
|
|
|
36
37
|
"./package.json": "./package.json"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
40
|
+
"CHANGELOG.md",
|
|
39
41
|
"lib",
|
|
42
|
+
"helper",
|
|
40
43
|
"cordis.patch.yml",
|
|
41
44
|
"scripts",
|
|
42
45
|
"dev-types",
|
|
@@ -58,7 +61,6 @@
|
|
|
58
61
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
62
|
"test": "node --test \"tests/*.spec.ts\"",
|
|
60
63
|
"prepare": "npm run build",
|
|
61
|
-
"presets": "node scripts/gen-presets.mjs",
|
|
62
64
|
"probe": "node scripts/probe-lcu.mjs",
|
|
63
65
|
"update-codex": "node scripts/update-codex.mjs"
|
|
64
66
|
},
|
package/scripts/probe-lcu.mjs
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* probe-lcu.mjs — talk to the
|
|
3
|
+
* probe-lcu.mjs — talk to the computer-use runtime directly.
|
|
4
4
|
*
|
|
5
|
-
* A diagnostic for `dsh-plugin-lcu` development: it
|
|
6
|
-
*
|
|
7
|
-
* declared), prints what the
|
|
5
|
+
* A diagnostic for `dsh-plugin-lcu` development: it builds the same launch plan
|
|
6
|
+
* the plugin builds, connects the way the plugin does (newline-delimited
|
|
7
|
+
* JSON-RPC over stdio, `capabilities.elicitation` declared), prints what the
|
|
8
|
+
* server advertises, and optionally calls one tool. No harness is involved, so
|
|
9
|
+
* this separates a plugin problem from a runtime problem.
|
|
8
10
|
*
|
|
9
11
|
* Usage
|
|
10
12
|
* -----
|
|
11
|
-
* node scripts/probe-lcu.mjs
|
|
12
|
-
* node scripts/probe-lcu.mjs --instructions
|
|
13
|
-
* node scripts/probe-lcu.mjs --call js --code '
|
|
14
|
-
* node scripts/probe-lcu.mjs --
|
|
13
|
+
* node scripts/probe-lcu.mjs # initialize + tools/list
|
|
14
|
+
* node scripts/probe-lcu.mjs --instructions # also dump server instructions
|
|
15
|
+
* node scripts/probe-lcu.mjs --call js --code 'await cua.getState();'
|
|
16
|
+
* node scripts/probe-lcu.mjs --chrome # enable the browser surface
|
|
17
|
+
* node scripts/probe-lcu.mjs --app /path/ChatGPT.app # a different installation
|
|
18
|
+
* node scripts/probe-lcu.mjs --command /path/binary # bypass the computed launch
|
|
15
19
|
*
|
|
16
20
|
* It never answers elicitations, so any call that needs an approval fails closed
|
|
17
21
|
* exactly like a host that cannot present the request.
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
import { spawn } from 'node:child_process'
|
|
21
|
-
import { homedir } from 'node:os'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
25
|
import { createInterface } from 'node:readline'
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
import { planLaunch } from '../lib/app.js'
|
|
26
28
|
|
|
27
29
|
/** Newest protocol revision the MCP SDK pair here is expected to negotiate. */
|
|
28
30
|
const PROTOCOL_VERSION = '2025-06-18'
|
|
29
31
|
|
|
30
32
|
function parseArgs(argv) {
|
|
31
|
-
const opts = {
|
|
33
|
+
const opts = {
|
|
34
|
+
app: undefined, command: undefined, chrome: false, audio: false,
|
|
35
|
+
instructions: false, call: undefined, code: undefined, limit: 2000,
|
|
36
|
+
}
|
|
32
37
|
for (let i = 0; i < argv.length; i += 1) {
|
|
33
38
|
const arg = argv[i]
|
|
34
|
-
if (arg === '--
|
|
39
|
+
if (arg === '--app') opts.app = argv[++i]
|
|
40
|
+
else if (arg === '--command') opts.command = argv[++i]
|
|
41
|
+
else if (arg === '--chrome') opts.chrome = true
|
|
42
|
+
else if (arg === '--audio') opts.audio = true
|
|
35
43
|
else if (arg === '--instructions') opts.instructions = true
|
|
36
44
|
else if (arg === '--call') opts.call = argv[++i]
|
|
37
45
|
else if (arg === '--code') opts.code = argv[++i]
|
|
@@ -41,17 +49,17 @@ function parseArgs(argv) {
|
|
|
41
49
|
return opts
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
/** One request/response session with the
|
|
52
|
+
/** One request/response session with the computer-use runtime. */
|
|
45
53
|
class LcuSession {
|
|
46
|
-
constructor(
|
|
54
|
+
constructor(plan) {
|
|
47
55
|
this.nextId = 1
|
|
48
56
|
this.pending = new Map()
|
|
49
57
|
this.notifications = []
|
|
50
58
|
this.stderr = ''
|
|
51
|
-
this.child = spawn(command, [], { stdio: ['pipe', 'pipe', 'pipe'], env:
|
|
59
|
+
this.child = spawn(plan.command, [...plan.args], { stdio: ['pipe', 'pipe', 'pipe'], env: plan.env })
|
|
52
60
|
this.child.stderr.on('data', (chunk) => { this.stderr += chunk.toString() })
|
|
53
61
|
this.child.on('exit', (code) => {
|
|
54
|
-
for (const { reject } of this.pending.values()) reject(new Error(`
|
|
62
|
+
for (const { reject } of this.pending.values()) reject(new Error(`the runtime exited (${code})\n${this.stderr.slice(-800)}`))
|
|
55
63
|
this.pending.clear()
|
|
56
64
|
})
|
|
57
65
|
createInterface({ input: this.child.stdout }).on('line', (line) => this.#onLine(line))
|
|
@@ -96,7 +104,21 @@ class LcuSession {
|
|
|
96
104
|
|
|
97
105
|
async function main() {
|
|
98
106
|
const opts = parseArgs(process.argv.slice(2))
|
|
99
|
-
const
|
|
107
|
+
const plan = planLaunch({
|
|
108
|
+
...(opts.app === undefined ? {} : { appPath: opts.app }),
|
|
109
|
+
...(opts.command === undefined ? {} : { command: opts.command }),
|
|
110
|
+
chrome: opts.chrome,
|
|
111
|
+
audio: opts.audio,
|
|
112
|
+
identity: 'dsh-plugin-lcu-probe',
|
|
113
|
+
})
|
|
114
|
+
console.log('=== launch plan ===')
|
|
115
|
+
console.log(` app: ${plan.paths.app}`)
|
|
116
|
+
console.log(` version: ${plan.paths.version}`)
|
|
117
|
+
console.log(` runtime: ${plan.paths.runtimeVersion}`)
|
|
118
|
+
console.log(` command: ${plan.command}`)
|
|
119
|
+
console.log(` args: ${JSON.stringify(plan.args)}`)
|
|
120
|
+
console.log(` surfaces: ${String(plan.env.CUA_REPL_ENABLED_SURFACES)}`)
|
|
121
|
+
const session = new LcuSession(plan)
|
|
100
122
|
try {
|
|
101
123
|
const initialized = await session.request('initialize', {
|
|
102
124
|
protocolVersion: PROTOCOL_VERSION,
|
package/scripts/gen-presets.mjs
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* gen-presets.mjs — derive DSH agent presets from the installed `ptc` preset.
|
|
4
|
-
*
|
|
5
|
-
* Why this exists
|
|
6
|
-
* ---------------
|
|
7
|
-
* DSH agent presets have **no inheritance** (`config.plugins` is a complete
|
|
8
|
-
* child-plugin list) and `cordis.patch.yml` patches **replace** a whole entry's
|
|
9
|
-
* config rather than deep-merging it. So a custom preset has to restate the full
|
|
10
|
-
* plugin list of whatever it is based on. This script reads that base list
|
|
11
|
-
* straight out of the *installed* app and regenerates our preset rows, so a DSH
|
|
12
|
-
* upgrade is one re-run instead of a hand merge.
|
|
13
|
-
*
|
|
14
|
-
* It is deliberately dependency-free (plain Node, no YAML library): the base
|
|
15
|
-
* file is copied line-for-line and only surgically edited, and every expected
|
|
16
|
-
* edit is asserted — if a DSH upgrade changes the shape, this script fails loudly
|
|
17
|
-
* instead of emitting a subtly wrong preset.
|
|
18
|
-
*
|
|
19
|
-
* Usage
|
|
20
|
-
* -----
|
|
21
|
-
* node scripts/gen-presets.mjs # write into the desktop profile patch
|
|
22
|
-
* node scripts/gen-presets.mjs --dry-run # print, write nothing
|
|
23
|
-
* node scripts/gen-presets.mjs --out FILE # write to FILE instead
|
|
24
|
-
* node scripts/gen-presets.mjs --with-heavy # also emit preset-heavy (needs dsh-plugin-lcu)
|
|
25
|
-
* node scripts/gen-presets.mjs --asar PATH # override the app.asar path
|
|
26
|
-
* node scripts/gen-presets.mjs --profile DIR # override the profile directory
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs'
|
|
30
|
-
import { homedir } from 'node:os'
|
|
31
|
-
import { join } from 'node:path'
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Defaults
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
const DEFAULT_ASAR =
|
|
38
|
-
'/Applications/DeepSeek Harness.app/Contents/Resources/app.asar'
|
|
39
|
-
const BASE_PRESET = 'dsh/node_modules/@deepseek-ai/dsh-web-app/presets/ptc.patch.yml'
|
|
40
|
-
const DEFAULT_PROFILE = join(homedir(), '.dsh', 'profiles', 'desktop')
|
|
41
|
-
|
|
42
|
-
const BEGIN = '# >>> BEGIN generated presets (scripts/gen-presets.mjs) >>>'
|
|
43
|
-
const END = '# <<< END generated presets <<<'
|
|
44
|
-
|
|
45
|
-
/** The two presets this script owns. `daily` is a faithful PTC + Codex; `heavy`
|
|
46
|
-
* flips the tool presentation to `both` so LCU's `js` is callable natively
|
|
47
|
-
* instead of being nested inside a `run_code` program. */
|
|
48
|
-
const PRESETS = {
|
|
49
|
-
daily: {
|
|
50
|
-
id: 'daily',
|
|
51
|
-
name: '日常',
|
|
52
|
-
description: '日常编码:PTC 程序化工具调用 + Codex 委派',
|
|
53
|
-
order: 5,
|
|
54
|
-
presentation: null, // keep the base's `ptc`
|
|
55
|
-
enable: ['tool-subagent-codex'],
|
|
56
|
-
append: [],
|
|
57
|
-
},
|
|
58
|
-
heavy: {
|
|
59
|
-
id: 'heavy',
|
|
60
|
-
name: '重活',
|
|
61
|
-
description: '超前/困难的重活:需要 Computer 或 Browser 时用',
|
|
62
|
-
order: 6,
|
|
63
|
-
// `both` so LCU's `js` stays callable natively: in pure `ptc` presentation
|
|
64
|
-
// the model would have to nest a whole CUA program inside a run_code string.
|
|
65
|
-
// No tool row is added here — `dsh-plugin-lcu` is mounted at the root and
|
|
66
|
-
// registers its tools per Agent by checking this preset's id.
|
|
67
|
-
presentation: 'both',
|
|
68
|
-
enable: ['tool-subagent-codex'],
|
|
69
|
-
append: [],
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Minimal asar reader (Electron's asar is a simple header + flat blob)
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Read one file out of an asar archive without Electron.
|
|
79
|
-
* @param asarPath - absolute path to the `.asar` file
|
|
80
|
-
* @param innerPath - slash-separated path inside the archive
|
|
81
|
-
* @returns the file contents
|
|
82
|
-
*/
|
|
83
|
-
function readAsarFile(asarPath, innerPath) {
|
|
84
|
-
const fd = openSync(asarPath, 'r')
|
|
85
|
-
try {
|
|
86
|
-
const head = Buffer.alloc(16)
|
|
87
|
-
readSync(fd, head, 0, 16, 0)
|
|
88
|
-
const baseOffset = 8 + head.readUInt32LE(4)
|
|
89
|
-
const jsonSize = head.readUInt32LE(12)
|
|
90
|
-
const json = Buffer.alloc(jsonSize)
|
|
91
|
-
readSync(fd, json, 0, jsonSize, 16)
|
|
92
|
-
let node = JSON.parse(json.toString('utf8'))
|
|
93
|
-
for (const segment of innerPath.split('/').filter(Boolean)) {
|
|
94
|
-
node = node.files?.[segment]
|
|
95
|
-
if (node === undefined) throw new Error(`not in archive: ${innerPath}`)
|
|
96
|
-
}
|
|
97
|
-
if (node.files !== undefined) throw new Error(`is a directory: ${innerPath}`)
|
|
98
|
-
const buf = Buffer.alloc(node.size)
|
|
99
|
-
readSync(fd, buf, 0, node.size, baseOffset + Number(node.offset))
|
|
100
|
-
return buf
|
|
101
|
-
} finally {
|
|
102
|
-
closeSync(fd)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Line surgery
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
/** Indentation (0-based column of the first non-space char); -1 for blank lines. */
|
|
111
|
-
const indentOf = (line) => (line.trim() === '' ? -1 : line.search(/\S/))
|
|
112
|
-
|
|
113
|
-
/** Index of the child row `- id: <rowId>` inside a plugin list. */
|
|
114
|
-
function findRow(lines, rowId) {
|
|
115
|
-
const index = lines.findIndex((line) => line.trim().startsWith(`- id: ${rowId}`))
|
|
116
|
-
if (index < 0) throw new Error(`plugin row not found: ${rowId}`)
|
|
117
|
-
return index
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Iterate the direct/indirect body lines of the row starting at `start`,
|
|
122
|
-
* stopping at the next row or any line dedented to the row's own level.
|
|
123
|
-
*/
|
|
124
|
-
function* body(lines, start) {
|
|
125
|
-
const base = indentOf(lines[start])
|
|
126
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
127
|
-
const ind = indentOf(lines[i])
|
|
128
|
-
if (ind === -1) continue
|
|
129
|
-
if (ind <= base) return
|
|
130
|
-
yield i
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Remove `disabled: true` from one row, turning it on.
|
|
136
|
-
* @returns a new line array
|
|
137
|
-
*/
|
|
138
|
-
function enableRow(lines, rowId) {
|
|
139
|
-
const start = findRow(lines, rowId)
|
|
140
|
-
for (const i of body(lines, start)) {
|
|
141
|
-
if (lines[i].trim().startsWith('disabled:')) {
|
|
142
|
-
return lines.filter((_, k) => k !== i)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return lines // already enabled — idempotent
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Rewrite `mode:` inside the `tool-presentation` row. */
|
|
149
|
-
function setPresentationMode(lines, mode) {
|
|
150
|
-
const start = findRow(lines, 'tool-presentation')
|
|
151
|
-
for (const i of body(lines, start)) {
|
|
152
|
-
if (/^\s*mode:\s*\S+\s*$/.test(lines[i])) {
|
|
153
|
-
const next = [...lines]
|
|
154
|
-
next[i] = lines[i].replace(/mode:\s*\S+\s*$/, `mode: ${mode}`)
|
|
155
|
-
return next
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
throw new Error('tool-presentation row has no `mode:` field')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Extract the `plugins:` sub-tree (the row list) from a preset patch file. */
|
|
162
|
-
function extractPlugins(lines) {
|
|
163
|
-
const index = lines.findIndex((line) => /^\s*plugins:\s*$/.test(line))
|
|
164
|
-
if (index < 0) throw new Error('base preset has no `plugins:` key')
|
|
165
|
-
const base = indentOf(lines[index])
|
|
166
|
-
const out = []
|
|
167
|
-
for (let i = index + 1; i < lines.length; i += 1) {
|
|
168
|
-
const ind = indentOf(lines[i])
|
|
169
|
-
if (ind !== -1 && ind <= base) break
|
|
170
|
-
out.push(lines[i])
|
|
171
|
-
}
|
|
172
|
-
if (out.every((line) => line.trim() === '')) throw new Error('base `plugins:` is empty')
|
|
173
|
-
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop()
|
|
174
|
-
return out
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Preset rendering
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
|
|
181
|
-
function renderPreset(spec, basePlugins) {
|
|
182
|
-
const plugins = spec.enable.reduce(enableRow, [...basePlugins])
|
|
183
|
-
const withMode = spec.presentation === null
|
|
184
|
-
? plugins
|
|
185
|
-
: setPresentationMode(plugins, spec.presentation)
|
|
186
|
-
const out = [
|
|
187
|
-
` - id: preset-${spec.id}`,
|
|
188
|
-
" name: '@deepseek-ai/dsh-agent-preset'",
|
|
189
|
-
' config:',
|
|
190
|
-
` id: ${spec.id}`,
|
|
191
|
-
` name: '${spec.name}'`,
|
|
192
|
-
` description: '${spec.description}'`,
|
|
193
|
-
` order: ${spec.order}`,
|
|
194
|
-
' plugins:',
|
|
195
|
-
...withMode,
|
|
196
|
-
...spec.append,
|
|
197
|
-
]
|
|
198
|
-
return out
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
// Main
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
|
|
205
|
-
function parseArgs(argv) {
|
|
206
|
-
const opts = {
|
|
207
|
-
dryRun: false,
|
|
208
|
-
withHeavy: false,
|
|
209
|
-
out: undefined,
|
|
210
|
-
asar: DEFAULT_ASAR,
|
|
211
|
-
profile: DEFAULT_PROFILE,
|
|
212
|
-
}
|
|
213
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
214
|
-
const arg = argv[i]
|
|
215
|
-
if (arg === '--dry-run') opts.dryRun = true
|
|
216
|
-
else if (arg === '--with-heavy') opts.withHeavy = true
|
|
217
|
-
else if (arg === '--out') opts.out = argv[++i]
|
|
218
|
-
else if (arg === '--asar') opts.asar = argv[++i]
|
|
219
|
-
else if (arg === '--profile') opts.profile = argv[++i]
|
|
220
|
-
else throw new Error(`unknown argument: ${arg}`)
|
|
221
|
-
}
|
|
222
|
-
return opts
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function buildBlock(baseLines, { withHeavy }) {
|
|
226
|
-
const basePlugins = extractPlugins(baseLines)
|
|
227
|
-
const specs = withHeavy ? [PRESETS.daily, PRESETS.heavy] : [PRESETS.daily]
|
|
228
|
-
const lines = [BEGIN, '- insert:']
|
|
229
|
-
for (const spec of specs) lines.push(...renderPreset(spec, basePlugins))
|
|
230
|
-
lines.push(END)
|
|
231
|
-
return { block: lines.join('\n'), basePlugins, specs }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Replace the marked block in place, or append one. */
|
|
235
|
-
function spliceBlock(text, block) {
|
|
236
|
-
const begin = text.indexOf(BEGIN)
|
|
237
|
-
const end = text.indexOf(END)
|
|
238
|
-
if (begin >= 0 && end > begin) {
|
|
239
|
-
return text.slice(0, begin) + block + text.slice(end + END.length)
|
|
240
|
-
}
|
|
241
|
-
const separator = text.endsWith('\n') ? '\n' : '\n\n'
|
|
242
|
-
return `${text}${separator}${block}\n`
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function main() {
|
|
246
|
-
const opts = parseArgs(process.argv.slice(2))
|
|
247
|
-
const baseLines = readAsarFile(opts.asar, BASE_PRESET).toString('utf8').split('\n')
|
|
248
|
-
const { block, basePlugins, specs } = buildBlock(baseLines, opts)
|
|
249
|
-
const verb = opts.dryRun ? 'would write' : 'wrote'
|
|
250
|
-
const rowCount = basePlugins.filter((line) => /^\s*- id: /.test(line)).length
|
|
251
|
-
|
|
252
|
-
if (opts.dryRun) {
|
|
253
|
-
process.stdout.write(`${block}\n`)
|
|
254
|
-
} else {
|
|
255
|
-
const target = opts.out ?? join(opts.profile, 'cordis.patch.yml')
|
|
256
|
-
const before = readFileSync(target, 'utf8')
|
|
257
|
-
writeFileSync(target, spliceBlock(before, block), { mode: 0o600 })
|
|
258
|
-
process.stderr.write(`${verb} ${target}\n`)
|
|
259
|
-
}
|
|
260
|
-
process.stderr.write(
|
|
261
|
-
`base: ${BASE_PRESET} (${rowCount} top-level rows)\n`
|
|
262
|
-
+ `presets: ${specs.map((s) => s.id).join(', ')}\n`,
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
main()
|