@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.
Files changed (197) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  7. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  9. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +744 -0
  11. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  13. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  14. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +711 -0
  15. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  16. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  17. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  18. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  19. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  20. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  21. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  22. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  23. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/index.js +174 -0
  29. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  30. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  31. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  32. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  33. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  34. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  35. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  36. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  37. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  38. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  39. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  40. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  41. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  42. package/dist/cjs/Sprinkle/actions/cli-adapter.js +390 -0
  43. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  44. package/dist/cjs/Sprinkle/actions/index.js +139 -0
  45. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  46. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +557 -0
  47. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  48. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  49. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  50. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  51. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  52. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  53. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  54. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  55. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  56. package/dist/cjs/Sprinkle/index.js +678 -5
  57. package/dist/cjs/Sprinkle/index.js.map +1 -1
  58. package/dist/cjs/Sprinkle/prompts.js +12 -7
  59. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  60. package/dist/cjs/Sprinkle/schemas.js +17 -1
  61. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  62. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  63. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  64. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  65. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  66. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  67. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  68. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  69. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  70. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  71. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  72. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +742 -0
  73. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  74. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  75. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  76. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +710 -0
  77. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  78. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  79. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  80. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  81. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  82. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  83. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  84. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  85. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  86. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  87. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  88. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  89. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  90. package/dist/esm/Sprinkle/actions/builtin/index.js +37 -0
  91. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  92. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  93. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  94. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  95. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  96. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  97. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  98. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  99. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  100. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  101. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  102. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  103. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  104. package/dist/esm/Sprinkle/actions/cli-adapter.js +379 -0
  105. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  106. package/dist/esm/Sprinkle/actions/index.js +12 -0
  107. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  108. package/dist/esm/Sprinkle/actions/mcp-adapter.js +547 -0
  109. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  110. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  111. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  112. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  113. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  114. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  115. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  116. package/dist/esm/Sprinkle/actions/types.js +61 -0
  117. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  118. package/dist/esm/Sprinkle/index.js +517 -7
  119. package/dist/esm/Sprinkle/index.js.map +1 -1
  120. package/dist/esm/Sprinkle/prompts.js +12 -7
  121. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  122. package/dist/esm/Sprinkle/schemas.js +16 -0
  123. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  124. package/dist/esm/Sprinkle/type-guards.js +3 -0
  125. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  126. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  127. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  128. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  129. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  130. package/dist/types/Sprinkle/actions/builtin/index.d.ts +30 -0
  131. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  132. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  133. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  134. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  135. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  136. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  137. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  138. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  139. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  140. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  141. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  142. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  143. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  144. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  145. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  146. package/dist/types/Sprinkle/actions/index.d.ts +13 -0
  147. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  148. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +116 -0
  149. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  150. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  151. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  152. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  153. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  154. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  155. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  156. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  157. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  158. package/dist/types/Sprinkle/index.d.ts +84 -2
  159. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  160. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  161. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  162. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  163. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  164. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  165. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  166. package/package.json +9 -2
  167. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  168. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  169. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  170. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  171. package/src/Sprinkle/__tests__/cli-adapter.test.ts +736 -0
  172. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  173. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +720 -0
  174. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  175. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  176. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  177. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  178. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  179. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  180. package/src/Sprinkle/actions/builtin/index.ts +125 -0
  181. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  182. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  183. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  184. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  185. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  186. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  187. package/src/Sprinkle/actions/cli-adapter.ts +446 -0
  188. package/src/Sprinkle/actions/index.ts +33 -0
  189. package/src/Sprinkle/actions/mcp-adapter.ts +638 -0
  190. package/src/Sprinkle/actions/registry.ts +97 -0
  191. package/src/Sprinkle/actions/runner.ts +200 -0
  192. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  193. package/src/Sprinkle/actions/types.ts +91 -0
  194. package/src/Sprinkle/index.ts +612 -3
  195. package/src/Sprinkle/prompts.ts +118 -72
  196. package/src/Sprinkle/schemas.ts +20 -0
  197. package/src/Sprinkle/type-guards.ts +9 -0
@@ -58,10 +58,14 @@ export {
58
58
  NetworkSchema,
59
59
  MultisigScriptModule,
60
60
  MultisigScript,
61
+ NativeScriptInputSchema,
62
+ NativeScriptsParam,
61
63
  ProviderSettingsSchema,
62
64
  WalletSettingsSchema,
63
65
  } from "./schemas.js";
64
66
  export type { TMultisigScript } from "./schemas.js";
67
+ import type { TMultisigScript } from "./schemas.js";
68
+ import { MultisigScript as MultisigScriptSchema } from "./schemas.js";
65
69
 
66
70
  // Import and re-export type guards
67
71
  import {
@@ -69,7 +73,10 @@ import {
69
73
  isImport,
70
74
  isArray,
71
75
  isBigInt,
76
+ isBoolean,
77
+ isInteger,
72
78
  isLiteral,
79
+ isNumber,
73
80
  isObject,
74
81
  isRef,
75
82
  isString,
@@ -88,7 +95,10 @@ export {
88
95
  isImport,
89
96
  isArray,
90
97
  isBigInt,
98
+ isBoolean,
99
+ isInteger,
91
100
  isLiteral,
101
+ isNumber,
92
102
  isObject,
93
103
  isRef,
94
104
  isString,
@@ -146,6 +156,53 @@ import {
146
156
  import { promptObject, promptArray } from "./menus/index.js";
147
157
  import { formatPath } from "./utils/formatting.js";
148
158
 
159
+ // Import and re-export action system
160
+ import {
161
+ ActionRegistry,
162
+ ActionError,
163
+ executeAction,
164
+ detectMode,
165
+ parseCliArgs,
166
+ generateActionHelp,
167
+ camelToKebab,
168
+ kebabToCamel,
169
+ coerceValue,
170
+ parseArgvWithSchema,
171
+ generateAppHelp,
172
+ runCli,
173
+ runMcp,
174
+ promptAndExecute,
175
+ } from "./actions/index.js";
176
+ import type { AnyAction, IActionContext, IActionResult } from "./actions/index.js";
177
+ import { toNativeScript } from "./actions/index.js";
178
+ export type { IAction, IActionContext, IActionResult, IActionSuccess, IActionFailure, AnyAction } from "./actions/index.js";
179
+ export {
180
+ ActionRegistry,
181
+ ActionError,
182
+ executeAction,
183
+ detectMode,
184
+ parseCliArgs,
185
+ generateActionHelp,
186
+ camelToKebab,
187
+ kebabToCamel,
188
+ coerceValue,
189
+ parseArgvWithSchema,
190
+ generateAppHelp,
191
+ runCli,
192
+ typeboxToJsonSchema,
193
+ coerceMcpInput,
194
+ getMcpSdk,
195
+ createMcpServer,
196
+ runMcp,
197
+ getBuiltinActions,
198
+ promptAndExecute,
199
+ } from "./actions/index.js";
200
+ import {
201
+ mintToken,
202
+ simpleSend,
203
+ registerStakeScript,
204
+ } from "./actions/builtin/utility-actions.js";
205
+
149
206
  export interface IMenuAction<S extends TSchema> {
150
207
  title: string;
151
208
  action: (sprinkle: Sprinkle<S>) => Promise<Sprinkle<S> | void>;
@@ -162,14 +219,17 @@ export class Sprinkle<S extends TSchema> {
162
219
  settings: TExact<S> = {} as TExact<S>;
163
220
  type: S;
164
221
  defaults: Record<string, unknown> = {};
222
+ addressbook: Record<string, TMultisigScript> = {};
165
223
  options: ISprinkleOptions;
166
224
  profileId: string = "";
167
225
  profileMeta: IProfileMeta = { name: "", createdAt: "", updatedAt: "" };
226
+ actionRegistry: ActionRegistry<S>;
168
227
 
169
228
  constructor(type: S, storagePath: string, options?: ISprinkleOptions) {
170
229
  this.type = type;
171
230
  this.storagePath = storagePath;
172
231
  this.options = options ?? {};
232
+ this.actionRegistry = new ActionRegistry<S>();
173
233
  }
174
234
 
175
235
  // --- Current Profile Accessor ---
@@ -252,6 +312,7 @@ export class Sprinkle<S extends TSchema> {
252
312
  this.profileMeta = parsed.meta;
253
313
  this.settings = await this.decryptSettings(parsed.settings as TExact<S>);
254
314
  this.defaults = parsed.defaults ?? {};
315
+ this.addressbook = parsed.addressbook ?? {};
255
316
  // Update active profile pointer
256
317
  fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), id, "utf-8");
257
318
  }
@@ -274,6 +335,7 @@ export class Sprinkle<S extends TSchema> {
274
335
  meta: this.profileMeta,
275
336
  settings: settingsToSave,
276
337
  defaults: this.defaults,
338
+ addressbook: this.addressbook,
277
339
  },
278
340
  bigIntReplacer,
279
341
  2,
@@ -354,6 +416,7 @@ export class Sprinkle<S extends TSchema> {
354
416
  },
355
417
  settings: parsed.settings,
356
418
  defaults: parsed.defaults ?? {},
419
+ addressbook: parsed.addressbook ?? {},
357
420
  };
358
421
  fs.mkdirSync(profilesDir, { recursive: true });
359
422
  fs.writeFileSync(
@@ -438,6 +501,7 @@ export class Sprinkle<S extends TSchema> {
438
501
  meta: { name, description, createdAt: now, updatedAt: now },
439
502
  settings: settingsToSave,
440
503
  defaults: this.defaults,
504
+ addressbook: this.addressbook,
441
505
  },
442
506
  bigIntReplacer,
443
507
  2,
@@ -549,6 +613,7 @@ export class Sprinkle<S extends TSchema> {
549
613
  if (!main) {
550
614
  choices.push({ name: "Back", value: -1 });
551
615
  } else {
616
+ choices.push({ name: "Utilities", value: -6 });
552
617
  choices.push({ name: "Settings & Profiles", value: -5 });
553
618
  choices.push({ name: "Exit", value: -1 });
554
619
  }
@@ -567,6 +632,74 @@ export class Sprinkle<S extends TSchema> {
567
632
  return;
568
633
  }
569
634
  const selection = selectionResult as number;
635
+ if (selection === -6) {
636
+ const utilitiesMenu: IMenu<S> = {
637
+ title: "Utilities",
638
+ items: [
639
+ {
640
+ title: "Mint Token",
641
+ action: async () => {
642
+ const result = await promptAndExecute(this, mintToken);
643
+ if (!result.success) {
644
+ if (result.error.code === "USER_CANCELLED") return;
645
+ console.error(`\nError (${result.error.code}): ${result.error.message}\n`);
646
+ return;
647
+ }
648
+ const data = result.data as { txCbor: string; policyId: string; tokenName: string; amount: string };
649
+ console.log(`\nPolicy: ${data.policyId}\nToken: ${data.tokenName}\nAmount: ${data.amount}\n`);
650
+ const blaze = await Sprinkle.GetBlaze(
651
+ (this.settings as Record<string, unknown>).network as TExact<typeof NetworkSchema>,
652
+ (this.settings as Record<string, unknown>).provider as TExact<typeof ProviderSettingsSchema>,
653
+ (this.settings as Record<string, unknown>).wallet as TExact<typeof WalletSettingsSchema>,
654
+ );
655
+ const tx = Core.Transaction.fromCbor(Core.TxCBOR(data.txCbor));
656
+ await this.TxDialog(blaze, tx);
657
+ },
658
+ },
659
+ {
660
+ title: "Simple Send",
661
+ action: async () => {
662
+ const result = await promptAndExecute(this, simpleSend);
663
+ if (!result.success) {
664
+ if (result.error.code === "USER_CANCELLED") return;
665
+ console.error(`\nError (${result.error.code}): ${result.error.message}\n`);
666
+ return;
667
+ }
668
+ const data = result.data as { txCbor: string };
669
+ const blaze = await Sprinkle.GetBlaze(
670
+ (this.settings as Record<string, unknown>).network as TExact<typeof NetworkSchema>,
671
+ (this.settings as Record<string, unknown>).provider as TExact<typeof ProviderSettingsSchema>,
672
+ (this.settings as Record<string, unknown>).wallet as TExact<typeof WalletSettingsSchema>,
673
+ );
674
+ const tx = Core.Transaction.fromCbor(Core.TxCBOR(data.txCbor));
675
+ await this.TxDialog(blaze, tx);
676
+ },
677
+ },
678
+ {
679
+ title: "Register Stake Script",
680
+ action: async () => {
681
+ const result = await promptAndExecute(this, registerStakeScript);
682
+ if (!result.success) {
683
+ if (result.error.code === "USER_CANCELLED") return;
684
+ console.error(`\nError (${result.error.code}): ${result.error.message}\n`);
685
+ return;
686
+ }
687
+ const data = result.data as { txCbor: string };
688
+ const blaze = await Sprinkle.GetBlaze(
689
+ (this.settings as Record<string, unknown>).network as TExact<typeof NetworkSchema>,
690
+ (this.settings as Record<string, unknown>).provider as TExact<typeof ProviderSettingsSchema>,
691
+ (this.settings as Record<string, unknown>).wallet as TExact<typeof WalletSettingsSchema>,
692
+ );
693
+ const tx = Core.Transaction.fromCbor(Core.TxCBOR(data.txCbor));
694
+ await this.TxDialog(blaze, tx);
695
+ },
696
+ },
697
+ ],
698
+ };
699
+ await this._showMenu(utilitiesMenu, false, [...path, "Utilities"], true);
700
+ await this._showMenu(menu, main, path, true);
701
+ return;
702
+ }
570
703
  if (selection === -5) {
571
704
  const settingsMenu: IMenu<S> = {
572
705
  title: "Settings & Profiles",
@@ -640,6 +773,114 @@ export class Sprinkle<S extends TSchema> {
640
773
  await this.deleteProfile();
641
774
  },
642
775
  },
776
+ {
777
+ title: "Addressbook",
778
+ items: [
779
+ {
780
+ title: "View entries",
781
+ action: async () => {
782
+ const entries = Object.entries(this.addressbook);
783
+ if (entries.length === 0) {
784
+ console.log("Addressbook is empty.");
785
+ } else {
786
+ for (const [name, ms] of entries) {
787
+ const json = JSON.stringify(ms, bigIntReplacer, 2);
788
+ let hashStr = "unknown";
789
+ try {
790
+ hashStr = toNativeScript(ms).hash();
791
+ } catch { /* skip */ }
792
+ console.log(colors.bold(name) + " " + colors.dim(hashStr));
793
+ console.log(json);
794
+ console.log();
795
+ }
796
+ }
797
+ await selectWithClear({
798
+ message: "Press Enter to continue...",
799
+ choices: [{ name: "Continue", value: "continue" }],
800
+ });
801
+ },
802
+ },
803
+ {
804
+ title: "Add entry",
805
+ action: async () => {
806
+ const name = await inputCancellable({
807
+ message: "Entry name:",
808
+ validate: (v) =>
809
+ v.trim().length > 0 ? true : "Name cannot be empty",
810
+ });
811
+ if (name === null) return;
812
+ if (this.addressbook[name]) {
813
+ const overwrite = await confirmCancellable({
814
+ message: `Entry "${name}" already exists. Overwrite?`,
815
+ default: false,
816
+ });
817
+ if (!overwrite) return;
818
+ }
819
+ try {
820
+ const script = await this.FillInStruct(MultisigScriptSchema);
821
+ this.addressbook[name] = script as TMultisigScript;
822
+ this.saveSettings();
823
+ console.log(`Added "${name}" to addressbook.`);
824
+ } catch (e) {
825
+ if (e instanceof UserCancelledError) return;
826
+ throw e;
827
+ }
828
+ },
829
+ },
830
+ {
831
+ title: "Edit entry",
832
+ action: async () => {
833
+ const entries = Object.keys(this.addressbook);
834
+ if (entries.length === 0) {
835
+ console.log("Addressbook is empty.");
836
+ return;
837
+ }
838
+ const selected = await selectCancellable({
839
+ message: "Select entry to edit:",
840
+ choices: entries.map((n) => ({ name: n, value: n })),
841
+ });
842
+ if (selected === null) return;
843
+ const editName = selected as string;
844
+ try {
845
+ const updated = await this.EditStruct(
846
+ MultisigScriptSchema,
847
+ this.addressbook[editName] as any,
848
+ );
849
+ this.addressbook[editName] = updated as TMultisigScript;
850
+ this.saveSettings();
851
+ console.log(`Updated "${editName}".`);
852
+ } catch (e) {
853
+ if (e instanceof UserCancelledError) return;
854
+ throw e;
855
+ }
856
+ },
857
+ },
858
+ {
859
+ title: "Delete entry",
860
+ action: async () => {
861
+ const entries = Object.keys(this.addressbook);
862
+ if (entries.length === 0) {
863
+ console.log("Addressbook is empty.");
864
+ return;
865
+ }
866
+ const delSelected = await selectCancellable({
867
+ message: "Select entry to delete:",
868
+ choices: entries.map((n) => ({ name: n, value: n })),
869
+ });
870
+ if (delSelected === null) return;
871
+ const delName = delSelected as string;
872
+ const confirmed = await confirmCancellable({
873
+ message: `Delete "${delName}"?`,
874
+ default: false,
875
+ });
876
+ if (!confirmed) return;
877
+ delete this.addressbook[delName];
878
+ this.saveSettings();
879
+ console.log(`Deleted "${delName}".`);
880
+ },
881
+ },
882
+ ],
883
+ } as IMenu<S>,
643
884
  ],
644
885
  };
645
886
  await this._showMenu(settingsMenu, false, [...path, "Settings & Profiles"], true);
@@ -771,6 +1012,105 @@ export class Sprinkle<S extends TSchema> {
771
1012
  return maskSensitiveFields(this.settings, this.type);
772
1013
  }
773
1014
 
1015
+ // --- Non-interactive profile management (for CLI/MCP actions) ---
1016
+
1017
+ /**
1018
+ * Look up a profile entry by its ID without loading it.
1019
+ * Returns undefined if no profile with the given ID exists.
1020
+ */
1021
+ getProfileById(id: string): IProfileEntry | undefined {
1022
+ return this.scanProfiles().find((p) => p.id === id);
1023
+ }
1024
+
1025
+ /**
1026
+ * Create a new profile file without interactive prompts.
1027
+ * Does NOT switch the active profile.
1028
+ *
1029
+ * @throws ActionError with code DUPLICATE_PROFILE if a profile with the same name already exists.
1030
+ * @returns The created IProfileEntry
1031
+ */
1032
+ async createProfileNonInteractive(
1033
+ name: string,
1034
+ description?: string,
1035
+ initialSettings?: Partial<TExact<S>>,
1036
+ ): Promise<IProfileEntry> {
1037
+ const profiles = this.scanProfiles();
1038
+ const nameLower = name.toLowerCase();
1039
+ const duplicate = profiles.find(
1040
+ (p) => p.meta.name.toLowerCase() === nameLower,
1041
+ );
1042
+ if (duplicate) {
1043
+ throw new ActionError(
1044
+ `A profile named "${name}" already exists.`,
1045
+ "DUPLICATE_PROFILE",
1046
+ { existingId: duplicate.id },
1047
+ );
1048
+ }
1049
+
1050
+ const profilesDir = Sprinkle.profilesDir(this.storagePath);
1051
+ if (!fs.existsSync(profilesDir)) {
1052
+ fs.mkdirSync(profilesDir, { recursive: true });
1053
+ }
1054
+
1055
+ const baseId = Sprinkle.sanitizeProfileId(name);
1056
+ const id = Sprinkle.findAvailableId(profilesDir, baseId);
1057
+ const now = new Date().toISOString();
1058
+ const meta: IProfileMeta = { name, description, createdAt: now, updatedAt: now };
1059
+
1060
+ const profileData = {
1061
+ meta,
1062
+ settings: initialSettings ?? {},
1063
+ defaults: {},
1064
+ addressbook: {},
1065
+ };
1066
+
1067
+ fs.writeFileSync(
1068
+ path.join(profilesDir, `${id}.json`),
1069
+ JSON.stringify(profileData, bigIntReplacer, 2),
1070
+ "utf-8",
1071
+ );
1072
+
1073
+ return { id, meta };
1074
+ }
1075
+
1076
+ /**
1077
+ * Delete a profile file by ID.
1078
+ *
1079
+ * @throws ActionError with code PROFILE_NOT_FOUND if no profile with the given ID exists.
1080
+ * @throws ActionError with code CANNOT_DELETE_ONLY_PROFILE if this is the only profile.
1081
+ * @throws ActionError with code CANNOT_DELETE_ACTIVE_PROFILE if this is the currently active profile.
1082
+ */
1083
+ deleteProfileById(id: string): void {
1084
+ const profiles = this.scanProfiles();
1085
+ const profile = profiles.find((p) => p.id === id);
1086
+
1087
+ if (!profile) {
1088
+ throw new ActionError(
1089
+ `Profile "${id}" not found.`,
1090
+ "PROFILE_NOT_FOUND",
1091
+ { id },
1092
+ );
1093
+ }
1094
+
1095
+ if (profiles.length === 1) {
1096
+ throw new ActionError(
1097
+ "Cannot delete the only profile.",
1098
+ "CANNOT_DELETE_ONLY_PROFILE",
1099
+ { id },
1100
+ );
1101
+ }
1102
+
1103
+ if (id === this.profileId) {
1104
+ throw new ActionError(
1105
+ "Cannot delete the active profile. Switch to a different profile first.",
1106
+ "CANNOT_DELETE_ACTIVE_PROFILE",
1107
+ { id },
1108
+ );
1109
+ }
1110
+
1111
+ fs.unlinkSync(Sprinkle.profilePath(this.storagePath, id));
1112
+ }
1113
+
774
1114
  async TxDialog<P extends Provider, W extends Wallet>(
775
1115
  blaze: Blaze<P, W>,
776
1116
  tx: Core.Transaction,
@@ -1095,9 +1435,58 @@ export class Sprinkle<S extends TSchema> {
1095
1435
  `Could not resolve type ${type["$ref"]} at ${path.join(".")}`,
1096
1436
  );
1097
1437
  }
1098
- return this._fillInStruct(resolvedType, path, defs, def) as Promise<
1099
- TExact<U>
1100
- >;
1438
+
1439
+ // Addressbook integration for MultisigScript fields
1440
+ if (type["$ref"] === "MultisigScript" && Object.keys(this.addressbook).length > 0) {
1441
+ const sourceChoice = await selectWithClear({
1442
+ message: "Select source for native script:",
1443
+ choices: [
1444
+ { name: "Enter manually", value: "manual" },
1445
+ { name: "Select from addressbook", value: "addressbook" },
1446
+ ],
1447
+ });
1448
+ if (sourceChoice === null) throw new UserCancelledError();
1449
+ if (sourceChoice === "addressbook") {
1450
+ const entries = Object.entries(this.addressbook);
1451
+ const abChoices = entries.map(([name, ms]) => {
1452
+ let hashStr = "";
1453
+ try { hashStr = " " + colors.dim(toNativeScript(ms).hash()); } catch { /* skip */ }
1454
+ return { name: name + hashStr, value: name };
1455
+ });
1456
+ const selected = await selectWithClear({
1457
+ message: "Select addressbook entry:",
1458
+ choices: abChoices,
1459
+ });
1460
+ if (selected === null) throw new UserCancelledError();
1461
+ return this.addressbook[selected as string] as TExact<U>;
1462
+ }
1463
+ }
1464
+
1465
+ const result = await this._fillInStruct(resolvedType, path, defs, def) as TExact<U>;
1466
+
1467
+ // Offer to save manually entered MultisigScript to addressbook
1468
+ if (type["$ref"] === "MultisigScript" && result !== undefined) {
1469
+ const shouldSave = await selectWithClear({
1470
+ message: "Save this script to the addressbook?",
1471
+ choices: [
1472
+ { name: "No", value: false },
1473
+ { name: "Yes", value: true },
1474
+ ],
1475
+ });
1476
+ if (shouldSave) {
1477
+ const entryName = await inputCancellable({
1478
+ message: "Addressbook entry name:",
1479
+ validate: (v) => v.trim().length > 0 ? true : "Name cannot be empty",
1480
+ });
1481
+ if (entryName !== null) {
1482
+ this.addressbook[entryName] = result as TMultisigScript;
1483
+ this.saveSettings();
1484
+ console.log(`Saved "${entryName}" to addressbook.`);
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ return result;
1101
1490
  }
1102
1491
  if (isOptional(type)) {
1103
1492
  const pathDisplay = formatPath(path) || "value";
@@ -1254,6 +1643,22 @@ export class Sprinkle<S extends TSchema> {
1254
1643
  return BigInt(answer) as TExact<U>;
1255
1644
  }
1256
1645
 
1646
+ if (isBoolean(type)) {
1647
+ const pathDisplay = formatPath(path) || "value";
1648
+ const answer = await selectWithClear({
1649
+ message: Sprinkle.ExtractMessage(type, `${pathDisplay}?`),
1650
+ choices: [
1651
+ { name: "Yes", value: true },
1652
+ { name: "No", value: false },
1653
+ ],
1654
+ default: def !== undefined ? (def as boolean) : hasDefault(type) ? getDefault(type) : undefined,
1655
+ });
1656
+ if (answer === null) {
1657
+ throw new UserCancelledError();
1658
+ }
1659
+ return answer as TExact<U>;
1660
+ }
1661
+
1257
1662
  if (isLiteral(type)) {
1258
1663
  return type.const as TExact<U>;
1259
1664
  }
@@ -1376,4 +1781,208 @@ export class Sprinkle<S extends TSchema> {
1376
1781
 
1377
1782
  return def;
1378
1783
  }
1784
+
1785
+ // --- Action System ---
1786
+
1787
+ /**
1788
+ * Register an action on this Sprinkle instance.
1789
+ * Delegates to the internal ActionRegistry with name and schema validation.
1790
+ */
1791
+ registerAction(action: AnyAction<S>): void {
1792
+ this.actionRegistry.register(action);
1793
+ }
1794
+
1795
+ /**
1796
+ * Retrieve a registered action by name.
1797
+ * @returns The action, or undefined if not registered
1798
+ */
1799
+ getAction(name: string): AnyAction<S> | undefined {
1800
+ return this.actionRegistry.get(name);
1801
+ }
1802
+
1803
+ /**
1804
+ * List all registered actions.
1805
+ */
1806
+ listActions(): AnyAction<S>[] {
1807
+ return this.actionRegistry.list();
1808
+ }
1809
+
1810
+ /**
1811
+ * List all registered actions grouped by category.
1812
+ * Uncategorized actions are grouped under "default".
1813
+ */
1814
+ listActionsByCategory(): Map<string, AnyAction<S>[]> {
1815
+ return this.actionRegistry.listByCategory();
1816
+ }
1817
+
1818
+ /**
1819
+ * Execute a registered action by name with raw (unvalidated) input.
1820
+ * Input is validated against the action's inputSchema before execution.
1821
+ *
1822
+ * @throws Error if the action name is not registered
1823
+ */
1824
+ async runAction(
1825
+ name: string,
1826
+ input: unknown,
1827
+ ): Promise<IActionResult<unknown>> {
1828
+ const action = this.actionRegistry.get(name);
1829
+ if (!action) {
1830
+ throw new Error(
1831
+ `Action "${name}" is not registered. Available actions: ${this.actionRegistry.list().map((a) => a.name).join(", ") || "(none)"}`,
1832
+ );
1833
+ }
1834
+
1835
+ const context: IActionContext<S> = {
1836
+ sprinkle: this,
1837
+ settings: this.settings,
1838
+ };
1839
+
1840
+ return executeAction(action, input, context);
1841
+ }
1842
+
1843
+ /**
1844
+ * Non-interactive profile initialization for CLI/MCP modes.
1845
+ * Resolves a profile without any user prompts.
1846
+ *
1847
+ * Resolution order:
1848
+ * 1. If profileName provided, load that profile directly
1849
+ * 2. If exactly one profile exists, auto-select it
1850
+ * 3. If multiple profiles exist and no name given, throw with available names
1851
+ * 4. If no profiles exist, throw instructing user to run in interactive mode
1852
+ */
1853
+ private async initNonInteractive(profileName?: string): Promise<void> {
1854
+ await this.migrateIfNeeded();
1855
+
1856
+ if (profileName) {
1857
+ await this.loadProfile(profileName);
1858
+ return;
1859
+ }
1860
+
1861
+ const profiles = this.scanProfiles();
1862
+
1863
+ if (profiles.length === 0) {
1864
+ throw new Error(
1865
+ "No profiles found. Run in interactive mode to create one.",
1866
+ );
1867
+ }
1868
+
1869
+ if (profiles.length === 1) {
1870
+ await this.loadProfile(profiles[0]!.id);
1871
+ return;
1872
+ }
1873
+
1874
+ // Multiple profiles without a --profile flag
1875
+ const names = profiles.map((p) => `"${p.id}" (${p.meta.name})`).join(", ");
1876
+ throw new Error(
1877
+ `Multiple profiles found. Specify one with --profile <id>. Available profiles: ${names}`,
1878
+ );
1879
+ }
1880
+
1881
+ /**
1882
+ * Static entry point that detects mode and runs accordingly.
1883
+ *
1884
+ * Modes:
1885
+ * - "tui" -- Interactive TUI menu (requires menu option)
1886
+ * - "cli" -- CLI action execution (parses argv for action name and args)
1887
+ * - "mcp" -- MCP protocol mode (not yet implemented)
1888
+ * - "help" -- Print available actions and exit
1889
+ */
1890
+ static async run<S extends TSchema>(opts: {
1891
+ type: S;
1892
+ storagePath: string;
1893
+ menu?: IMenu<S>;
1894
+ actions?: AnyAction<S>[];
1895
+ options?: ISprinkleOptions;
1896
+ argv?: string[];
1897
+ }): Promise<void> {
1898
+ const argv = opts.argv ?? process.argv.slice(2);
1899
+ const mode = detectMode(argv);
1900
+
1901
+ if (mode === "tui") {
1902
+ if (!opts.menu) {
1903
+ throw new Error(
1904
+ "TUI mode requires a menu. Provide a menu option or pass --help to see available actions.",
1905
+ );
1906
+ }
1907
+ const sprinkle = await Sprinkle.New(
1908
+ opts.type,
1909
+ opts.storagePath,
1910
+ opts.options,
1911
+ );
1912
+ // Register any provided actions
1913
+ for (const action of opts.actions ?? []) {
1914
+ sprinkle.registerAction(action);
1915
+ }
1916
+ await sprinkle.showMenu(opts.menu);
1917
+ return;
1918
+ }
1919
+
1920
+ if (mode === "help") {
1921
+ const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
1922
+ for (const action of opts.actions ?? []) {
1923
+ sprinkle.registerAction(action);
1924
+ }
1925
+ console.log(generateAppHelp(sprinkle.listActions()));
1926
+ return;
1927
+ }
1928
+
1929
+ if (mode === "cli") {
1930
+ const { actionName, args } = parseCliArgs(argv);
1931
+
1932
+ // Extract --profile flag from args if present
1933
+ const profileName =
1934
+ typeof args["profile"] === "string" ? args["profile"] : undefined;
1935
+
1936
+ const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
1937
+ for (const action of opts.actions ?? []) {
1938
+ sprinkle.registerAction(action);
1939
+ }
1940
+
1941
+ // Handle action-specific --help BEFORE profile initialization
1942
+ // (help should work without a profile)
1943
+ if (args["help"] === true) {
1944
+ const action = sprinkle.getAction(actionName);
1945
+ if (action) {
1946
+ console.log(generateActionHelp(action));
1947
+ } else {
1948
+ console.log(`Unknown action "${actionName}". Run --help to see available actions.`);
1949
+ }
1950
+ return;
1951
+ }
1952
+
1953
+ await sprinkle.initNonInteractive(profileName);
1954
+
1955
+ // Remove internal flags from args before passing to action
1956
+ const actionArgs = { ...args };
1957
+ delete actionArgs["profile"];
1958
+ delete actionArgs["help"];
1959
+
1960
+ // Build context and delegate to runCli for proper error routing
1961
+ const context = {
1962
+ sprinkle,
1963
+ settings: sprinkle.settings,
1964
+ };
1965
+ await runCli(sprinkle, actionName, actionArgs, context);
1966
+ return;
1967
+ }
1968
+
1969
+ if (mode === "mcp") {
1970
+ // Derive a server name from the storage path basename, falling back to a
1971
+ // sensible default.
1972
+ const serverName = path.basename(opts.storagePath) || "sprinkle-mcp";
1973
+
1974
+ const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
1975
+ for (const action of opts.actions ?? []) {
1976
+ sprinkle.registerAction(action);
1977
+ }
1978
+
1979
+ // Read profile name from environment variable (MCP clients have no
1980
+ // interactive terminal to prompt for profile selection).
1981
+ const profileName = process.env["SPRINKLE_PROFILE"];
1982
+
1983
+ await sprinkle.initNonInteractive(profileName);
1984
+ await runMcp(sprinkle, serverName);
1985
+ return;
1986
+ }
1987
+ }
1379
1988
  }