@sundaeswap/sprinkles 0.6.1 → 0.8.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 +178 -181
- package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
- package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
- package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
- package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +744 -0
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +711 -0
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
- package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
- package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
- package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
- package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
- package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
- package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/index.js +174 -0
- package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
- package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
- package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
- package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
- package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
- package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
- package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/cli-adapter.js +390 -0
- package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/index.js +139 -0
- package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js +557 -0
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/registry.js +92 -0
- package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/runner.js +190 -0
- package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
- package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/types.js +68 -0
- package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
- package/dist/cjs/Sprinkle/index.js +678 -5
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/prompts.js +12 -7
- package/dist/cjs/Sprinkle/prompts.js.map +1 -1
- package/dist/cjs/Sprinkle/schemas.js +17 -1
- package/dist/cjs/Sprinkle/schemas.js.map +1 -1
- package/dist/cjs/Sprinkle/type-guards.js +7 -1
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
- package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
- package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
- package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +742 -0
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +710 -0
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
- package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
- package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
- package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
- package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
- package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
- package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/index.js +37 -0
- package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
- package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
- package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
- package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
- package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
- package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
- package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/cli-adapter.js +379 -0
- package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
- package/dist/esm/Sprinkle/actions/index.js +12 -0
- package/dist/esm/Sprinkle/actions/index.js.map +1 -0
- package/dist/esm/Sprinkle/actions/mcp-adapter.js +547 -0
- package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
- package/dist/esm/Sprinkle/actions/registry.js +85 -0
- package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
- package/dist/esm/Sprinkle/actions/runner.js +182 -0
- package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
- package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
- package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
- package/dist/esm/Sprinkle/actions/types.js +61 -0
- package/dist/esm/Sprinkle/actions/types.js.map +1 -0
- package/dist/esm/Sprinkle/index.js +517 -7
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/prompts.js +12 -7
- package/dist/esm/Sprinkle/prompts.js.map +1 -1
- package/dist/esm/Sprinkle/schemas.js +16 -0
- package/dist/esm/Sprinkle/schemas.js.map +1 -1
- package/dist/esm/Sprinkle/type-guards.js +3 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -1
- package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
- package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
- package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/index.d.ts +30 -0
- package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
- package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
- package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
- package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
- package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
- package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
- package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
- package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/index.d.ts +13 -0
- package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +116 -0
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
- package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
- package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
- package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/types.d.ts +76 -0
- package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
- package/dist/types/Sprinkle/index.d.ts +84 -2
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
- package/dist/types/Sprinkle/schemas.d.ts +72 -0
- package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
- package/dist/types/Sprinkle/type-guards.d.ts +4 -1
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +9 -2
- package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
- package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
- package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
- package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
- package/src/Sprinkle/__tests__/cli-adapter.test.ts +736 -0
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
- package/src/Sprinkle/__tests__/mcp-adapter.test.ts +720 -0
- package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
- package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
- package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
- package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
- package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
- package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
- package/src/Sprinkle/actions/builtin/index.ts +125 -0
- package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
- package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
- package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
- package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
- package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
- package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
- package/src/Sprinkle/actions/cli-adapter.ts +446 -0
- package/src/Sprinkle/actions/index.ts +33 -0
- package/src/Sprinkle/actions/mcp-adapter.ts +638 -0
- package/src/Sprinkle/actions/registry.ts +97 -0
- package/src/Sprinkle/actions/runner.ts +200 -0
- package/src/Sprinkle/actions/tui-helpers.ts +114 -0
- package/src/Sprinkle/actions/types.ts +91 -0
- package/src/Sprinkle/index.ts +612 -3
- package/src/Sprinkle/prompts.ts +118 -72
- package/src/Sprinkle/schemas.ts +20 -0
- package/src/Sprinkle/type-guards.ts +9 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action registry for storing and retrieving registered actions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
6
|
+
import type { AnyAction } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/** Regex for valid kebab-case action names */
|
|
9
|
+
const KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registry for managing actions registered on a Sprinkle app.
|
|
13
|
+
* Enforces name uniqueness and schema validation at registration time.
|
|
14
|
+
*/
|
|
15
|
+
export class ActionRegistry<S extends TSchema> {
|
|
16
|
+
private actions: Map<string, AnyAction<S>> = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register an action with the registry.
|
|
20
|
+
*
|
|
21
|
+
* Validates:
|
|
22
|
+
* - Name is kebab-case (lowercase letters, digits, hyphens)
|
|
23
|
+
* - Name is unique within this registry
|
|
24
|
+
* - inputSchema and outputSchema are present
|
|
25
|
+
*
|
|
26
|
+
* @throws Error if validation fails
|
|
27
|
+
*/
|
|
28
|
+
register(action: AnyAction<S>): void {
|
|
29
|
+
if (!KEBAB_CASE_REGEX.test(action.name)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid action name "${action.name}": must be kebab-case (e.g. "my-action", "get-balance")`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.actions.has(action.name)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Action "${action.name}" is already registered. Action names must be unique.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!action.inputSchema) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Action "${action.name}" is missing inputSchema. All actions must define an inputSchema.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!action.outputSchema) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Action "${action.name}" is missing outputSchema. All actions must define an outputSchema.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.actions.set(action.name, action);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Retrieve an action by name.
|
|
58
|
+
* @returns The action, or undefined if not registered
|
|
59
|
+
*/
|
|
60
|
+
get(name: string): AnyAction<S> | undefined {
|
|
61
|
+
return this.actions.get(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if an action with the given name is registered.
|
|
66
|
+
*/
|
|
67
|
+
has(name: string): boolean {
|
|
68
|
+
return this.actions.has(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List all registered actions.
|
|
73
|
+
*/
|
|
74
|
+
list(): AnyAction<S>[] {
|
|
75
|
+
return Array.from(this.actions.values());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Group all registered actions by their category.
|
|
80
|
+
* Actions without a category are placed under "default".
|
|
81
|
+
*/
|
|
82
|
+
listByCategory(): Map<string, AnyAction<S>[]> {
|
|
83
|
+
const result = new Map<string, AnyAction<S>[]>();
|
|
84
|
+
|
|
85
|
+
for (const action of this.actions.values()) {
|
|
86
|
+
const category = action.category ?? "default";
|
|
87
|
+
const bucket = result.get(category);
|
|
88
|
+
if (bucket) {
|
|
89
|
+
bucket.push(action);
|
|
90
|
+
} else {
|
|
91
|
+
result.set(category, [action]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action runner utilities: input validation, execution, mode detection, and CLI arg parsing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Value } from "@sinclair/typebox/value";
|
|
6
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
7
|
+
import { ActionError } from "./types.js";
|
|
8
|
+
import type { IAction, IActionContext, IActionResult } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute an action with raw (unvalidated) input.
|
|
12
|
+
*
|
|
13
|
+
* - Validates rawInput against action.inputSchema using Value.Check
|
|
14
|
+
* - Applies defaults and transforms via Value.Decode
|
|
15
|
+
* - Calls action.execute with decoded input
|
|
16
|
+
* - Wraps all errors in ActionError for consistent error handling
|
|
17
|
+
*
|
|
18
|
+
* @returns A discriminated IActionResult -- never throws
|
|
19
|
+
*/
|
|
20
|
+
export async function executeAction<TInput, TOutput, S extends TSchema>(
|
|
21
|
+
action: IAction<TInput, TOutput, S>,
|
|
22
|
+
rawInput: unknown,
|
|
23
|
+
context: IActionContext<S>,
|
|
24
|
+
): Promise<IActionResult<TOutput>> {
|
|
25
|
+
// First check validity so we can collect all errors at once
|
|
26
|
+
if (!Value.Check(action.inputSchema, rawInput)) {
|
|
27
|
+
const errors = Array.from(Value.Errors(action.inputSchema, rawInput)).map(
|
|
28
|
+
(e) => ({ path: e.path, message: e.message, value: e.value }),
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: new ActionError(
|
|
33
|
+
`Invalid input for action "${action.name}"`,
|
|
34
|
+
"VALIDATION_ERROR",
|
|
35
|
+
errors,
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Apply defaults and transforms
|
|
41
|
+
let decodedInput: TInput;
|
|
42
|
+
try {
|
|
43
|
+
decodedInput = Value.Decode(action.inputSchema, rawInput) as TInput;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: new ActionError(
|
|
48
|
+
`Failed to decode input for action "${action.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
49
|
+
"DECODE_ERROR",
|
|
50
|
+
err,
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const output = await action.execute(decodedInput, context);
|
|
57
|
+
return { success: true, data: output };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof ActionError) {
|
|
60
|
+
return { success: false, error: err };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: new ActionError(
|
|
65
|
+
err instanceof Error ? err.message : String(err),
|
|
66
|
+
"EXECUTION_ERROR",
|
|
67
|
+
err,
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detect the intended run mode from process argv.
|
|
75
|
+
*
|
|
76
|
+
* - No args or --interactive: "tui"
|
|
77
|
+
* - --mcp: "mcp"
|
|
78
|
+
* - --help or -h (with no positional arg): "help"
|
|
79
|
+
* - First positional arg (not starting with --): "cli"
|
|
80
|
+
* - Default fallback: "tui"
|
|
81
|
+
*/
|
|
82
|
+
export function detectMode(
|
|
83
|
+
argv: string[],
|
|
84
|
+
): "tui" | "cli" | "mcp" | "help" {
|
|
85
|
+
if (argv.length === 0) return "tui";
|
|
86
|
+
|
|
87
|
+
// Check for explicit flags first
|
|
88
|
+
if (argv.includes("--interactive")) return "tui";
|
|
89
|
+
if (argv.includes("--mcp")) return "mcp";
|
|
90
|
+
|
|
91
|
+
// Find first positional arg (not starting with -)
|
|
92
|
+
const firstPositional = argv.find((arg) => !arg.startsWith("-"));
|
|
93
|
+
if (firstPositional !== undefined) return "cli";
|
|
94
|
+
|
|
95
|
+
// Flag-only invocations
|
|
96
|
+
if (argv.includes("--help") || argv.includes("-h")) return "help";
|
|
97
|
+
|
|
98
|
+
return "tui";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse CLI argv into an action name and key-value argument record.
|
|
103
|
+
*
|
|
104
|
+
* Expects:
|
|
105
|
+
* - First positional arg as the action name
|
|
106
|
+
* - Subsequent --flag value or --flag=value pairs as arguments
|
|
107
|
+
* - --no-flag for boolean negation
|
|
108
|
+
* - Values starting with { or [ are parsed as JSON
|
|
109
|
+
*
|
|
110
|
+
* Note: This is a preliminary implementation. Complex nested inputs
|
|
111
|
+
* should be provided as JSON strings (e.g. --input '{"key":"value"}').
|
|
112
|
+
*
|
|
113
|
+
* @throws Error if no action name is found in argv
|
|
114
|
+
*/
|
|
115
|
+
export function parseCliArgs(
|
|
116
|
+
argv: string[],
|
|
117
|
+
): { actionName: string; args: Record<string, unknown> } {
|
|
118
|
+
const args: Record<string, unknown> = {};
|
|
119
|
+
|
|
120
|
+
// Extract action name: first arg that is not a flag
|
|
121
|
+
const actionName = argv.find((arg) => !arg.startsWith("-"));
|
|
122
|
+
if (!actionName) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
"No action name provided. Usage: <action-name> [--flag value ...]",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Helper to append a value, supporting repeated flags as arrays
|
|
129
|
+
const appendValue = (key: string, value: unknown): void => {
|
|
130
|
+
const existing = args[key];
|
|
131
|
+
if (existing === undefined) {
|
|
132
|
+
args[key] = value;
|
|
133
|
+
} else if (Array.isArray(existing)) {
|
|
134
|
+
existing.push(value);
|
|
135
|
+
} else {
|
|
136
|
+
// Second occurrence of the same flag -- promote to array
|
|
137
|
+
args[key] = [existing, value];
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Parse flags after the action name
|
|
142
|
+
const flagArgs = argv.slice(argv.indexOf(actionName) + 1);
|
|
143
|
+
let i = 0;
|
|
144
|
+
while (i < flagArgs.length) {
|
|
145
|
+
const arg = flagArgs[i]!;
|
|
146
|
+
|
|
147
|
+
if (arg.startsWith("--no-")) {
|
|
148
|
+
// Boolean negation: --no-flag => flag: false
|
|
149
|
+
const key = arg.slice(5);
|
|
150
|
+
appendValue(key, false);
|
|
151
|
+
i++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (arg.startsWith("--")) {
|
|
156
|
+
const eqIdx = arg.indexOf("=");
|
|
157
|
+
if (eqIdx !== -1) {
|
|
158
|
+
// --flag=value syntax
|
|
159
|
+
const key = arg.slice(2, eqIdx);
|
|
160
|
+
const rawValue = arg.slice(eqIdx + 1);
|
|
161
|
+
appendValue(key, parseArgValue(rawValue));
|
|
162
|
+
i++;
|
|
163
|
+
} else {
|
|
164
|
+
// --flag value syntax
|
|
165
|
+
const key = arg.slice(2);
|
|
166
|
+
const nextArg = flagArgs[i + 1];
|
|
167
|
+
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
168
|
+
appendValue(key, parseArgValue(nextArg));
|
|
169
|
+
i += 2;
|
|
170
|
+
} else {
|
|
171
|
+
// Flag with no value: treat as boolean true
|
|
172
|
+
appendValue(key, true);
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Skip non-flag positional args (there should only be the action name)
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { actionName, args };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse a single CLI argument value.
|
|
188
|
+
* Detects JSON objects/arrays; otherwise returns the string as-is.
|
|
189
|
+
*/
|
|
190
|
+
function parseArgValue(raw: string): unknown {
|
|
191
|
+
const trimmed = raw.trim();
|
|
192
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(trimmed);
|
|
195
|
+
} catch {
|
|
196
|
+
// Fall through: return as string if JSON parse fails
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return raw;
|
|
200
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI helper utilities for integrating the action system with interactive prompts.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - promptAndExecute: gather input via FillInStruct then run executeAction
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Functions in this module require a fully initialized Sprinkle instance with
|
|
9
|
+
* terminal access. They are intended for TUI mode only; in CLI or MCP mode use
|
|
10
|
+
* executeAction directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
14
|
+
import { ActionError } from "./types.js";
|
|
15
|
+
import type { IAction, IActionContext, IActionResult } from "./types.js";
|
|
16
|
+
import { executeAction } from "./runner.js";
|
|
17
|
+
|
|
18
|
+
// Import Sprinkle as a type only to avoid circular dependency
|
|
19
|
+
import type { Sprinkle } from "../index.js";
|
|
20
|
+
|
|
21
|
+
// Import UserCancelledError to detect user cancellations from FillInStruct
|
|
22
|
+
import { UserCancelledError } from "../types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Interactively gather input for an action using FillInStruct, then execute
|
|
26
|
+
* the action and return the result.
|
|
27
|
+
*
|
|
28
|
+
* The function:
|
|
29
|
+
* 1. Calls `sprinkle.FillInStruct(action.inputSchema, defaults)` to collect
|
|
30
|
+
* input from the user via TUI prompts.
|
|
31
|
+
* 2. Builds an `IActionContext` from the sprinkle instance.
|
|
32
|
+
* 3. Calls `executeAction(action, input, context)` and returns the result.
|
|
33
|
+
*
|
|
34
|
+
* If the user cancels the prompt (`UserCancelledError` is thrown), the function
|
|
35
|
+
* returns an `IActionFailure` with code `USER_CANCELLED` rather than throwing.
|
|
36
|
+
*
|
|
37
|
+
* Two separate schema type parameters are used:
|
|
38
|
+
* - `S` is the concrete settings schema of the `Sprinkle` instance.
|
|
39
|
+
* - `SA` is the settings schema the action was typed against (often `TSchema`).
|
|
40
|
+
*
|
|
41
|
+
* This allows a `Sprinkle<TObject<...>>` to be passed alongside an action
|
|
42
|
+
* typed with the base `TSchema` constraint (the same pattern used by all
|
|
43
|
+
* built-in actions), while still preserving full type safety for TInput/TOutput.
|
|
44
|
+
*
|
|
45
|
+
* @param sprinkle - An initialized Sprinkle instance (TUI mode).
|
|
46
|
+
* @param action - The action to prompt for and execute.
|
|
47
|
+
* @param defaults - Optional partial defaults pre-filled in the prompt.
|
|
48
|
+
* @returns An `IActionResult` discriminated union (never throws).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const result = await promptAndExecute(sprinkle, greetAction);
|
|
53
|
+
* if (result.success) {
|
|
54
|
+
* console.log(result.data.greeting);
|
|
55
|
+
* } else {
|
|
56
|
+
* if (result.error.code === "USER_CANCELLED") return; // user backed out
|
|
57
|
+
* console.error(result.error.message);
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export async function promptAndExecute<
|
|
62
|
+
TInput,
|
|
63
|
+
TOutput,
|
|
64
|
+
S extends TSchema,
|
|
65
|
+
SA extends TSchema,
|
|
66
|
+
>(
|
|
67
|
+
sprinkle: Sprinkle<S>,
|
|
68
|
+
action: IAction<TInput, TOutput, SA>,
|
|
69
|
+
// defaults is typed as Partial<TInput> -- the static input shape.
|
|
70
|
+
// Internally it is cast to `never` because FillInStruct expects TExact<U>
|
|
71
|
+
// which is derived from the inputSchema's TSchema generic, not TInput.
|
|
72
|
+
defaults?: Partial<TInput>,
|
|
73
|
+
): Promise<IActionResult<TOutput>> {
|
|
74
|
+
let rawInput: unknown;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// FillInStruct<U> is generic over TSchema; we pass the action's inputSchema
|
|
78
|
+
// and cast defaults to satisfy the generic bound.
|
|
79
|
+
rawInput = await sprinkle.FillInStruct(
|
|
80
|
+
action.inputSchema,
|
|
81
|
+
defaults as never,
|
|
82
|
+
);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof UserCancelledError) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: new ActionError(
|
|
88
|
+
"User cancelled the input prompt.",
|
|
89
|
+
"USER_CANCELLED",
|
|
90
|
+
err,
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Any other error from FillInStruct is unexpected -- wrap it
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: new ActionError(
|
|
98
|
+
`Prompt failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
99
|
+
"PROMPT_ERROR",
|
|
100
|
+
err,
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build the context. The action's SA context is satisfied by the concrete
|
|
106
|
+
// Sprinkle<S> instance via a cast -- the action only accesses sprinkle via
|
|
107
|
+
// the IActionContext interface which it should treat as opaque when SA = TSchema.
|
|
108
|
+
const context = {
|
|
109
|
+
sprinkle: sprinkle as unknown as Sprinkle<SA>,
|
|
110
|
+
settings: sprinkle.settings as unknown,
|
|
111
|
+
} as IActionContext<SA>;
|
|
112
|
+
|
|
113
|
+
return executeAction(action, rawInput, context);
|
|
114
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core action types for the action-centric architecture.
|
|
3
|
+
* Actions are pure functions that receive validated typed input
|
|
4
|
+
* and return typed output, with no interactive prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TSchema, Static } from "@sinclair/typebox";
|
|
8
|
+
// Import Sprinkle only as a type to avoid circular dependency
|
|
9
|
+
import type { Sprinkle } from "../index.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Utility type to extract the static type from a TypeBox schema.
|
|
13
|
+
*/
|
|
14
|
+
type TExact<T> = T extends TSchema ? Static<T> : T;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context passed to action execute functions.
|
|
18
|
+
* Provides access to the Sprinkle instance and typed settings shorthand.
|
|
19
|
+
*/
|
|
20
|
+
export interface IActionContext<S extends TSchema> {
|
|
21
|
+
/** The full Sprinkle instance (settings, wallet, provider helpers) */
|
|
22
|
+
sprinkle: Sprinkle<S>;
|
|
23
|
+
/** Typed shorthand for sprinkle.settings */
|
|
24
|
+
settings: TExact<S>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Successful action result.
|
|
29
|
+
*/
|
|
30
|
+
export interface IActionSuccess<T> {
|
|
31
|
+
success: true;
|
|
32
|
+
data: T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Failed action result.
|
|
37
|
+
*/
|
|
38
|
+
export interface IActionFailure {
|
|
39
|
+
success: false;
|
|
40
|
+
error: ActionError;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Discriminated union result type for actions.
|
|
45
|
+
*/
|
|
46
|
+
export type IActionResult<T> = IActionSuccess<T> | IActionFailure;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Structured error type for action failures.
|
|
50
|
+
* Carries a machine-readable code and optional details.
|
|
51
|
+
*/
|
|
52
|
+
export class ActionError extends Error {
|
|
53
|
+
code: string;
|
|
54
|
+
details: unknown;
|
|
55
|
+
|
|
56
|
+
constructor(message: string, code: string, details?: unknown) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ActionError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.details = details;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A typed action with input/output schemas and an execute function.
|
|
66
|
+
*
|
|
67
|
+
* @template TInput - The static input type (derived from inputSchema)
|
|
68
|
+
* @template TOutput - The static output type (derived from outputSchema)
|
|
69
|
+
* @template S - The Sprinkle settings schema type
|
|
70
|
+
*/
|
|
71
|
+
export interface IAction<TInput, TOutput, S extends TSchema> {
|
|
72
|
+
/** Unique kebab-case identifier for this action */
|
|
73
|
+
name: string;
|
|
74
|
+
/** Human-readable description of what this action does */
|
|
75
|
+
description: string;
|
|
76
|
+
/** Optional grouping category for display/help */
|
|
77
|
+
category?: string;
|
|
78
|
+
/** TypeBox schema for validating and decoding input */
|
|
79
|
+
inputSchema: TSchema;
|
|
80
|
+
/** TypeBox schema describing the output shape */
|
|
81
|
+
outputSchema: TSchema;
|
|
82
|
+
/** Pure execution function -- no interactive prompts allowed */
|
|
83
|
+
execute: (input: TInput, context: IActionContext<S>) => Promise<TOutput>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convenience alias for an action with any input/output types.
|
|
88
|
+
* Used for registry storage where exact types are not required.
|
|
89
|
+
*/
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
export type AnyAction<S extends TSchema> = IAction<any, any, S>;
|