infaira-canvas 0.1.10 → 0.3.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 CHANGED
@@ -11,6 +11,13 @@ CLI toolkit for building and publishing widgets for the **InfAIra Canvas (ICan)*
11
11
  1. **Scaffold** a new widget project with all required files
12
12
  2. **Upload** a built widget bundle to an ICan portal
13
13
 
14
+ Widgets scaffolded with this CLI can call:
15
+
16
+ - **ICan native actions** — `icanContext.executeAction('Buildings', 'List', { siteId })`
17
+ - **Mythos workflow actions** — `icanContext.executeAction('mythos:Inventory', 'UpdateStock', { sku, qty })`
18
+
19
+ The only difference at the call site is the `mythos:` prefix on the model name. The ICan backend handles routing (set `MYTHOS_BASE_URL` on the backend to enable). See **section 2a** of the bundled `ICan-Widget-Development-Guide.md` for the full Mythos integration walkthrough.
20
+
14
21
  ---
15
22
 
16
23
  ## Installation
@@ -288,20 +288,57 @@ import LocalizationMessages from '../localization.json';
288
288
  // ─── Types ────────────────────────────────────────────────────────────────────
289
289
 
290
290
  export interface IActionOptions {
291
+ /** Parse response body as JSON before resolving. */
291
292
  json?: boolean;
293
+ /** Stable identifier for cancellation. Required when cancelPrevious is set. */
292
294
  key?: string;
295
+ /** When true and "key" is also set, any prior in-flight call sharing the
296
+ * same key is cancelled (its Promise resolves to null). Use for live-filter
297
+ * widgets where only the latest call result should land in state. */
293
298
  cancelPrevious?: boolean;
299
+ /** Skip the 100ms batch window and fire immediately. */
294
300
  executeImmediately?: boolean;
295
301
  }
296
302
 
303
+ /** A Mythos action published and callable from this widget. Returned by
304
+ * IContextProvider.listMythosActions. Invoke one via
305
+ * executeAction('mythos:' + model_name, action_name, params). */
306
+ export interface IMythosAction {
307
+ model_id: string;
308
+ model_name: string;
309
+ action_id: string;
310
+ action_name: string;
311
+ description?: string;
312
+ capability?: string;
313
+ }
314
+
297
315
  export interface IContextProvider {
298
316
  environment: 'dev' | 'prod';
299
317
  orchUrl?: string;
300
318
  userKey: string;
301
319
  root: string;
302
320
  scriptFiles?: string[];
321
+ /** Invoke a model action.
322
+ * - executeAction('Buildings', 'List', { siteId }) → ICan native
323
+ * - executeAction('mythos:Inventory', 'UpdateStock', p) → Mythos engine
324
+ * The 'mythos:' prefix is the only difference at the call site; the
325
+ * ICan backend routes the call appropriately.
326
+ * options.cancelPrevious + options.key: when set, an in-flight call with
327
+ * the same key is cancelled (resolves null) and the new call wins. */
303
328
  executeAction(model: string, action: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
304
329
  executeService(app: string, service: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
330
+ /** List Mythos actions callable via executeAction('mythos:...', ...).
331
+ * Optional — present only when MYTHOS_BASE_URL is configured on the ICan
332
+ * backend. Treat absence as "Mythos integration disabled". */
333
+ listMythosActions?(): Promise<IMythosAction[]>;
334
+ /** Fire a Mythos external event with an optional payload. */
335
+ fireMythosEvent?(eventType: string, payload?: Record<string, unknown>): Promise<void>;
336
+ /** Build an IDataFunction over a Mythos collection for DataList/DataTable. */
337
+ fromMythosCollection?(model: string, collection: string): (
338
+ max: number,
339
+ lastPageToken: string,
340
+ args?: { search?: string } & Record<string, unknown>,
341
+ ) => Promise<{ items: Array<Record<string, unknown>>; pageToken: string }>;
305
342
  fireEvent(eventId: string): Promise<void>;
306
343
  hasAppRole(app: string, role: string): boolean;
307
344
  themeName?: string;
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "infaira-canvas",
3
- "version": "0.1.10",
4
- "description": "InfAIra Canvas CLI — Widget development toolkit for InfAIra Canvas",
3
+ "version": "0.3.0",
4
+ "description": "InfAIra Canvas CLI — scaffold widgets that talk to ICan and Mythos (actions, events, public portals, paginated collections).",
5
5
  "keywords": [
6
6
  "infaira",
7
7
  "ican",
8
+ "mythos",
8
9
  "widget",
9
10
  "cli",
10
- "scaffold"
11
+ "scaffold",
12
+ "workflow"
11
13
  ],
12
14
  "homepage": "https://infaira.co",
13
15
  "license": "MIT",
@@ -38,7 +38,10 @@ In the portal, `icanContext` is always defined — the widget is never mounted u
38
38
  | `language` | `string` | Active language code, e.g. `'en'` |
39
39
  | `themeName` | `string \| undefined` | e.g. `'Dark'`, `'Glass Light'` |
40
40
  | `themeType` | `'Dark' \| 'Light' \| 'Glass-Dark' \| 'Glass-Light' \| undefined` | Structured theme type for chart colours |
41
- | `executeAction` | `(model, action, params?, options?) => Promise<unknown>` | Call an Orch model |
41
+ | `executeAction` | `(model, action, params?, options?) => Promise<unknown>` | Call an action. ICan native by default; prefix `model` with `mythos:` to route to the Mythos engine instead. `options.cancelPrevious` + `options.key` to deduplicate live-filter calls. |
42
+ | `listMythosActions` | `() => Promise<IMythosAction[]> \| undefined` | List Mythos actions callable from this widget. Present only when the ICan backend has `MYTHOS_BASE_URL` configured. |
43
+ | `fireMythosEvent` | `(event_type, payload?) => Promise<void> \| undefined` | Fire a named external event into the Mythos bus. Triggers any matching Mythos event handlers. |
44
+ | `fromMythosCollection` | `(model, collection) => IDataFunction \| undefined` | Build a paginated data source over a Mythos collection — drops into DataList / DataTable. |
42
45
  | `fireEvent` | `(eventId) => Promise<void>` | Trigger a portal event |
43
46
  | `hasAppRole` | `(app, role) => boolean` | Check if the user has a role |
44
47
  | `$L` | `(code, params?) => string` | Translate a localisation key |
@@ -73,6 +76,114 @@ The dev harness always sets `environment: 'dev'`. The portal always sets `enviro
73
76
 
74
77
  ---
75
78
 
79
+ ## 2a. Calling Mythos actions — the `mythos:` prefix
80
+
81
+ ICan widgets can invoke actions running in the Mythos workflow engine through the exact same `executeAction` call, just by prefixing the `model` argument with `mythos:`. The host backend inspects the prefix and routes the request to the Mythos engine via the shared JWT.
82
+
83
+ ```tsx
84
+ // 1. Discover what's available (call once at mount and cache)
85
+ const actions = await icanContext.listMythosActions?.();
86
+ // → [{ model_id, model_name: 'Inventory', action_id, action_name: 'UpdateStock', description }]
87
+
88
+ // 2. Invoke a Mythos action — only the `mythos:` prefix differs from a
89
+ // native ICan call
90
+ const result = await icanContext.executeAction(
91
+ 'mythos:Inventory', // <- the prefix is what triggers Mythos routing
92
+ 'UpdateStock',
93
+ { sku: 'ABC-123', qty: 5 },
94
+ );
95
+ // → { run_id: '...', status: 'ok', output?: {...} }
96
+ ```
97
+
98
+ ### When to use Mythos vs native
99
+
100
+ | Use Mythos when… | Use native ICan when… |
101
+ |---|---|
102
+ | The logic is a multi-step workflow with branches, retries, or external calls | The call is a direct read/write to a backing service (Locations, Assets, etc.) |
103
+ | You want the call recorded in Mythos's execution log + debugger | You don't need step-level tracing |
104
+ | Multiple widgets need to call the same business operation | The operation is widget-local |
105
+ | You want to publish the same logic over a REST endpoint too | The widget owns the orchestration |
106
+
107
+ ### Failure modes to handle
108
+
109
+ `executeAction('mythos:...')` can reject with:
110
+
111
+ - **`mythos integration not configured`** — the ICan backend doesn't have `MYTHOS_BASE_URL` set. Surface this to the user and fall back gracefully.
112
+ - **`mythos model "X" not found or not published`** — the action either doesn't exist or isn't toggled `published: true` in Mythos.
113
+ - **HTTP / network errors** — same as any `executeAction` call.
114
+
115
+ `listMythosActions` is also `undefined` when integration is disabled — so guard with `?.()`.
116
+
117
+ ```tsx
118
+ const handleRun = async () => {
119
+ try {
120
+ const r = await icanContext.executeAction('mythos:Inventory', 'UpdateStock', { sku, qty });
121
+ setResult(r);
122
+ } catch (e) {
123
+ setError(e instanceof Error ? e.message : 'Failed');
124
+ }
125
+ };
126
+ ```
127
+
128
+ A working reference widget lives at `Widgets/ICan widgets/mythosdemo/` — pick an action, fill params, run, render the response.
129
+
130
+ ### Cancelling stale calls (`options.cancelPrevious`)
131
+
132
+ Widgets that fire on every keystroke — search bars, live filters — end up racing requests against themselves. Use `cancelPrevious` + `key` to ensure only the latest call's result lands:
133
+
134
+ ```tsx
135
+ const onSearch = (text: string) => {
136
+ icanContext.executeAction(
137
+ 'mythos:Inventory',
138
+ 'Search',
139
+ { q: text },
140
+ { cancelPrevious: true, key: 'inventory-search' },
141
+ ).then((res) => {
142
+ if (res !== null) setResults(res); // null means this call was cancelled
143
+ });
144
+ };
145
+ ```
146
+
147
+ Calls with the same `key` are tracked; when a newer one is issued, older ones resolve to `null` instead of delivering stale data into your state.
148
+
149
+ ### Firing Mythos events
150
+
151
+ `icanContext.fireMythosEvent('order_placed', { id: 4821 })` publishes the event into Mythos's bus. Any Mythos workflow with a matching External Event trigger fires. Useful for widget → workflow dispatch where you don't need a return value.
152
+
153
+ ```tsx
154
+ await icanContext.fireMythosEvent?.('user_logged_in', { userId });
155
+ ```
156
+
157
+ Optional — `fireMythosEvent` is `undefined` when Mythos integration is disabled. Guard with `?.()`.
158
+
159
+ ### Reading from a Mythos collection
160
+
161
+ `icanContext.fromMythosCollection(model, collection)` returns a function with the same shape ICan's DataList / DataTable already accept:
162
+
163
+ ```tsx
164
+ const fetchPage = icanContext.fromMythosCollection!('Inventory', 'items');
165
+
166
+ <DataList
167
+ dataFunction={fetchPage}
168
+ pageSize={50}
169
+ renderItem={(item) => <div>{item.sku} — {item.qty}</div>}
170
+ />
171
+ ```
172
+
173
+ The function takes `(max, lastPageToken, args?)` and returns `{ items, pageToken }`. Empty `pageToken` means "no more pages". Supports `args.search` for text filtering against the collection's searchable fields.
174
+
175
+ ### Public portals (anonymous Mythos access)
176
+
177
+ When a widget runs on a `is_public: true` portal, the host calls `/api/public/execute-batch` instead of the authenticated endpoint. For `mythos:` calls in that batch:
178
+
179
+ - The Mythos action **must** have `no_auth: true` set in its API Route config
180
+ - The call is forwarded to Mythos's public route `/api/<scope>/models/{name}/actions/{name}` with no JWT
181
+ - Mythos enforces `no_auth` at its middleware layer — actions without that flag return `401` to the widget
182
+
183
+ No code change in the widget itself — the same `executeAction('mythos:X', 'Y', params)` call works in both authenticated and public portals.
184
+
185
+ ---
186
+
76
187
  ## 3. Widget settings panel — `IWidgetPropConfig`
77
188
 
78
189
  Widgets can expose a settings form in the portal. Users open it by clicking the ⚙ button on a widget cell in edit mode. You define the fields in `registerWidget`:
@@ -594,7 +594,10 @@ interface IContextProvider {
594
594
  orchUrl?: string;
595
595
  apiKey?: string;
596
596
 
597
- // Fetch data from Orch
597
+ // Invoke an action. Prefix model with `mythos:` to route to the Mythos
598
+ // workflow engine instead of the default ICan native pipeline:
599
+ // executeAction('Buildings', 'List', { siteId }) → ICan native
600
+ // executeAction('mythos:Inventory', 'UpdateStock', { ... }) → Mythos engine
598
601
  executeAction(
599
602
  model: string,
600
603
  action: string,
@@ -602,6 +605,16 @@ interface IContextProvider {
602
605
  options?: IActionOptions
603
606
  ): Promise<unknown>;
604
607
 
608
+ // List Mythos actions callable via executeAction('mythos:...', ...).
609
+ // Optional — present only when MYTHOS_BASE_URL is set on the ICan backend.
610
+ listMythosActions?(): Promise<Array<{
611
+ model_id: string;
612
+ model_name: string;
613
+ action_id: string;
614
+ action_name: string;
615
+ description?: string;
616
+ }>>;
617
+
605
618
  executeService(
606
619
  app: string,
607
620
  service: string,
@@ -701,17 +701,81 @@ declare module "ican/context" {
701
701
  email?: string;
702
702
  roles?: string[];
703
703
  };
704
- /** Invoke a registered model action on this dashboard. */
704
+ /**
705
+ * Invoke a model action on this dashboard.
706
+ *
707
+ * Routing happens server-side by inspecting `model`:
708
+ * - `executeAction('Buildings', 'List', { siteId })`
709
+ * → ICan native action (existing Orch pipeline).
710
+ * - `executeAction('mythos:Inventory', 'UpdateStock', { sku, qty })`
711
+ * → routed to the Mythos engine via the `mythos:` prefix. Resolves
712
+ * to the published Mythos action with that (model_name, action_name)
713
+ * and POSTs to `/api/models/{id}/actions/{id}/run`.
714
+ *
715
+ * Both call shapes return a Promise that resolves to the action's output
716
+ * envelope. For Mythos, the envelope is `{ run_id, status, output? }`.
717
+ *
718
+ * `options.cancelPrevious` + `options.key`: when an in-flight call shares
719
+ * the same key, its Promise resolves to `null` and the newer call wins.
720
+ * Designed for live-filter widgets where only the latest result matters.
721
+ */
705
722
  executeAction(
706
723
  model: string,
707
724
  action: string,
708
- params?: Record<string, unknown>
725
+ params?: Record<string, unknown>,
726
+ options?: { cancelPrevious?: boolean; key?: string }
709
727
  ): Promise<unknown>;
728
+ /**
729
+ * List every Mythos action published and callable from this widget.
730
+ * Optional — present only when the ICan backend has MYTHOS_BASE_URL set.
731
+ * Widgets that build action pickers (settings panels, etc.) call this
732
+ * once at mount and cache the result themselves.
733
+ */
734
+ listMythosActions?: () => Promise<IMythosAction[]>;
735
+ /**
736
+ * Fire an external event into the Mythos engine. The event_type matches
737
+ * what Mythos event-handlers subscribe to (configured in the model's
738
+ * Events tab). Mirrors UXP's fireEvent, with an optional payload.
739
+ * Optional — present only when MYTHOS_BASE_URL is configured.
740
+ */
741
+ fireMythosEvent?: (
742
+ eventType: string,
743
+ payload?: Record<string, unknown>
744
+ ) => Promise<void>;
745
+ /**
746
+ * Build an IDataFunction over a Mythos collection — drop-in data source
747
+ * for DataList / DataTable. Mirrors UXP's fromLucyDataCollection.
748
+ * const fn = icanContext.fromMythosCollection('Inventory', 'items')
749
+ * const { items, pageToken } = await fn(50, '', { search: 'foo' })
750
+ * Optional — present only when MYTHOS_BASE_URL is configured.
751
+ */
752
+ fromMythosCollection?: (
753
+ model: string,
754
+ collection: string
755
+ ) => (
756
+ max: number,
757
+ lastPageToken: string,
758
+ args?: { search?: string } & Record<string, unknown>
759
+ ) => Promise<{ items: Array<Record<string, unknown>>; pageToken: string }>;
710
760
  /** Resolve a relative ICan backend path against the portal base URL. */
711
761
  resolveUrl(path: string): string;
712
762
  /** Logging helper namespaced to this widget instance. */
713
763
  log(level: "info" | "warn" | "error", ...args: unknown[]): void;
714
764
  }
765
+
766
+ /**
767
+ * A single Mythos action published as callable from a widget. Returned by
768
+ * `icanContext.listMythosActions()`. To invoke one, call
769
+ * `icanContext.executeAction('mythos:' + model_name, action_name, params)`.
770
+ */
771
+ export interface IMythosAction {
772
+ model_id: string;
773
+ model_name: string;
774
+ action_id: string;
775
+ action_name: string;
776
+ description?: string;
777
+ capability?: string;
778
+ }
715
779
  }
716
780
 
717
781
  // ─── Global runtime declarations ─────────────────────────────────────────────
@@ -1419,13 +1419,46 @@
1419
1419
  orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1420
1420
  apiKey: localStorage.getItem(LS_API_KEY) || '',
1421
1421
  executeAction: function (model, action, params) {
1422
- console.log('[ICan Dev] executeAction:', model, action, params);
1422
+ // Mythos-routed calls use the `mythos:<ModelName>` prefix in production.
1423
+ // In the dev harness we just log them distinctively so authors can see
1424
+ // whether their widget is targeting ICan native or Mythos.
1425
+ var isMythos = typeof model === 'string' && model.toLowerCase().indexOf('mythos:') === 0;
1426
+ console.log('[ICan Dev]' + (isMythos ? ' [Mythos]' : '') + ' executeAction:', model, action, params);
1427
+ if (isMythos) {
1428
+ // Mock the Mythos response envelope: { run_id, status, output }.
1429
+ return Promise.resolve({ run_id: 'dev-' + Date.now(), status: 'ok', output: {} });
1430
+ }
1423
1431
  return Promise.resolve([]);
1424
1432
  },
1425
1433
  executeService: function (app, service, params) {
1426
1434
  console.log('[ICan Dev] executeService:', app, service, params);
1427
1435
  return Promise.resolve(null);
1428
1436
  },
1437
+ listMythosActions: function () {
1438
+ // Dev stub — returns a couple of synthetic actions so widgets that
1439
+ // populate dropdowns from discovery don't render empty.
1440
+ console.log('[ICan Dev] listMythosActions');
1441
+ return Promise.resolve([
1442
+ { model_id: 'dev-model-1', model_name: 'Inventory', action_id: 'dev-action-1', action_name: 'UpdateStock', description: 'Dev stub' },
1443
+ { model_id: 'dev-model-2', model_name: 'Buildings', action_id: 'dev-action-2', action_name: 'List', description: 'Dev stub' },
1444
+ ]);
1445
+ },
1446
+ fireMythosEvent: function (eventType, payload) {
1447
+ console.log('[ICan Dev] fireMythosEvent:', eventType, payload);
1448
+ return Promise.resolve();
1449
+ },
1450
+ fromMythosCollection: function (model, collection) {
1451
+ return function (max, lastPageToken, args) {
1452
+ console.log('[ICan Dev] fromMythosCollection:', model, collection, { max: max, lastPageToken: lastPageToken, args: args });
1453
+ // Return one page of synthetic docs so DataList renders something.
1454
+ return Promise.resolve({
1455
+ items: Array.from({ length: Math.min(max, 5) }, function (_, i) {
1456
+ return { id: 'dev-' + i, name: model + ' row ' + i, value: i * 10 };
1457
+ }),
1458
+ pageToken: '',
1459
+ });
1460
+ };
1461
+ },
1429
1462
  fireEvent: function (id) {
1430
1463
  console.log('[ICan Dev] fireEvent:', id);
1431
1464
  return Promise.resolve();
package/templates/ui.html CHANGED
@@ -1123,13 +1123,40 @@
1123
1123
  orchUrl: localStorage.getItem(LS_ORCH_URL) || '',
1124
1124
  apiKey: localStorage.getItem(LS_API_KEY) || '',
1125
1125
  executeAction: function (model, action, params) {
1126
- console.log('[ICan Dev] executeAction:', model, action, params);
1126
+ // Mythos-routed calls use the `mythos:<ModelName>` prefix.
1127
+ var isMythos = typeof model === 'string' && model.toLowerCase().indexOf('mythos:') === 0;
1128
+ console.log('[ICan Dev]' + (isMythos ? ' [Mythos]' : '') + ' executeAction:', model, action, params);
1129
+ if (isMythos) {
1130
+ return Promise.resolve({ run_id: 'dev-' + Date.now(), status: 'ok', output: {} });
1131
+ }
1127
1132
  return Promise.resolve([]);
1128
1133
  },
1129
1134
  executeService: function (app, service, params) {
1130
1135
  console.log('[ICan Dev] executeService:', app, service, params);
1131
1136
  return Promise.resolve(null);
1132
1137
  },
1138
+ listMythosActions: function () {
1139
+ console.log('[ICan Dev] listMythosActions');
1140
+ return Promise.resolve([
1141
+ { model_id: 'dev-model-1', model_name: 'Inventory', action_id: 'dev-action-1', action_name: 'UpdateStock', description: 'Dev stub' },
1142
+ { model_id: 'dev-model-2', model_name: 'Buildings', action_id: 'dev-action-2', action_name: 'List', description: 'Dev stub' },
1143
+ ]);
1144
+ },
1145
+ fireMythosEvent: function (eventType, payload) {
1146
+ console.log('[ICan Dev] fireMythosEvent:', eventType, payload);
1147
+ return Promise.resolve();
1148
+ },
1149
+ fromMythosCollection: function (model, collection) {
1150
+ return function (max, lastPageToken, args) {
1151
+ console.log('[ICan Dev] fromMythosCollection:', model, collection, { max: max, lastPageToken: lastPageToken, args: args });
1152
+ return Promise.resolve({
1153
+ items: Array.from({ length: Math.min(max, 5) }, function (_, i) {
1154
+ return { id: 'dev-' + i, name: model + ' row ' + i, value: i * 10 };
1155
+ }),
1156
+ pageToken: '',
1157
+ });
1158
+ };
1159
+ },
1133
1160
  fireEvent: function (id) {
1134
1161
  console.log('[ICan Dev] fireEvent:', id);
1135
1162
  return Promise.resolve();