toolcraft 0.0.103 → 0.0.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/composition.json +7 -2
  2. package/dist/cli.js +53 -0
  3. package/dist/composition.json +7 -2
  4. package/dist/index.d.ts +25 -3
  5. package/dist/index.js +18 -0
  6. package/dist/mcp.d.ts +6 -0
  7. package/dist/mcp.js +121 -1
  8. package/dist/sdk.d.ts +2 -1
  9. package/dist/sdk.js +56 -0
  10. package/dist/stream.d.ts +19 -0
  11. package/dist/stream.js +92 -0
  12. package/dist/testing/harness.d.ts +11 -0
  13. package/dist/testing/harness.js +70 -0
  14. package/dist/testing/index.d.ts +1 -1
  15. package/node_modules/tiny-stdio-mcp-server/LICENSE +21 -0
  16. package/node_modules/tiny-stdio-mcp-server/README.md +231 -0
  17. package/node_modules/tiny-stdio-mcp-server/dist/content/audio.d.ts +15 -0
  18. package/node_modules/tiny-stdio-mcp-server/dist/content/audio.js +84 -0
  19. package/node_modules/tiny-stdio-mcp-server/dist/content/convert.d.ts +16 -0
  20. package/node_modules/tiny-stdio-mcp-server/dist/content/convert.js +61 -0
  21. package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.d.ts +11 -0
  22. package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.js +93 -0
  23. package/node_modules/tiny-stdio-mcp-server/dist/content/file.d.ts +28 -0
  24. package/node_modules/tiny-stdio-mcp-server/dist/content/file.js +110 -0
  25. package/node_modules/tiny-stdio-mcp-server/dist/content/image.d.ts +15 -0
  26. package/node_modules/tiny-stdio-mcp-server/dist/content/image.js +72 -0
  27. package/node_modules/tiny-stdio-mcp-server/dist/content/index.d.ts +7 -0
  28. package/node_modules/tiny-stdio-mcp-server/dist/content/index.js +9 -0
  29. package/node_modules/tiny-stdio-mcp-server/dist/content/mime.d.ts +7 -0
  30. package/node_modules/tiny-stdio-mcp-server/dist/content/mime.js +52 -0
  31. package/node_modules/tiny-stdio-mcp-server/dist/content/remote.d.ts +5 -0
  32. package/node_modules/tiny-stdio-mcp-server/dist/content/remote.js +69 -0
  33. package/node_modules/tiny-stdio-mcp-server/dist/index.d.ts +9 -0
  34. package/node_modules/tiny-stdio-mcp-server/dist/index.js +7 -0
  35. package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.d.ts +14 -0
  36. package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.js +118 -0
  37. package/node_modules/tiny-stdio-mcp-server/dist/schema.d.ts +20 -0
  38. package/node_modules/tiny-stdio-mcp-server/dist/schema.js +26 -0
  39. package/node_modules/tiny-stdio-mcp-server/dist/server.d.ts +35 -0
  40. package/node_modules/tiny-stdio-mcp-server/dist/server.js +865 -0
  41. package/node_modules/tiny-stdio-mcp-server/dist/testing.d.ts +7 -0
  42. package/node_modules/tiny-stdio-mcp-server/dist/testing.js +20 -0
  43. package/node_modules/tiny-stdio-mcp-server/dist/types.d.ts +247 -0
  44. package/node_modules/tiny-stdio-mcp-server/dist/types.js +22 -0
  45. package/node_modules/tiny-stdio-mcp-server/package.json +56 -0
  46. package/node_modules/toolcraft-schema/package.json +1 -1
  47. package/package.json +9 -5
package/composition.json CHANGED
@@ -46,9 +46,14 @@
46
46
  "version": "0.1.0",
47
47
  "license": "MIT"
48
48
  },
49
+ {
50
+ "name": "tiny-stdio-mcp-server",
51
+ "version": "0.1.0",
52
+ "license": "MIT"
53
+ },
49
54
  {
50
55
  "name": "toolcraft",
51
- "version": "0.0.103",
56
+ "version": "0.0.105",
52
57
  "license": "MIT"
53
58
  },
54
59
  {
@@ -58,7 +63,7 @@
58
63
  },
59
64
  {
60
65
  "name": "toolcraft-schema",
61
- "version": "0.0.103",
66
+ "version": "0.0.105",
62
67
  "license": "MIT"
63
68
  }
64
69
  ]
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { enableSourceMaps, formatDebugStack } from "./stack-trim.js";
18
18
  import { suggest } from "./suggest.js";
19
19
  import { throwValidationErrors } from "./validation-errors.js";
20
20
  import { RESERVED_SERVICE_NAMES, createEnv, createFs, validateServices } from "./runtime/io.js";
21
+ import { createManagedStream } from "./stream.js";
21
22
  export { renderErrorReport } from "./error-report.js";
22
23
  configureTheme({ brand: "blue", label: "Toolcraft" });
23
24
  export { configureTheme };
@@ -3050,6 +3051,58 @@ async function executeCommand(state, services, requirementOptions, runtimeFetch,
3050
3051
  ...preflightContext,
3051
3052
  params
3052
3053
  };
3054
+ if (state.command.stream !== undefined) {
3055
+ const stream = createManagedStream({
3056
+ eventSchema: state.command.stream.event,
3057
+ onStatus(event) {
3058
+ diagnostics.emit({
3059
+ level: "info",
3060
+ message: event.message ?? event.type,
3061
+ category: "runtime"
3062
+ });
3063
+ if (output === "rich" && event.message !== undefined) {
3064
+ logger.info(event.message);
3065
+ }
3066
+ },
3067
+ async create(signal, status) {
3068
+ return await state.command.handler({
3069
+ ...context,
3070
+ signal,
3071
+ status,
3072
+ async refreshSecrets() {
3073
+ return resolveCommandSecrets(state.command, runtimeEnv);
3074
+ }
3075
+ });
3076
+ }
3077
+ });
3078
+ const interrupt = () => {
3079
+ void stream.cancel(new UserError("Operation cancelled."));
3080
+ };
3081
+ process.once("SIGINT", interrupt);
3082
+ try {
3083
+ for await (const event of stream) {
3084
+ if (output === "json") {
3085
+ const line = JSON.stringify(event);
3086
+ if (outputEmitter === undefined) {
3087
+ process.stdout.write(`${line}\n`);
3088
+ }
3089
+ else {
3090
+ outputEmitter(line);
3091
+ }
3092
+ }
3093
+ else {
3094
+ renderResult(state.command, event, output, primitives, outputEmitter === undefined
3095
+ ? undefined
3096
+ : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk));
3097
+ }
3098
+ }
3099
+ }
3100
+ finally {
3101
+ process.removeListener("SIGINT", interrupt);
3102
+ await stream.cancel();
3103
+ }
3104
+ return;
3105
+ }
3053
3106
  if (state.command.confirm &&
3054
3107
  !state.command.humanInLoop &&
3055
3108
  !resolvedFlags.yes &&
@@ -46,9 +46,14 @@
46
46
  "version": "0.1.0",
47
47
  "license": "MIT"
48
48
  },
49
+ {
50
+ "name": "tiny-stdio-mcp-server",
51
+ "version": "0.1.0",
52
+ "license": "MIT"
53
+ },
49
54
  {
50
55
  "name": "toolcraft",
51
- "version": "0.0.103",
56
+ "version": "0.0.105",
52
57
  "license": "MIT"
53
58
  },
54
59
  {
@@ -58,7 +63,7 @@
58
63
  },
59
64
  {
60
65
  "name": "toolcraft-schema",
61
- "version": "0.0.103",
66
+ "version": "0.0.105",
62
67
  "license": "MIT"
63
68
  }
64
69
  ]
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { McpServerConfig } from "@poe-code/agent-mcp-config";
2
- import type { ObjectSchema, Static } from "toolcraft-schema";
2
+ import type { AnySchema, ObjectSchema, Static } from "toolcraft-schema";
3
3
  import type { LoggerOutput, RenderTableOptions, ThemePalette } from "toolcraft-design";
4
4
  import { ApprovalDeclinedError } from "./human-in-loop/types.js";
5
5
  import type { HumanInLoopConfig, HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/types.js";
6
6
  import { ToolcraftBugError, UserError } from "./user-error.js";
7
7
  import type { RuntimeLogger } from "./runtime-logging.js";
8
+ import type { StreamStatusEvent, ToolcraftStream } from "./stream.js";
9
+ export { createManagedStream } from "./stream.js";
10
+ export type { StreamConsumerOptions, StreamStatusEvent, StreamStatusType, ToolcraftStream } from "./stream.js";
8
11
  type ScopeValue = "cli" | "mcp" | "sdk";
9
12
  type AnyObjectSchema = ObjectSchema<Record<string, never>>;
10
13
  type EmptyServices = Record<string, never>;
@@ -114,9 +117,23 @@ export interface CommandConfig<TServices extends object, TParamsSchema extends O
114
117
  confirm?: boolean;
115
118
  humanInLoop?: HumanInLoopConfig<TParamsSchema> | null;
116
119
  requires?: Requires<CommandCheckContext<TParamsSchema, TSecrets, TServices>>;
117
- handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult>;
120
+ handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult> | TResult;
118
121
  render?: Renderers<TResult>;
119
122
  }
123
+ export interface StreamDefinition<TEventSchema extends AnySchema = AnySchema> {
124
+ event: TEventSchema;
125
+ bufferSize: number;
126
+ }
127
+ export type StreamHandlerContext<TParamsSchema extends ObjectSchema<any>, TSecrets extends SecretDeclarations | undefined, TServices extends object> = HandlerContext<TParamsSchema, TSecrets, TServices> & {
128
+ signal: AbortSignal;
129
+ status(event: StreamStatusEvent): void;
130
+ refreshSecrets(): Promise<InferSecrets<TSecrets>>;
131
+ };
132
+ export interface StreamCommandConfig<TServices extends object, TParamsSchema extends ObjectSchema<any>, TSecrets extends SecretDeclarations | undefined, TEventSchema extends AnySchema> extends Omit<CommandConfig<TServices, TParamsSchema, TSecrets, AsyncIterable<Static<TEventSchema>>>, "handler" | "render" | "result" | "humanInLoop" | "confirm"> {
133
+ event: TEventSchema;
134
+ handler: (ctx: StreamHandlerContext<TParamsSchema, TSecrets, TServices>) => AsyncIterable<Static<TEventSchema>> | Promise<AsyncIterable<Static<TEventSchema>>>;
135
+ render?: Renderers<Static<TEventSchema>>;
136
+ }
120
137
  export interface Command<TServices extends object = EmptyServices, TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TResult = unknown> {
121
138
  kind: "command";
122
139
  name: string;
@@ -127,12 +144,13 @@ export interface Command<TServices extends object = EmptyServices, TParamsSchema
127
144
  positional: string[];
128
145
  params: TParamsSchema;
129
146
  result?: ObjectSchema<any>;
147
+ stream?: StreamDefinition<any>;
130
148
  secrets: SecretDeclarations;
131
149
  scope: Scope[];
132
150
  confirm: boolean;
133
151
  humanInLoop?: HumanInLoopConfig<TParamsSchema> | null;
134
152
  requires?: Requires<any>;
135
- handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult>;
153
+ handler: (ctx: HandlerContext<TParamsSchema, TSecrets, TServices>) => Promise<TResult> | TResult;
136
154
  render?: Renderers<TResult>;
137
155
  }
138
156
  export interface GroupConfig<TServices extends object> {
@@ -193,6 +211,10 @@ export declare function defineCommand<TServices extends object = EmptyServices,
193
211
  scope?: TOwnScope;
194
212
  humanInLoop?: TOwnHumanInLoop;
195
213
  }): Command<TServices, TParamsSchema, TSecrets, TResult> & TypedCommandMetadata<TName, TParamsSchema, TResult, TOwnScope, ResolveOwnHumanInLoopMode<TOwnHumanInLoop>>;
214
+ export declare function defineStreamCommand<TServices extends object = EmptyServices, TName extends string = string, TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TEventSchema extends AnySchema = AnySchema, TOwnScope extends ScopeInput = undefined>(config: Omit<StreamCommandConfig<TServices, TParamsSchema, TSecrets, TEventSchema>, "name" | "scope"> & {
215
+ name: TName;
216
+ scope?: TOwnScope;
217
+ }): Command<TServices, TParamsSchema, TSecrets, AsyncIterable<Static<TEventSchema>>> & TypedCommandMetadata<TName, TParamsSchema, ToolcraftStream<Static<TEventSchema>>, TOwnScope, undefined>;
196
218
  export declare function defineGroup<TServices extends object = EmptyServices, TName extends string = string, TChildren extends readonly unknown[] = readonly CommandNode<TServices>[], TOwnScope extends ScopeInput = undefined, TOwnHumanInLoop extends HumanInLoopConfig<AnyObjectSchema> | null | undefined = undefined>(config: Omit<GroupConfig<TServices>, "name" | "children" | "scope" | "humanInLoop"> & {
197
219
  name: TName;
198
220
  children: TChildren & readonly CommandNode<TServices>[];
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { ApprovalDeclinedError } from "./human-in-loop/types.js";
3
3
  import { mergeHumanInLoopFromGroup, validateHumanInLoopOnDefine } from "./human-in-loop/config.js";
4
4
  import { ToolcraftBugError, UserError } from "./user-error.js";
5
5
  import { suggest } from "./suggest.js";
6
+ export { createManagedStream } from "./stream.js";
6
7
  const commandConfigSymbol = Symbol("toolcraft.command.config");
7
8
  const groupConfigSymbol = Symbol("toolcraft.group.config");
8
9
  const commandSourcePathSymbol = Symbol("toolcraft.command.sourcePath");
@@ -297,6 +298,7 @@ function createBaseCommand(config) {
297
298
  positional: [...(config.positional ?? [])],
298
299
  params: config.params,
299
300
  result: config.result,
301
+ stream: undefined,
300
302
  secrets: cloneSecrets(config.secrets),
301
303
  scope: resolveCommandScope(config.scope, undefined),
302
304
  confirm: config.confirm ?? false,
@@ -365,6 +367,7 @@ function materializeCommand(command, inherited) {
365
367
  positional: [...command.positional],
366
368
  params: command.params,
367
369
  result: internal.result,
370
+ stream: command.stream,
368
371
  secrets: mergeSecrets(inherited.secrets, internal.secrets),
369
372
  scope: resolveCommandScope(internal.scope, inherited.scope),
370
373
  confirm: command.confirm,
@@ -456,6 +459,21 @@ export function defineCommand(config) {
456
459
  requires: undefined
457
460
  });
458
461
  }
462
+ export function defineStreamCommand(config) {
463
+ const command = defineCommand({
464
+ ...config,
465
+ confirm: false,
466
+ humanInLoop: null,
467
+ handler: config.handler,
468
+ render: undefined
469
+ });
470
+ command.stream = {
471
+ event: config.event,
472
+ bufferSize: 1
473
+ };
474
+ command.render = config.render;
475
+ return command;
476
+ }
459
477
  export function defineGroup(config) {
460
478
  validateRenameMap(config.rename);
461
479
  validateHumanInLoopOnDefine(config);
package/dist/mcp.d.ts CHANGED
@@ -6,6 +6,12 @@ type Casing = "snake" | "camel";
6
6
  type CmdkitServer = Omit<TinyServer, "connect"> & {
7
7
  connect(transport: SDKTransport): Promise<void>;
8
8
  };
9
+ export declare const MCP_STREAM_METHODS: {
10
+ readonly list: "toolcraft/streams/list";
11
+ readonly subscribe: "toolcraft/streams/subscribe";
12
+ readonly unsubscribe: "toolcraft/streams/unsubscribe";
13
+ readonly notification: "notifications/toolcraft/stream";
14
+ };
9
15
  export interface RunMCPOptions<TServices extends object = Record<string, unknown>> {
10
16
  apiVersion?: string;
11
17
  approvals?: boolean;
package/dist/mcp.js CHANGED
@@ -14,6 +14,13 @@ import { suggest } from "./suggest.js";
14
14
  import { throwValidationErrors } from "./validation-errors.js";
15
15
  import { createRuntimeLogger } from "./runtime-logging.js";
16
16
  import { createEnv, createFs, validateServices } from "./runtime/io.js";
17
+ import { createManagedStream } from "./stream.js";
18
+ export const MCP_STREAM_METHODS = {
19
+ list: "toolcraft/streams/list",
20
+ subscribe: "toolcraft/streams/subscribe",
21
+ unsubscribe: "toolcraft/streams/unsubscribe",
22
+ notification: "notifications/toolcraft/stream"
23
+ };
17
24
  function normalizeRoots(roots) {
18
25
  if (!Array.isArray(roots)) {
19
26
  return roots;
@@ -728,7 +735,120 @@ function createResolvedMCPServer(root, options) {
728
735
  version,
729
736
  validateToolArguments: false
730
737
  });
731
- for (const tool of tools) {
738
+ const streamTools = tools.filter((tool) => tool.command.stream !== undefined);
739
+ const subscriptions = new Map();
740
+ let nextSubscriptionId = 0;
741
+ server.method(MCP_STREAM_METHODS.list, () => ({
742
+ streams: streamTools.map((tool) => ({
743
+ name: tool.name,
744
+ description: tool.description,
745
+ inputSchema: tool.inputSchema,
746
+ eventSchema: applySchemaCasing(toJsonSchema(tool.command.stream.event), casing),
747
+ bufferSize: tool.command.stream.bufferSize
748
+ }))
749
+ }));
750
+ server.method(MCP_STREAM_METHODS.subscribe, async (request, session) => {
751
+ const name = typeof request?.name === "string" ? request.name : undefined;
752
+ const tool = streamTools.find((candidate) => candidate.name === name);
753
+ if (tool === undefined) {
754
+ throw new UserError(`Stream not found: ${name ?? ""}`);
755
+ }
756
+ const streamDefinition = tool.command.stream;
757
+ if (streamDefinition === undefined) {
758
+ throw new ToolcraftBugError(`Command "${tool.commandPath}" is not a stream.`);
759
+ }
760
+ const argumentsValue = isPlainObject(request?.arguments) ? request.arguments : {};
761
+ const secrets = resolveCommandSecrets(tool.command, options.env);
762
+ const baseContext = {
763
+ ...servicesWithBuiltIns,
764
+ secrets,
765
+ fetch: runtimeFetch,
766
+ fs: createFs(options.fs),
767
+ env: createEnv(options.env),
768
+ diagnostics,
769
+ progress(message) {
770
+ diagnostics.emit({ level: "info", message, category: "progress" });
771
+ }
772
+ };
773
+ await assertCommandRequirements(tool.command, { ...baseContext, params: undefined }, { apiVersion: options.apiVersion, env: options.env });
774
+ const params = validateToolArguments(tool.command.params, argumentsValue, casing);
775
+ const subscriptionId = `stream-${++nextSubscriptionId}`;
776
+ let notificationQueue = Promise.resolve();
777
+ const notify = (params) => {
778
+ notificationQueue = notificationQueue.then(() => session.notify(MCP_STREAM_METHODS.notification, params));
779
+ return notificationQueue;
780
+ };
781
+ const stream = createManagedStream({
782
+ eventSchema: streamDefinition.event,
783
+ signal: session.signal,
784
+ onStatus(event) {
785
+ void notify({
786
+ subscriptionId,
787
+ type: "status",
788
+ status: event
789
+ });
790
+ },
791
+ async create(signal, status) {
792
+ return await tool.command.handler({
793
+ ...baseContext,
794
+ params,
795
+ signal,
796
+ status,
797
+ async refreshSecrets() {
798
+ return resolveCommandSecrets(tool.command, options.env);
799
+ }
800
+ });
801
+ }
802
+ });
803
+ subscriptions.set(subscriptionId, { signal: session.signal, stream });
804
+ void (async () => {
805
+ try {
806
+ for await (const event of stream) {
807
+ await notify({
808
+ subscriptionId,
809
+ type: "data",
810
+ event
811
+ });
812
+ }
813
+ if (!session.signal.aborted) {
814
+ await notify({
815
+ subscriptionId,
816
+ type: "end"
817
+ });
818
+ }
819
+ }
820
+ catch (error) {
821
+ if (!session.signal.aborted) {
822
+ await notify({
823
+ subscriptionId,
824
+ type: "error",
825
+ error: error instanceof Error ? error.message : String(error)
826
+ });
827
+ }
828
+ }
829
+ finally {
830
+ subscriptions.delete(subscriptionId);
831
+ await stream.cancel();
832
+ }
833
+ })();
834
+ return {
835
+ subscriptionId,
836
+ eventSchema: applySchemaCasing(toJsonSchema(streamDefinition.event), casing)
837
+ };
838
+ });
839
+ server.method(MCP_STREAM_METHODS.unsubscribe, async (request, session) => {
840
+ const subscriptionId = typeof request?.subscriptionId === "string" ? request.subscriptionId : undefined;
841
+ const subscription = subscriptionId === undefined ? undefined : subscriptions.get(subscriptionId);
842
+ if (subscriptionId === undefined ||
843
+ subscription === undefined ||
844
+ subscription.signal !== session.signal) {
845
+ return { unsubscribed: false };
846
+ }
847
+ await subscription.stream.cancel();
848
+ subscriptions.delete(subscriptionId);
849
+ return { unsubscribed: true };
850
+ });
851
+ for (const tool of tools.filter((candidate) => candidate.command.stream === undefined)) {
732
852
  const handler = async (argumentsValue) => {
733
853
  let params;
734
854
  let secrets;
package/dist/sdk.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { Group, HandlerFs, LogLevel, RuntimeLoggerInput, Scope } from "./in
3
3
  import { type ErrorReportsOption } from "./error-report.js";
4
4
  import type { HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
5
5
  import { type ValidationError } from "./validation-errors.js";
6
+ import type { StreamConsumerOptions, ToolcraftStream } from "./stream.js";
6
7
  type ScopeInput = readonly Scope[] | undefined;
7
8
  type HumanInLoopMode = "sync" | "async";
8
9
  type HumanInLoopModeInput = HumanInLoopMode | null | undefined;
@@ -32,7 +33,7 @@ type Camelize<TValue> = TValue extends Primitive ? TValue : TValue extends reado
32
33
  [TKey in keyof TValue as TKey extends string ? CamelCase<TKey> : TKey]: Camelize<TValue[TKey]>;
33
34
  } : TValue;
34
35
  type SDKResult<TResult, THumanInLoopMode extends HumanInLoopMode | undefined> = [THumanInLoopMode] extends ["async"] ? ["async"] extends [THumanInLoopMode] ? HumanInLoopPending : TResult : TResult;
35
- type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
36
+ type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = TResult extends ToolcraftStream<infer TEvent> ? (params: Camelize<Static<TParamsSchema>>, options?: StreamConsumerOptions) => ToolcraftStream<TEvent> : (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
36
37
  type UnionToIntersection<TValue> = (TValue extends unknown ? (value: TValue) => void : never) extends (value: infer TResult) => void ? TResult : never;
37
38
  type Simplify<TValue> = {
38
39
  [TKey in keyof TValue]: TValue[TKey];
package/dist/sdk.js CHANGED
@@ -10,6 +10,7 @@ import { suggest } from "./suggest.js";
10
10
  import { throwValidationErrors } from "./validation-errors.js";
11
11
  import { createRuntimeLogger } from "./runtime-logging.js";
12
12
  import { createEnv, createFs, validateServices } from "./runtime/io.js";
13
+ import { createManagedStream } from "./stream.js";
13
14
  function splitWords(value) {
14
15
  const words = [];
15
16
  let current = "";
@@ -330,6 +331,61 @@ function createResolvedSDK(root, options = {}) {
330
331
  if (sdkParamsSchema?.kind === "object") {
331
332
  validateUniqueSDKParameterMembers(sdkParamsSchema);
332
333
  }
334
+ if (node.stream !== undefined) {
335
+ return (params, streamOptions = {}) => createManagedStream({
336
+ eventSchema: node.stream.event,
337
+ ...streamOptions,
338
+ async create(signal, status) {
339
+ const commandPath = [...path, node.name].join(".");
340
+ let secrets;
341
+ let validatedParams;
342
+ try {
343
+ secrets = resolveCommandSecrets(node, options.env);
344
+ const baseContext = {
345
+ ...services,
346
+ runtimeOptions,
347
+ root,
348
+ secrets,
349
+ fetch: runtimeFetch,
350
+ fs: createFs(options.fs),
351
+ env: createEnv(options.env),
352
+ diagnostics,
353
+ progress(message) {
354
+ diagnostics.emit({ level: "info", message, category: "progress" });
355
+ }
356
+ };
357
+ await assertCommandRequirements(node, { ...baseContext, params: undefined }, { apiVersion: options.apiVersion, env: options.env });
358
+ const paramsSchema = filterSchemaForScope(node.params, "sdk");
359
+ if (paramsSchema === undefined || paramsSchema.kind !== "object") {
360
+ throw new ToolcraftBugError(`command "${node.name}" must define an object params schema for SDK.`);
361
+ }
362
+ validatedParams = validateSDKArguments(paramsSchema, params);
363
+ return await node.handler({
364
+ ...baseContext,
365
+ params: validatedParams,
366
+ signal,
367
+ status,
368
+ async refreshSecrets() {
369
+ return resolveCommandSecrets(node, options.env);
370
+ }
371
+ });
372
+ }
373
+ catch (error) {
374
+ await writeErrorReport({
375
+ command: node,
376
+ commandPath,
377
+ env: process.env,
378
+ error,
379
+ errorReports: options.errorReports,
380
+ params: validatedParams,
381
+ projectRoot: options.projectRoot,
382
+ secrets
383
+ });
384
+ throw error;
385
+ }
386
+ }
387
+ });
388
+ }
333
389
  return async (params) => {
334
390
  const commandPath = [...path, node.name].join(".");
335
391
  let secrets;
@@ -0,0 +1,19 @@
1
+ import { type AnySchema, type Static } from "toolcraft-schema";
2
+ export type StreamStatusType = "connected" | "progress" | "reconnecting";
3
+ export interface StreamStatusEvent {
4
+ type: StreamStatusType;
5
+ message?: string;
6
+ }
7
+ export interface StreamConsumerOptions {
8
+ signal?: AbortSignal;
9
+ onStatus?: (event: StreamStatusEvent) => void;
10
+ }
11
+ export interface ToolcraftStream<TEvent> extends AsyncIterable<TEvent> {
12
+ readonly signal: AbortSignal;
13
+ cancel(reason?: unknown): Promise<void>;
14
+ }
15
+ export interface ManagedStreamOptions<TSchema extends AnySchema> extends StreamConsumerOptions {
16
+ eventSchema: TSchema;
17
+ create(signal: AbortSignal, status: (event: StreamStatusEvent) => void): Promise<AsyncIterable<Static<TSchema>>>;
18
+ }
19
+ export declare function createManagedStream<TSchema extends AnySchema>(options: ManagedStreamOptions<TSchema>): ToolcraftStream<Static<TSchema>>;
package/dist/stream.js ADDED
@@ -0,0 +1,92 @@
1
+ import { validate } from "toolcraft-schema";
2
+ import { UserError } from "./user-error.js";
3
+ function validationMessage(schema, value) {
4
+ const result = validate(schema, value);
5
+ return result.ok ? undefined : result.issues.map((issue) => issue.message).join("; ");
6
+ }
7
+ export function createManagedStream(options) {
8
+ const controller = new AbortController();
9
+ let iterator;
10
+ let iteratorPromise;
11
+ let closePromise;
12
+ let done = false;
13
+ const getIterator = async () => {
14
+ if (iterator !== undefined) {
15
+ return iterator;
16
+ }
17
+ iteratorPromise ??= options
18
+ .create(controller.signal, (event) => options.onStatus?.(event))
19
+ .then((iterable) => iterable[Symbol.asyncIterator]());
20
+ iterator = await iteratorPromise;
21
+ return iterator;
22
+ };
23
+ const close = async (reason) => {
24
+ if (closePromise !== undefined) {
25
+ return closePromise;
26
+ }
27
+ closePromise = (async () => {
28
+ controller.abort(reason);
29
+ options.signal?.removeEventListener("abort", abortFromConsumer);
30
+ if (done || (iterator === undefined && iteratorPromise === undefined)) {
31
+ done = true;
32
+ return;
33
+ }
34
+ const activeIterator = await getIterator();
35
+ done = true;
36
+ await activeIterator.return?.();
37
+ })();
38
+ return closePromise;
39
+ };
40
+ const abortFromConsumer = () => {
41
+ void close(options.signal?.reason);
42
+ };
43
+ if (options.signal?.aborted === true) {
44
+ abortFromConsumer();
45
+ }
46
+ else {
47
+ options.signal?.addEventListener("abort", abortFromConsumer, { once: true });
48
+ }
49
+ const stream = {
50
+ signal: controller.signal,
51
+ cancel: close,
52
+ [Symbol.asyncIterator]() {
53
+ return {
54
+ async next() {
55
+ if (done || controller.signal.aborted) {
56
+ await close(controller.signal.reason);
57
+ return { done: true, value: undefined };
58
+ }
59
+ const activeIterator = await getIterator();
60
+ let result;
61
+ try {
62
+ result = await activeIterator.next();
63
+ }
64
+ catch (error) {
65
+ await close(error);
66
+ throw error;
67
+ }
68
+ if (result.done === true) {
69
+ done = true;
70
+ options.signal?.removeEventListener("abort", abortFromConsumer);
71
+ return { done: true, value: undefined };
72
+ }
73
+ const message = validationMessage(options.eventSchema, result.value);
74
+ if (message !== undefined) {
75
+ await close(new UserError(message));
76
+ throw new UserError(message);
77
+ }
78
+ return { done: false, value: result.value };
79
+ },
80
+ async return() {
81
+ await close();
82
+ return { done: true, value: undefined };
83
+ },
84
+ async throw(error) {
85
+ await close(error);
86
+ throw error;
87
+ }
88
+ };
89
+ }
90
+ };
91
+ return stream;
92
+ }
@@ -3,6 +3,7 @@ import { type DiagnosticLogEvent, type LogLevel } from "../runtime-logging.js";
3
3
  import { type FetchRoute } from "./fakes.js";
4
4
  import { type FsChange, type MemoryFs } from "./memory-fs.js";
5
5
  import { type ParityResult } from "./parity.js";
6
+ import { type StreamStatusEvent } from "../stream.js";
6
7
  export type PipelineStage = "resolve" | "secrets" | "requirements" | "params" | "confirm" | "handler" | "render";
7
8
  export type EffectEvent = {
8
9
  seq: number;
@@ -67,10 +68,20 @@ export interface RunResult<T> {
67
68
  }
68
69
  export interface CommandTestHarness {
69
70
  run<T>(path: string[], params?: Record<string, unknown>): Promise<RunResult<T>>;
71
+ stream<T>(path: string[], params?: Record<string, unknown>, options?: {
72
+ limit?: number;
73
+ signal?: AbortSignal;
74
+ }): Promise<StreamRunResult<T>>;
70
75
  parity(path: string[], params?: Record<string, unknown>): Promise<ParityResult>;
71
76
  fs: MemoryFs;
72
77
  timeline: EffectEvent[];
73
78
  }
79
+ export interface StreamRunResult<T> {
80
+ ok: boolean;
81
+ events: T[];
82
+ statuses: StreamStatusEvent[];
83
+ error?: unknown;
84
+ }
74
85
  type EmptyHarnessServices = Record<string, never>;
75
86
  export declare function createCommandTestHarness<TServices extends object = EmptyHarnessServices>(root: Group, options?: HarnessOptions<TServices>): CommandTestHarness;
76
87
  export {};