@zhanla/sdk-ts 0.1.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 +347 -0
- package/bin/cli.js +62 -0
- package/bin/discover.js +70 -0
- package/bin/postinstall.js +37 -0
- package/bin/run.js +144 -0
- package/dist/executor.d.ts +13 -0
- package/dist/executor.js +564 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +10 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +43 -0
- package/dist/manifest.d.ts +60 -0
- package/dist/manifest.js +275 -0
- package/dist/trace_store.d.ts +38 -0
- package/dist/trace_store.js +30 -0
- package/dist/types.d.ts +283 -0
- package/dist/types.js +697 -0
- package/dist/wrap.d.ts +37 -0
- package/dist/wrap.js +255 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# @zhanla/sdk-ts
|
|
2
|
+
|
|
3
|
+
`@zhanla/sdk-ts` is the TypeScript SDK for defining bench components in code.
|
|
4
|
+
|
|
5
|
+
You export component instances from a TypeScript module, then run them with `zhanla` or the bundled `@zhanla/sdk-ts` helper CLI.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @zhanla/sdk-ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node `>=18`.
|
|
14
|
+
|
|
15
|
+
Provider packages such as `@anthropic-ai/sdk`, `openai`, and `@google/genai` are optional. Install them only if you use `Runner` with those clients.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
21
|
+
import { Agent, CodeEval, Runner } from "@zhanla/sdk-ts";
|
|
22
|
+
|
|
23
|
+
const runner = new Runner({
|
|
24
|
+
client: new Anthropic(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const supportAgent = new Agent({
|
|
28
|
+
name: "support_agent",
|
|
29
|
+
description: "Respond to support requests.",
|
|
30
|
+
instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
|
|
31
|
+
model: "claude-sonnet-4-6",
|
|
32
|
+
runner,
|
|
33
|
+
outputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
answer: { type: "string" },
|
|
37
|
+
},
|
|
38
|
+
required: ["answer"],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const supportEval = new CodeEval({
|
|
43
|
+
name: "support_eval",
|
|
44
|
+
description: "Check whether an answer was returned.",
|
|
45
|
+
fn: (kwargs: unknown) => {
|
|
46
|
+
const { model_response } = kwargs as { model_response?: string };
|
|
47
|
+
const parsed = model_response ? JSON.parse(model_response) : {};
|
|
48
|
+
return { score: typeof parsed.answer === "string" ? 1.0 : 0.0 };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run it with the CLI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bench run components.ts:support_agent --dataset tickets.json --eval components.ts:support_eval
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Public API
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
Tool,
|
|
64
|
+
CodeEval,
|
|
65
|
+
Skill,
|
|
66
|
+
Agent,
|
|
67
|
+
LLMProcessor,
|
|
68
|
+
LLMEval,
|
|
69
|
+
Runner,
|
|
70
|
+
Orchestration,
|
|
71
|
+
Step,
|
|
72
|
+
Conditional,
|
|
73
|
+
Checklist,
|
|
74
|
+
EvalTree,
|
|
75
|
+
Branch,
|
|
76
|
+
Edge,
|
|
77
|
+
Leaf,
|
|
78
|
+
wrap,
|
|
79
|
+
parseJsonResponse,
|
|
80
|
+
} from "@zhanla/sdk-ts";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
TypeScript discovery is export-based. Only exported component instances are discoverable.
|
|
84
|
+
|
|
85
|
+
## Components
|
|
86
|
+
|
|
87
|
+
### `Tool`
|
|
88
|
+
|
|
89
|
+
Use a `Tool` for deterministic TypeScript logic.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export const lookupCustomer = new Tool({
|
|
93
|
+
name: "lookup_customer",
|
|
94
|
+
description: "Fetch a customer record by ID.",
|
|
95
|
+
inputSchema: { type: "object", properties: {} },
|
|
96
|
+
fn: (kwargs: unknown) => {
|
|
97
|
+
const { customerId } = kwargs as { customerId: string };
|
|
98
|
+
return { id: customerId, email: "customer@example.com" };
|
|
99
|
+
},
|
|
100
|
+
outputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
id: { type: "string" },
|
|
104
|
+
email: { type: "string" },
|
|
105
|
+
},
|
|
106
|
+
required: ["id", "email"],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- `fn` can be sync or async.
|
|
114
|
+
- Local execution passes a single `kwargs` object.
|
|
115
|
+
- Non-object return values are wrapped as `{ result: value }`.
|
|
116
|
+
|
|
117
|
+
### `Skill`
|
|
118
|
+
|
|
119
|
+
Use a `Skill` for reusable instructions and tool access.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export const summarizeSkill = new Skill({
|
|
123
|
+
name: "summarize_skill",
|
|
124
|
+
description: "Reusable summarization instructions.",
|
|
125
|
+
instructions: "Summarize the provided text in one short paragraph.",
|
|
126
|
+
tools: [lookupCustomer],
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`Skill` is a non-executable configuration construct. It is not a local runtime entry point.
|
|
131
|
+
|
|
132
|
+
### `Agent`
|
|
133
|
+
|
|
134
|
+
Use an `Agent` for LLM-backed execution with instructions, tools, skills, and nested agents.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import OpenAI from "openai";
|
|
138
|
+
import { Agent, Runner } from "@zhanla/sdk-ts";
|
|
139
|
+
|
|
140
|
+
const runner = new Runner({
|
|
141
|
+
client: new OpenAI(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const supportAgent = new Agent({
|
|
145
|
+
name: "support_agent",
|
|
146
|
+
description: "Respond to support requests.",
|
|
147
|
+
instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
|
|
148
|
+
model: "gpt-4.1-mini",
|
|
149
|
+
runner,
|
|
150
|
+
tools: [lookupCustomer],
|
|
151
|
+
skills: [summarizeSkill],
|
|
152
|
+
outputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
answer: { type: "string" },
|
|
156
|
+
},
|
|
157
|
+
required: ["answer"],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `LLMProcessor`
|
|
163
|
+
|
|
164
|
+
Use an `LLMProcessor` for prompt-defined transformation steps.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
168
|
+
import { LLMProcessor, Runner } from "@zhanla/sdk-ts";
|
|
169
|
+
|
|
170
|
+
const runner = new Runner({
|
|
171
|
+
client: new Anthropic(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const intentClassifier = new LLMProcessor({
|
|
175
|
+
name: "intent_classifier",
|
|
176
|
+
description: "Classify intent.",
|
|
177
|
+
instructions: 'Return JSON: {"intent": "billing|technical|other"}',
|
|
178
|
+
model: "claude-haiku-4-5",
|
|
179
|
+
runner,
|
|
180
|
+
outputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
intent: { type: "string" },
|
|
184
|
+
},
|
|
185
|
+
required: ["intent"],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `LLMEval`
|
|
191
|
+
|
|
192
|
+
Use an `LLMEval` for LLM-backed evaluation logic.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
export const toneEval = new LLMEval({
|
|
196
|
+
name: "tone_eval",
|
|
197
|
+
description: "Evaluate tone.",
|
|
198
|
+
instructions: 'Return JSON: {"score": 0.0, "reason": "..."}',
|
|
199
|
+
model: "gpt-4.1-mini",
|
|
200
|
+
runner,
|
|
201
|
+
outputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
score: { type: "number" },
|
|
205
|
+
reason: { type: "string" },
|
|
206
|
+
},
|
|
207
|
+
required: ["score", "reason"],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `Orchestration`
|
|
213
|
+
|
|
214
|
+
Use an `Orchestration` to compose steps into a DAG.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
export const supportPipeline = new Orchestration({
|
|
218
|
+
name: "support_pipeline",
|
|
219
|
+
description: "Classify intent, then draft a reply.",
|
|
220
|
+
steps: [
|
|
221
|
+
new Step({ name: "classify", component: intentClassifier, next: ["reply"] }),
|
|
222
|
+
new Step({ name: "reply", component: supportAgent }),
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `Conditional`
|
|
228
|
+
|
|
229
|
+
Use `Conditional` inside an orchestration to branch.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
new Step({
|
|
233
|
+
name: "route",
|
|
234
|
+
component: new Conditional({
|
|
235
|
+
condition: (state) => state.intent === "billing",
|
|
236
|
+
ifTrue: "billing_reply",
|
|
237
|
+
ifFalse: "general_reply",
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### `CodeEval`, `Checklist`, `EvalTree`
|
|
243
|
+
|
|
244
|
+
`CodeEval` runs deterministic TypeScript scoring logic. `Checklist` combines evals with optional weights. `EvalTree` supports score-based branching across eval nodes.
|
|
245
|
+
|
|
246
|
+
## `Runner`
|
|
247
|
+
|
|
248
|
+
`Runner` is the local execution bridge for `Agent`, `LLMProcessor`, and `LLMEval`.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
252
|
+
import { Runner } from "@zhanla/sdk-ts";
|
|
253
|
+
|
|
254
|
+
const runner = new Runner({
|
|
255
|
+
client: new Anthropic(),
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`Runner` behavior:
|
|
260
|
+
|
|
261
|
+
- `constructor({ client })` wraps the client internally with `wrap(...)`
|
|
262
|
+
- `buildMessages(component, row)` defaults to `[system instructions, user JSON row]`
|
|
263
|
+
- `callLlm({ messages, model, tools, outputSchema })` supports Anthropic, OpenAI-compatible chat clients, and Gemini
|
|
264
|
+
|
|
265
|
+
Current local execution behavior:
|
|
266
|
+
|
|
267
|
+
- `runner` is required for `Agent`, `LLMProcessor`, and `LLMEval`
|
|
268
|
+
- `model` must be set explicitly
|
|
269
|
+
- response text is parsed as JSON when possible, otherwise wrapped as `{ result: text }`
|
|
270
|
+
- `outputSchema` is used for validation
|
|
271
|
+
- returned tool calls raise a not-implemented error for now
|
|
272
|
+
|
|
273
|
+
## Observability
|
|
274
|
+
|
|
275
|
+
### `wrap(client)`
|
|
276
|
+
|
|
277
|
+
Wrap an LLM client so calls are recorded against the active trace context.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
281
|
+
import { wrap } from "@zhanla/sdk-ts";
|
|
282
|
+
|
|
283
|
+
const client = wrap(new Anthropic());
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`wrap()` is observational only. It does not change your application logic. `Runner` calls `wrap()` internally, so you do not need to wrap the same client first.
|
|
287
|
+
|
|
288
|
+
### Trace Context
|
|
289
|
+
|
|
290
|
+
The CLI sets a `TraceContext` before runner execution. Wrapped clients read the active context automatically via `AsyncLocalStorage`.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { traceStorage } from "@zhanla/sdk-ts";
|
|
294
|
+
|
|
295
|
+
const ctx = traceStorage.getStore();
|
|
296
|
+
if (ctx) {
|
|
297
|
+
console.log(ctx.traceId);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Discovery And CLI Usage
|
|
302
|
+
|
|
303
|
+
TypeScript discovery loads your module and collects exported component instances.
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
bench run workflow.ts:support_pipeline --dataset tickets.json --eval evals.ts:answer_quality
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The package also ships a helper CLI:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npx @zhanla/sdk-ts discover components.ts
|
|
313
|
+
npx @zhanla/sdk-ts run components.ts:support_agent --eval evals.ts:support_eval
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Validation Rules
|
|
317
|
+
|
|
318
|
+
When run through `zhanla`, TypeScript component manifests are validated before execution.
|
|
319
|
+
|
|
320
|
+
- `Tool` must provide a callable implementation and an `outputSchema`
|
|
321
|
+
- `CodeEval` must provide a callable implementation
|
|
322
|
+
- `Skill`, `Agent`, `LLMProcessor`, and `LLMEval` must provide `instructions`
|
|
323
|
+
- `Agent`, `LLMProcessor`, and `LLMEval` must provide `model`
|
|
324
|
+
|
|
325
|
+
## Utilities
|
|
326
|
+
|
|
327
|
+
The package also exports lower-level helpers:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import {
|
|
331
|
+
collectExportedComponents,
|
|
332
|
+
toManifest,
|
|
333
|
+
executeComponent,
|
|
334
|
+
executeRow,
|
|
335
|
+
wrap,
|
|
336
|
+
parseJsonResponse,
|
|
337
|
+
TraceContext,
|
|
338
|
+
traceStorage,
|
|
339
|
+
} from "@zhanla/sdk-ts";
|
|
340
|
+
import type { LLMCall, LLMResponse, ToolCall } from "@zhanla/sdk-ts";
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- `collectExportedComponents(...)` collects component instances from a loaded module
|
|
344
|
+
- `toManifest(...)` serializes a component into the CLI manifest shape
|
|
345
|
+
- `executeComponent(...)` runs one component locally
|
|
346
|
+
- `executeRow(...)` runs one component plus optional eval for a single row
|
|
347
|
+
- `parseJsonResponse(text)` extracts JSON from raw or fenced model text
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zhanla-sdk-ts CLI entry point.
|
|
4
|
+
* Re-execs itself under the tsx loader so it can import user-authored .ts
|
|
5
|
+
* files directly, then dispatches to discover or run subcommands.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* zhanla-sdk-ts discover <file.ts>
|
|
9
|
+
* zhanla-sdk-ts run <file.ts:name> [--eval <file.ts:name>]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
import { createRequire } from "module";
|
|
14
|
+
|
|
15
|
+
if (process.env.BENCH_SDK_TS_LOADER_ACTIVE !== "1") {
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const loaderPath = require.resolve("tsx");
|
|
18
|
+
const child = spawnSync(
|
|
19
|
+
process.execPath,
|
|
20
|
+
["--import", loaderPath, new URL(import.meta.url).pathname, ...process.argv.slice(2)],
|
|
21
|
+
{
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
env: { ...process.env, BENCH_SDK_TS_LOADER_ACTIVE: "1" },
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
process.exit(child.status ?? 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const [, , subcommand, ...args] = process.argv;
|
|
31
|
+
|
|
32
|
+
if (!subcommand) {
|
|
33
|
+
console.error("Usage: zhanla-sdk-ts <discover|run> <target>");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (subcommand === "discover") {
|
|
37
|
+
const [filePath] = args;
|
|
38
|
+
if (!filePath) {
|
|
39
|
+
console.error("Usage: zhanla-sdk-ts discover <file.ts>");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const { runDiscover } = await import("./discover.js");
|
|
43
|
+
await runDiscover(filePath);
|
|
44
|
+
} else if (subcommand === "run") {
|
|
45
|
+
const [target, ...rest] = args;
|
|
46
|
+
if (!target) {
|
|
47
|
+
console.error("Usage: zhanla-sdk-ts run <file.ts:name>");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
let evalTarget = null;
|
|
51
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
52
|
+
if (rest[i] === "--eval") {
|
|
53
|
+
evalTarget = rest[i + 1] ?? null;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const { runComponent } = await import("./run.js");
|
|
58
|
+
await runComponent(target, evalTarget);
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`Unknown subcommand: ${subcommand}. Use 'discover' or 'run'.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
package/bin/discover.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zhanla-sdk-ts discover <file.ts>
|
|
4
|
+
*
|
|
5
|
+
* Loads a TypeScript module via tsx, collects exported BaseComponent instances,
|
|
6
|
+
* emits their manifests as a JSON array to stdout.
|
|
7
|
+
* Exits non-zero if no components are found or the file fails to load.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
|
|
13
|
+
export async function runDiscover(filePath) {
|
|
14
|
+
const absolutePath = resolve(filePath);
|
|
15
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
16
|
+
|
|
17
|
+
let moduleExports;
|
|
18
|
+
try {
|
|
19
|
+
moduleExports = await import(fileUrl);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(`Failed to load ${filePath}: ${err.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Dynamically import from the built SDK (or tsx will resolve from node_modules)
|
|
26
|
+
let collectExportedComponents, toManifest;
|
|
27
|
+
try {
|
|
28
|
+
const sdk = await import("@zhanla/sdk-ts");
|
|
29
|
+
collectExportedComponents = sdk.collectExportedComponents;
|
|
30
|
+
toManifest = sdk.toManifest;
|
|
31
|
+
} catch {
|
|
32
|
+
// Fallback: try loading from relative path (dev mode)
|
|
33
|
+
const sdk = await import(new URL("../dist/index.js", import.meta.url).href);
|
|
34
|
+
collectExportedComponents = sdk.collectExportedComponents;
|
|
35
|
+
toManifest = sdk.toManifest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const components = collectExportedComponents(moduleExports);
|
|
39
|
+
|
|
40
|
+
if (components.length === 0) {
|
|
41
|
+
console.error(`No zhanla components found in ${filePath}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const symbolByComponent = new Map();
|
|
46
|
+
const namespaces = [moduleExports];
|
|
47
|
+
if (
|
|
48
|
+
moduleExports.default != null &&
|
|
49
|
+
typeof moduleExports.default === "object" &&
|
|
50
|
+
!Array.isArray(moduleExports.default)
|
|
51
|
+
) {
|
|
52
|
+
namespaces.push(moduleExports.default);
|
|
53
|
+
}
|
|
54
|
+
for (const namespace of namespaces) {
|
|
55
|
+
for (const [exportName, value] of Object.entries(namespace)) {
|
|
56
|
+
if (!symbolByComponent.has(value)) {
|
|
57
|
+
symbolByComponent.set(value, exportName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const manifests = components.map((comp) =>
|
|
63
|
+
toManifest(comp, {
|
|
64
|
+
filePath: absolutePath,
|
|
65
|
+
symbolName: symbolByComponent.get(comp) ?? comp.name,
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
process.stdout.write(JSON.stringify(manifests) + "\n");
|
|
70
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const ENTRIES = ["node_modules", "dist", "package-lock.json"];
|
|
6
|
+
|
|
7
|
+
// Walk up from cwd to find the project root (where package.json lives, outside node_modules)
|
|
8
|
+
function findProjectRoot() {
|
|
9
|
+
let dir = process.cwd();
|
|
10
|
+
// When running as a postinstall script the cwd is inside node_modules/<pkg>
|
|
11
|
+
// Walk up until we're outside of node_modules
|
|
12
|
+
while (dir.includes("node_modules")) {
|
|
13
|
+
dir = path.dirname(dir);
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const root = findProjectRoot();
|
|
19
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
20
|
+
|
|
21
|
+
let existing = "";
|
|
22
|
+
try {
|
|
23
|
+
existing = fs.readFileSync(gitignorePath, "utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
// .gitignore doesn't exist yet — we'll create it
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
29
|
+
const toAdd = ENTRIES.filter((e) => !existingLines.has(e));
|
|
30
|
+
|
|
31
|
+
if (toAdd.length === 0) {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const addition = "\n# zhanla-sdk-ts\n" + toAdd.join("\n") + "\n";
|
|
36
|
+
fs.appendFileSync(gitignorePath, addition);
|
|
37
|
+
console.log(`zhanla-sdk-ts: added to .gitignore: ${toAdd.join(", ")}`);
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zhanla-sdk-ts run <file.ts:name> [--eval <file.ts:name>]
|
|
4
|
+
*
|
|
5
|
+
* Loads a TypeScript module, resolves the named component, reads rows as
|
|
6
|
+
* NDJSON from stdin, executes each row, emits results as NDJSON to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Each result line: {"status": "ok", "output": {...}} or {"status": "error", "error": "..."}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pathToFileURL } from "url";
|
|
12
|
+
import { resolve } from "path";
|
|
13
|
+
import readline from "readline";
|
|
14
|
+
|
|
15
|
+
function redirectConsoleToStderr() {
|
|
16
|
+
const stringifyArg = (arg) => {
|
|
17
|
+
if (typeof arg === "string") return arg;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(arg);
|
|
20
|
+
} catch {
|
|
21
|
+
return String(arg);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const write = (...args) => {
|
|
26
|
+
process.stderr.write(`${args.map(stringifyArg).join(" ")}\n`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
console.log = (...args) => write(...args);
|
|
30
|
+
console.info = (...args) => write(...args);
|
|
31
|
+
console.warn = (...args) => write(...args);
|
|
32
|
+
console.error = (...args) => write(...args);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseTarget(target) {
|
|
36
|
+
const colonIdx = target.lastIndexOf(":");
|
|
37
|
+
if (colonIdx === -1) {
|
|
38
|
+
console.error(`Invalid target format. Expected <file.ts:name>, got: ${target}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filePath = target.slice(0, colonIdx);
|
|
43
|
+
const symbolName = target.slice(colonIdx + 1);
|
|
44
|
+
|
|
45
|
+
if (!symbolName) {
|
|
46
|
+
console.error(`Empty component name in target: ${target}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { filePath, symbolName };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function loadTargetComponent(target, collectExportedComponents) {
|
|
54
|
+
const { filePath, symbolName } = parseTarget(target);
|
|
55
|
+
|
|
56
|
+
const absolutePath = resolve(filePath);
|
|
57
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
58
|
+
|
|
59
|
+
let moduleExports;
|
|
60
|
+
try {
|
|
61
|
+
moduleExports = await import(fileUrl);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`Failed to load ${filePath}: ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const components = collectExportedComponents(moduleExports);
|
|
68
|
+
const component = components.find((c) => c.name === symbolName);
|
|
69
|
+
|
|
70
|
+
if (!component) {
|
|
71
|
+
const available = components.map((c) => c.name).join(", ");
|
|
72
|
+
console.error(`Component '${symbolName}' not found. Available: ${available}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return component;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runComponent(target, evalTarget = null) {
|
|
80
|
+
redirectConsoleToStderr();
|
|
81
|
+
// Dynamically import from SDK
|
|
82
|
+
let collectExportedComponents, executeRow;
|
|
83
|
+
try {
|
|
84
|
+
const sdk = await import("@zhanla/sdk-ts");
|
|
85
|
+
collectExportedComponents = sdk.collectExportedComponents;
|
|
86
|
+
executeRow = sdk.executeRow;
|
|
87
|
+
} catch {
|
|
88
|
+
const sdk = await import(new URL("../dist/index.js", import.meta.url).href);
|
|
89
|
+
collectExportedComponents = sdk.collectExportedComponents;
|
|
90
|
+
executeRow = sdk.executeRow;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Read rows from stdin as NDJSON, emit results to stdout
|
|
94
|
+
const component = await loadTargetComponent(target, collectExportedComponents);
|
|
95
|
+
const evalComponent = evalTarget
|
|
96
|
+
? await loadTargetComponent(evalTarget, collectExportedComponents)
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
crlfDelay: Infinity,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
for await (const line of rl) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed) continue;
|
|
107
|
+
|
|
108
|
+
let row;
|
|
109
|
+
try {
|
|
110
|
+
row = JSON.parse(trimmed);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const result = { status: "error", error: `Invalid JSON row: ${err.message}` };
|
|
113
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await executeRow(component, evalComponent, row);
|
|
119
|
+
const output =
|
|
120
|
+
result.output && typeof result.output === "object" ? { ...result.output } : result.output;
|
|
121
|
+
let evalOutput;
|
|
122
|
+
if (output && typeof output === "object" && "_eval" in output) {
|
|
123
|
+
evalOutput = output._eval;
|
|
124
|
+
delete output._eval;
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
status: result.status,
|
|
129
|
+
output,
|
|
130
|
+
pathTaken: result.pathTaken,
|
|
131
|
+
eval_output: evalOutput,
|
|
132
|
+
error: result.status === "error" ? result.error : undefined,
|
|
133
|
+
eval_error: result.status === "ok" ? result.error : undefined,
|
|
134
|
+
}) + "\n"
|
|
135
|
+
);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const result = {
|
|
138
|
+
status: "error",
|
|
139
|
+
error: err instanceof Error ? err.message : String(err),
|
|
140
|
+
};
|
|
141
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local execution engine for TypeScript SDK components.
|
|
3
|
+
* Mirrors the Python CLI executor's component-type dispatch semantics.
|
|
4
|
+
*/
|
|
5
|
+
import { BaseComponent } from "./types.js";
|
|
6
|
+
export interface RowResult {
|
|
7
|
+
status: "ok" | "error";
|
|
8
|
+
output?: Record<string, unknown>;
|
|
9
|
+
error?: string;
|
|
10
|
+
pathTaken?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare function executeComponent(comp: BaseComponent, kwargs: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
13
|
+
export declare function executeRow(component: BaseComponent, evalComponent: BaseComponent | null, row: Record<string, unknown>): Promise<RowResult>;
|