@xpert-ai/plugin-live-dashboard 0.2.0 → 0.3.1

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.
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "sources": {
3
- ".xpertai-plugin/plugin.json": "8b4dae57dad788202e1df7d3201b0fed111f2a3546859cce5f1a9fbcc77caf56",
4
- "package.json": "0ff6c6f2749692dd7cfaf449987a741ccd2fea2c481b194beeaeb942781df570",
5
- "scripts/build-action.mjs": "283f26ef3c1c2ddf7f3f2bfc7a7780229b4d7061f1e3f1b0f222d012b78583de",
3
+ ".xpertai-plugin/plugin.json": "1ee877449129cba345af770103c31745eb162df370f9502effd22adfeddf3e77",
4
+ "package.json": "45298d9c5530d07385f7f6724631524069b3a0efe7103e8aec89fb465892a15a",
5
+ "scripts/build-action.mjs": "3a3de4d7fd05d81f5fdeb9eacbd04235dcc6e5000c6fff2d07f0e4bdc7b40c30",
6
6
  "scripts/build-libraries.mjs": "20bd34ca3f8bb6e87df2637a3cca85496962688c8ea4ceff5bc294ebd2de704d",
7
7
  "scripts/build-remote.mjs": "492550ac5c95eb371253d863567723d8853522013a0bcdfa0e264720206fda16",
8
8
  "scripts/dist-manifest.mjs": "8dcca9f1e74c520171649e848f9b5570ca9cc83fb5e8d2f265be3d2e5ceb970c",
9
+ "scripts/import-prompts.mjs": "c8c611fec92e168f63487a09ce0509d6ccef80d0b6d2c44ecf23da63e782a05d",
9
10
  "scripts/renderer-asset.mjs": "e7edf2fb41f2384e838c27c2e5566f5d479135ad365ae2a99fb421433464d82a",
10
11
  "scripts/verify-dist.mjs": "523d0fe9703a0f3d12099173d115b5145d5665b06551cfab2869a2db76d880e2",
11
12
  "skills/live-dashboard/SKILL.md": "1825876a74391c2c514548b7a1b37e123d044952f9c882498dd5225950f169d0",
@@ -13,9 +14,10 @@
13
14
  "skills/live-dashboard/references/data-validation.md": "4455240470b3456fcd7db8f0f58b040d1dceb548010414542dd5cc86f3abfe63",
14
15
  "skills/live-dashboard/references/html-lifecycle.md": "4bf88730709ab45166425e1917610d1e618c9a0883e615ce76c0fe55488ffd51",
15
16
  "skills/live-dashboard/references/html-runtime.md": "5be3dc50e618b30e83ef59c2d684ceed5a3e37d63a550bdf157b0d8953af5d38",
16
- "src/index.ts": "6aab2d46788fe6de8ed2671b2d1de4f0b1e4d1f2287fd3ec8b14a1610ebf0426",
17
+ "src/index.ts": "26af7bca762bda113956697f7928971ccf694c80c4c4596f06a60ddabe5cdf86",
17
18
  "src/lib/agent.schema.ts": "1444585eab3618b985b774937309d950df99de1a1804e4caecdff8026de70297",
18
19
  "src/lib/analytics-source.ts": "6fb5fd394fffa5a0c12f1fe32d0c9d7dbd8597052047b6122fc5b160fa7af54f",
20
+ "src/lib/assistant-template.ts": "6be01da06edeff18a79c724a708396f9bd9fe7d8944c31e4f4f71db32c0b8d92",
19
21
  "src/lib/content-capability.ts": "fa1252a539a681be9780d318b2b3710da273c4405c8e21d58ac675294cef3836",
20
22
  "src/lib/content.provider.ts": "3cfbccec1483269c150ba3dade7aa812ee59c998c30d89a57835977027c7e622",
21
23
  "src/lib/dashboard-icon.ts": "3f95a626e91d3ce198cdef6b05365591b6e65af4f58d6f5765ec0836a590a494",
@@ -24,9 +26,9 @@
24
26
  "src/lib/dashboard-receipt.ts": "d02b7e4e1be0a075b7f27e887ebb555684f17036ebb9442982e6725ab37a53ab",
25
27
  "src/lib/dashboard-search.ts": "0cd14c0004fb078b54c095faeab3195fb7e03a20c3400b5e67aec7cb4b2442e2",
26
28
  "src/lib/dashboard-sharing.ts": "61199f135cc53b6b9f0bef0f75f14c4d40bff6a3986e62e2823ab80c564534d3",
27
- "src/lib/dashboard-view.schema.ts": "11562ce17c573611eedd5ab5d0db378c6ae676955c1b3f8606d3e60b367ed05e",
29
+ "src/lib/dashboard-view.schema.ts": "4db0f043e836a1e050e8621074e5370e00507f5c6862ab9e2d624001c0452f26",
28
30
  "src/lib/dashboard.schema.ts": "982e15591e70102ea506951cd10de9ec4864281157b0b95a7b591a8fff3da813",
29
- "src/lib/dashboard.service.ts": "2a404e66ec95877ab37b55ce05dd024049c9d556e52a1a780bedcc45c425ac31",
31
+ "src/lib/dashboard.service.ts": "8b61384e2e655821dcbd427f3e393c51b90d5d023ee50e078f1038617ef26dcb",
30
32
  "src/lib/definition-tools.ts": "ba48842396e57499331a52ca7cf5e4e1eb72e1e519b5dabfff2743fddf15947d",
31
33
  "src/lib/document-host.ts": "3543ddb443006b90cd4689b65446b0e3b3305362d3d24d93209c094967921ffe",
32
34
  "src/lib/document-libraries.ts": "9c228995c509dc7f606cdc70d972ddb22df0164df4e0070583d0ab901f6605d0",
@@ -36,24 +38,26 @@
36
38
  "src/lib/filter-evidence.ts": "594ea9065e5c8d32c6bd4fc055e78c2df7ad2418c2019106136917c89c8ac331",
37
39
  "src/lib/html-guidance.ts": "0bd0b830b33045357a070c2079b2aedaadb57807ba366be56b767b7d55120325",
38
40
  "src/lib/html-validation.ts": "9f342f6716d457ba1a46c9d228f3f7858dcae2c0f64a3c98bc1d28d35a8f43ac",
39
- "src/lib/middleware.ts": "d1173e3b058778fb6e569f861ddec1ffc3c36a4fb46df71b4652bfc8fc74115a",
40
- "src/lib/plugin.ts": "c2788944df7697802350cde7fcc292a49b433b5176c7f8eec6a9809fbdf4ced7",
41
+ "src/lib/middleware.ts": "8f9e05fa6d44e46e89d050c441fc867c2dbdbcc037df2ac5aaf8f2bed9343e40",
42
+ "src/lib/plugin.ts": "715f0ce8e1c8e6b32a778d418c5c7cd1f7f516f1a47ccb8437ebe1342ea1f9e8",
41
43
  "src/lib/presentation.provider.ts": "3b37387636d9e7317ee6e36c9e607fadb20002c08c24d54e424342e913b2405f",
42
44
  "src/lib/preview-retry.ts": "55fa28cc61f11adac4dce5a2066cbdd6d529ffd5ffd3cca7dcb552c6b7299500",
45
+ "src/lib/prompt-workflows.ts": "c2aa959f7fad508415394bc0ce6e19ec48f27b82fa04105e6dc2dfd30b7bfe25",
46
+ "src/lib/provider-options.controller.ts": "0b329686f75182ec6a6db2bb741bedd3ade0e7a25093ddbb7b15336282270349",
43
47
  "src/lib/remote-entry.ts": "4c760963ff099b4d9a8961c933b1a586c0ac57d85a88c745c2578e2f66bf1539",
44
48
  "src/lib/renderer.ts": "418ac7cc5f37f9d638c0b2dd3a5361464674f21dbfb801b4414a9d68f1c9edc8",
45
49
  "src/lib/resource-card.ts": "d64f91e640a3db220b941adef4a582e011a7aea253d57cc5d70a5bdcdacd8f71",
46
50
  "src/lib/save-evidence.ts": "458f8fe2ef74fd4809fb9543d8c31bb42e0e16b10550b2a7808ca92ec3662bda",
47
51
  "src/lib/sharing.service.ts": "e52c98d967f27a2f3bc80614f31da15e373dd4450b80aea250156efe24c77f3b",
48
- "src/lib/view.provider.ts": "ba4087458334cbf71a256232695f1b56858926200f9430d79e95d5ac71fe1a26",
49
- "src/remote/bridge.ts": "c191bc1876e11b44397b0fb41f4a0ecd7842b9a43dbc5eeddfb05ee21df72421",
50
- "src/remote/dashboard-picker.tsx": "c84c147aeaa0774bd3f2bcd8b9502be4a78111167b01867e167c9cd72d263800",
52
+ "src/lib/view.provider.ts": "5bafa599cb1d618fc8256db0eb3360533f888b2cd63cd012cdd3ae45edc4da41",
53
+ "src/remote/bridge.ts": "6c213ac74daf8efee87100a94396fb13728027f23cfc189e65c524985235aea7",
54
+ "src/remote/dashboard-picker.tsx": "ffd5ddf54469a439bcdbabe974c0764de967a1f2716ea1304625fb6c54f67461",
51
55
  "src/remote/html-dashboard.tsx": "3e7eba0f8f760cd5334735a790278eea51beaa33ad9ff36a31b560bf7a0ed20d",
52
56
  "src/remote/i18n.ts": "65bb7941903e0a0e6445836cdf25f22e30d875d709fb07d60ac2d88d591b7218",
53
- "src/remote/locales/en.json": "f5090ccb4f8117082d79bec2da7d508b50602151da89f5867eb22008a419a9bf",
54
- "src/remote/locales/zh-Hans.json": "8df4134a625b2f1fa07c73834bc208f451dab40b0a73200bffaf025b55c51b41",
55
- "src/remote/locales/zh-Hant.json": "290c2f16c915a2c648ef58b5817d4cd2b007a88ab8ce809b11dd1bb2580807d9",
56
- "src/remote/main.tsx": "4c9e9ac143e8508558f5721fd9f5574a7fb6ecdda5b5b4469702d3a95b90f669",
57
+ "src/remote/locales/en.json": "8564f0f61fa0e4739e7ee2d55a23b7ff23cbfd40a8a18eeb2c32bdf15abe9a2e",
58
+ "src/remote/locales/zh-Hans.json": "5060899f01c9da602d0de4a84548de0115246c75f2a1e2f690beefa0f44f9913",
59
+ "src/remote/locales/zh-Hant.json": "123c66ae7f1b4ff4cb0001e225164193b0eb3a5069d0768879284ee96a926855",
60
+ "src/remote/main.tsx": "33ec783a2087a37126aca5792bdbb95bd656cecd034674e7c5bf29175d218125",
57
61
  "src/remote/share-dialog.tsx": "f4a78a3bbf99d49c84e10631be9a9bcd3dc11b42130ce79e00010e53fbc2af84",
58
62
  "src/remote/style.css": "8491fc393bacecd73cff2295526769cf73495d7842ca1ce57fb1abd0b7b5b60f",
59
63
  "src/remote/viewer-controls.tsx": "b55e27a8a7932575a994fe30645b535c3dd5ba7e27df1f18a1f86462a6354ca3",
@@ -61,8 +65,8 @@
61
65
  "tsconfig.remote.json": "e0b173f885c7439dfce09d411ff97fb7511bd77f6f1d7fdd66b5adb477a1f411"
62
66
  },
63
67
  "outputs": {
64
- "app.js": "fef3c4052f17a6629c6c5c57bef6daab106172e29677c4c4fb9333966406b5d6",
65
- "app.css": "418aaf0742026be83a2abb385204341b9726db63a1944c936d2c8d94263f0a0b",
68
+ "app.js": "06a6ef10aa3a96cb333c6a7772a8cbf03571e1edf0aba34b92ec8024420d7cfa",
69
+ "app.css": "6e5081fc763445d24f2c61f9eb0c3eea53b7e392001ee54b54d1a34424c77d3c",
66
70
  "renderer.html": "30abe65cd0d32a7596e02c5e94f2e20879fa73644507fefa35c7119a018ff7d2"
67
71
  },
68
72
  "libraries": {
@@ -6,5 +6,5 @@
6
6
  "playwrightVersion": "1.61.0",
7
7
  "bundle": "./bundle",
8
8
  "entrypoint": "runner.mjs",
9
- "bundleSha256": "de79ec597b5b7984c7f6cbd84832881bd5fccc32d4dc84e012eae6c090e8f539"
9
+ "bundleSha256": "81ed423dae0f11964c5c48ddec4cd64b1a72c202fbef9c19507a13cffff0e870"
10
10
  }
@@ -0,0 +1,39 @@
1
+ // Reuse successful static resource bytes only within one validation. Every request
2
+ // still passes the document resource policy; redirects are never fetched out of band.
3
+ export function createResourceCache() {
4
+ const admitted = new WeakSet(), entries = new Map()
5
+ const maxResourceBytes = 8 * 1024 * 1024, maxTotalBytes = 32 * 1024 * 1024
6
+ let totalBytes = 0
7
+ const key = request => `${request.resourceType()}:${request.url().split('#')[0]}`
8
+ return {
9
+ async route(page, route) {
10
+ const request = route.request()
11
+ const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
12
+ DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
13
+ if (!allowed) return route.abort('blockedbyclient')
14
+ admitted.add(request)
15
+ const cached = await entries.get(key(request))
16
+ if (cached) return route.fulfill(cached)
17
+ return route.continue()
18
+ },
19
+ capture(response) {
20
+ const request = response.request(), id = key(request), headers = response.headers()
21
+ if (!admitted.has(request) || response.status() !== 200 || entries.has(id) || entries.size >= 128 ||
22
+ headers['set-cookie'] || /(?:^|,)\s*no-store(?:,|$)/i.test(headers['cache-control'] ?? '') ||
23
+ headers.vary === '*' || Number(headers['content-length']) > maxResourceBytes) return
24
+ const pending = (async () => {
25
+ try {
26
+ const body = await response.body()
27
+ if (body.length > maxResourceBytes || totalBytes + body.length > maxTotalBytes) return
28
+ totalBytes += body.length
29
+ // Playwright exposes decoded bytes; do not replay wire compression/length.
30
+ const replayHeaders = { ...headers }
31
+ for (const name of ['content-encoding', 'content-length', 'transfer-encoding']) delete replayHeaders[name]
32
+ return { status: 200, headers: replayHeaders, body }
33
+ } catch { return undefined } // A failed load is reported by the page, never cached.
34
+ })()
35
+ entries.set(id, pending)
36
+ },
37
+ async settle() { await Promise.all(entries.values()) }
38
+ }
39
+ }
@@ -1,18 +1,58 @@
1
- export async function validateDocument(input, browser, hostPath) {
2
- const errors = [], addError = value => { const message = String(value).slice(0, 300); if (errors.length < 16 && !errors.includes(message)) errors.push(message) }
3
- const loadedResources = new Set()
1
+ // Invariants: the SDK's per-document rate/concurrency limits also apply in preflight.
2
+ // Independent synthetic interactions need fresh mounts, not a larger production quota.
3
+ // Each scenario still validates initial load, its interaction sequence and host refresh.
4
+ import { createResourceCache } from './resource-cache.mjs'
5
+
6
+ export async function validateDocument(input, browser, hostPath, { timeoutMs = 55000 } = {}) {
4
7
  const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, serviceWorkers: 'block' })
8
+ const resources = createResourceCache()
9
+ let timer
10
+ const validation = validateScenarios(input, context, hostPath, resources)
5
11
  try {
12
+ // Leave time for browser/report cleanup within the host's 60-second Action budget.
13
+ return await Promise.race([validation, new Promise(resolve => {
14
+ timer = setTimeout(() => resolve({ ok: false, errors: ['DOCUMENT_PREFLIGHT_TIMEOUT'],
15
+ requestedBindings: [], renderedTextLength: 0 }), timeoutMs)
16
+ })])
17
+ } finally {
18
+ clearTimeout(timer)
19
+ await context.close()
20
+ await validation.catch(() => {}) // Closing the context cancels outstanding Playwright operations.
21
+ }
22
+ }
23
+
24
+ async function validateScenarios(input, context, hostPath, resources) {
25
+ const initial = await validateScenario(input, context, hostPath, resources, { type: 'refresh' })
26
+ if (!initial.report.ok) return initial.report
27
+ const scenarios = [
28
+ ...Array.from({ length: input.parameters ? Math.min(initial.selectCount, 24) : 0 }, (_, index) => ({ type: 'select', index })),
29
+ ...Array.from({ length: Math.min(initial.buttonCount, 12) }, (_, index) => ({ type: 'button', index }))
30
+ ]
31
+ const requestedBindings = new Set(initial.report.requestedBindings)
32
+ // Bound live pages while keeping the full control/button sweep within the Action deadline.
33
+ for (let offset = 0; offset < scenarios.length; offset += 2) {
34
+ const results = await Promise.allSettled(scenarios.slice(offset, offset + 2).map(scenario =>
35
+ validateScenario(input, context, hostPath, resources, scenario)))
36
+ // Settle both pages before closing the context, including on infrastructure errors.
37
+ for (const result of results) {
38
+ if (result.status === 'rejected') throw result.reason
39
+ const { report } = result.value
40
+ report.requestedBindings.forEach(id => requestedBindings.add(id))
41
+ if (!report.ok) return { ...report, requestedBindings: [...requestedBindings] }
42
+ }
43
+ }
44
+ return { ...initial.report, requestedBindings: [...requestedBindings] }
45
+ }
46
+
47
+ async function validateScenario(input, context, hostPath, resources, scenario) {
48
+ const errors = [], addError = value => { const message = String(value).slice(0, 300); if (errors.length < 16 && !errors.includes(message)) errors.push(message) }
49
+ const loadedResources = new Set()
6
50
  const page = await context.newPage()
7
- await page.route('**/*', async route => {
8
- const request = route.request()
9
- const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
10
- DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
11
- if (allowed) await route.continue()
12
- else await route.abort('blockedbyclient')
13
- })
51
+ try {
52
+ await page.route('**/*', route => resources.route(page, route))
14
53
  page.on('requestfailed', request => addError(`DOCUMENT_RESOURCE_LOAD_FAILED: ${request.resourceType()} ${request.url().split('?')[0]} (${request.failure()?.errorText ?? 'request failed'})`))
15
54
  page.on('response', response => {
55
+ resources.capture(response)
16
56
  if (response.status() >= 400) addError(`DOCUMENT_RESOURCE_HTTP_ERROR: ${response.status()} ${response.url().split('?')[0]}`)
17
57
  else if (response.status() < 300 || response.status() === 304) {
18
58
  for (let request = response.request(); request; request = request.redirectedFrom()) loadedResources.add(request.url().split('#')[0])
@@ -79,9 +119,10 @@ const errors = [], addError = value => { const message = String(value).slice(0,
79
119
  }
80
120
  // Exercise bounded native selects against the saved parameter contract; data remains the real default preview.
81
121
  // This detects invalid filter wiring, not the upstream effect of a filter or arbitrary custom controls.
82
- if (frame && input.parameters) {
122
+ if (frame && input.parameters && scenario.type === 'select') {
83
123
  const selects = frame.locator('select')
84
- for (let index = 0; index < Math.min(await selects.count(), 24); index++) {
124
+ if (scenario.index >= await selects.count()) addError('DOCUMENT_SELECT_INTERACTION_FAILED')
125
+ for (let index = scenario.index; index < Math.min(await selects.count(), scenario.index + 1); index++) {
85
126
  const select = selects.nth(index)
86
127
  if (!(await select.isVisible()) || !(await select.isEnabled())) continue
87
128
  const original = await select.inputValue()
@@ -138,14 +179,8 @@ const errors = [], addError = value => { const message = String(value).slice(0,
138
179
  }
139
180
  // Page buttons are untrusted executable UI too. Exercise a bounded set inside
140
181
  // the isolated preview; do not guess which button is refresh from its label.
141
- if (frame) {
142
- const buttons = frame.locator('button')
143
- for (let index = 0; index < Math.min(await buttons.count(), 12); index++) {
144
- const button = buttons.nth(index)
145
- if (!(await button.isVisible()) || !(await button.isEnabled())) continue
146
- await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
147
- await page.waitForTimeout(200)
148
- }
182
+ if (frame && scenario.type === 'button') {
183
+ await exerciseButtonPath(frame, page, scenario.index, addError)
149
184
  }
150
185
  // Exercise the same refresh event as the Workbench. A renderer that fails on its
151
186
  // second Dataset must not pass merely because the first query painted a page.
@@ -156,6 +191,39 @@ const errors = [], addError = value => { const message = String(value).slice(0,
156
191
  const renderedTextLength = frame ? (await frame.locator('body').innerText()).trim().length : 0
157
192
  if (!renderedTextLength) addError('DOCUMENT_EMPTY_PAGE')
158
193
  if (!state.requestedBindings.length) addError('DOCUMENT_INITIAL_QUERY_MISSING')
159
- return { ok: !errors.length, errors, requestedBindings: [...new Set(state.requestedBindings)], renderedTextLength }
160
- } finally { await context.close() }
194
+ await resources.settle()
195
+ return {
196
+ report: { ok: !errors.length, errors, requestedBindings: [...new Set(state.requestedBindings)], renderedTextLength },
197
+ selectCount: frame ? await frame.locator('select').count() : 0,
198
+ buttonCount: frame ? await frame.locator('button').count() : 0
199
+ }
200
+ } finally { await page.close() }
201
+ }
202
+
203
+ async function actionableButtons(frame) {
204
+ const buttons = (await frame.locator('button').elementHandles()).slice(0, 12)
205
+ const actionable = []
206
+ for (const button of buttons)
207
+ if (await button.isVisible() && await button.isEnabled()) actionable.push(button)
208
+ return actionable
209
+ }
210
+
211
+ async function exerciseButtonPath(frame, page, index, addError) {
212
+ const root = (await frame.locator('button').elementHandles())[index]
213
+ if (!root) { addError('DOCUMENT_BUTTON_INTERACTION_FAILED'); return }
214
+ const pending = [root], clicked = []
215
+ while (pending.length) {
216
+ const button = pending.shift()
217
+ if (!(await button.isVisible()) || !(await button.isEnabled()) ||
218
+ await button.evaluate((element, previous) => previous.includes(element), clicked)) continue
219
+ if (clicked.length >= 12) { addError('DOCUMENT_BUTTON_PATH_LIMIT'); return }
220
+ const before = await actionableButtons(frame)
221
+ await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
222
+ clicked.push(button)
223
+ await page.waitForTimeout(200)
224
+ // Follow newly inserted, revealed or enabled controls in this same document.
225
+ // Compare DOM identity, not labels or indexes that can shift after a click.
226
+ for (const next of await actionableButtons(frame))
227
+ if (!await next.evaluate((element, previous) => previous.includes(element), before)) pending.push(next)
228
+ }
161
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xpert-ai/plugin-live-dashboard",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Versioned live dashboards using platform analytics and Data Analytics renderer",
5
5
  "main": "./index.cjs",
6
6
  "types": "./dist/index.d.ts",
@@ -19,7 +19,8 @@
19
19
  "docs",
20
20
  "examples",
21
21
  ".xpertai-plugin",
22
- "skills"
22
+ "skills",
23
+ "scripts/import-prompts.mjs"
23
24
  ],
24
25
  "license": "AGPL-3.0",
25
26
  "private": false,
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module'
3
+ import { realpathSync } from 'node:fs'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { parseArgs } from 'node:util'
6
+
7
+ const require = createRequire(import.meta.url)
8
+ const { dashboardWorkspacePrompts } = require('../dist/lib/prompt-workflows.js')
9
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
10
+
11
+ /** Create-only import: no Assistant edits, broad associations, replacement or automatic publication. */
12
+ export async function importPrompts({ apiUrl, organizationId, assistantId, token, apply = false, request = fetch, log = console.log }) {
13
+ const base = new URL(apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`)
14
+ if (base.username || base.password || base.search || base.hash ||
15
+ !['http:', 'https:'].includes(base.protocol)) throw new Error('Use an HTTP(S) API base URL without credentials, query or fragment.')
16
+ if (base.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))
17
+ throw new Error('Use HTTPS for a remote API. TLS certificate verification must remain enabled.')
18
+ if (!uuid.test(organizationId) || !uuid.test(assistantId)) throw new Error('Organization and Assistant IDs must be UUIDs.')
19
+ const bearer = token?.trim().replace(/^Bearer\s+/i, '')
20
+ if (!bearer) throw new Error('Set XPERT_TOKEN to a platform login token.')
21
+ const headers = { Authorization: `Bearer ${bearer}`, 'organization-id': organizationId, 'Content-Type': 'application/json' }
22
+ async function api(path, body) {
23
+ const response = await request(new URL(path, base), {
24
+ method: body ? 'POST' : 'GET', headers, redirect: 'error', signal: AbortSignal.timeout(30000),
25
+ ...(body ? { body: JSON.stringify(body) } : {})
26
+ })
27
+ if (!response.ok) throw new Error(`API ${body ? 'POST' : 'GET'} failed (${response.status}). No existing data was overwritten. Rerun to inspect partial progress.`)
28
+ return response.json()
29
+ }
30
+ const assistant = await api(`xpert/${assistantId}`)
31
+ if (assistant?.id !== assistantId || !uuid.test(assistant.workspaceId ?? '') ||
32
+ assistant.organizationId !== organizationId || assistant.latest === false)
33
+ throw new Error('The Assistant must be the current version in the selected organization and an authoring workspace.')
34
+ const nodes = assistant.graph?.nodes
35
+ if (!Array.isArray(nodes)) throw new Error('The Assistant has no published graph. Configure and publish LiveDashboard first.')
36
+ const middlewares = nodes.filter(node => node?.type === 'workflow' && node.entity?.type === 'middleware' && node.entity.provider === 'LiveDashboard')
37
+ // Required middleware is always active, so prompts need no stale per-Assistant node keys.
38
+ if (middlewares.length !== 1 || middlewares[0].entity.required !== true)
39
+ throw new Error('Configure one LiveDashboard middleware as required (System middleware), publish, then rerun.')
40
+ async function runtimeCommands() {
41
+ const runtime = await api(`xpert/${assistantId}/runtime-capabilities`)
42
+ if (!Array.isArray(runtime?.commands)) throw new Error('Unexpected runtime command response; import stopped.')
43
+ return runtime.commands
44
+ }
45
+ const commands = await runtimeCommands()
46
+ if (!commands.some(command => command.source?.type === 'middleware' && command.source.provider === 'LiveDashboard' && command.source.nodeKey === middlewares[0].key))
47
+ throw new Error('Published LiveDashboard commands are unavailable. Connect the middleware to the main Agent and load the updated plugin first.')
48
+ function available(workflow, existing, catalog) {
49
+ return catalog.some(command => command.name === workflow.name && command.kind === 'prompt_workflow' &&
50
+ command.source?.type === 'workspace_prompt_workflow' && command.source.workflowId === existing.id &&
51
+ command.action?.type === 'insert_text' && command.action.template === workflow.template)
52
+ }
53
+ const workspace = encodeURIComponent(assistant.workspaceId)
54
+ const creates = []
55
+ const skipped = []
56
+ const conflicts = []
57
+ for (const workflow of dashboardWorkspacePrompts) {
58
+ const data = encodeURIComponent(JSON.stringify({ where: { name: workflow.name }, take: 2 }))
59
+ const page = await api(`prompt-workflow/by-workspace/${workspace}?data=${data}`)
60
+ if (!Array.isArray(page?.items) || page.total !== page.items.length || page.total > 1 ||
61
+ page.items.some(item => item?.name !== workflow.name)) throw new Error('Unexpected prompt lookup result; import stopped.')
62
+ const existing = page.items[0]
63
+ if (existing) {
64
+ const applicable = !existing.associatedXpertIds?.length || existing.associatedXpertIds.includes(assistantId)
65
+ if (existing.archivedAt || existing.actionType === 'insert_invocation' || existing.actionType === 'submit_prompt' ||
66
+ existing.template !== workflow.template || !applicable || !available(workflow, existing, commands)) conflicts.push(workflow.name)
67
+ else skipped.push(workflow.name)
68
+ } else {
69
+ if (commands.some(command => command.name === workflow.name)) conflicts.push(workflow.name)
70
+ creates.push({ ...workflow, associatedXpertIds: [assistantId] })
71
+ }
72
+ }
73
+ if (conflicts.length) throw new Error(`Resolve existing prompts in the workspace prompt library before importing (no writes made): ${[...new Set(conflicts)].join(', ')}. Associate the existing prompt with this Assistant if appropriate; user edits and archived prompts are preserved.`)
74
+ log(`${apply ? 'Apply' : 'Preview'}: create ${creates.length}, already available ${skipped.length}. Assistant ${assistantId}.`)
75
+ const created = []
76
+ const savedWorkflows = []
77
+ if (apply) for (const body of creates) {
78
+ const saved = await api(`prompt-workflow/workspace/${workspace}`, body)
79
+ if (!uuid.test(saved?.id ?? '') || saved.name !== body.name ||
80
+ saved.template !== body.template || !saved.associatedXpertIds?.includes(assistantId))
81
+ throw new Error(`Creation response did not confirm ${body.name}; inspect the prompt library before retrying.`)
82
+ created.push(body.name)
83
+ savedWorkflows.push(saved)
84
+ log(`Created ${body.name}`)
85
+ }
86
+ if (savedWorkflows.length) {
87
+ const updated = await runtimeCommands()
88
+ if (savedWorkflows.some(workflow => !available(workflow, workflow, updated)))
89
+ throw new Error('Prompts were created but runtime shortcuts are not confirmed. Inspect the Assistant command settings; existing data was not overwritten.')
90
+ log('Created shortcuts confirmed by the published Assistant runtime.')
91
+ }
92
+ return { planned: creates.map(item => item.name), created, skipped }
93
+ }
94
+
95
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
96
+ try {
97
+ const { values } = parseArgs({ options: {
98
+ 'api-url': { type: 'string' }, 'organization-id': { type: 'string' },
99
+ 'assistant-id': { type: 'string' }, apply: { type: 'boolean', default: false }, help: { type: 'boolean' }
100
+ } })
101
+ if (values.help) console.log('Set XPERT_TOKEN, then run import-prompts.mjs --api-url https://host/api --organization-id UUID --assistant-id UUID [--apply]. Default: read-only preview. Requires Node 20+.')
102
+ else if (!values['api-url'] || !values['organization-id'] || !values['assistant-id']) throw new Error('Required: --api-url, --organization-id, --assistant-id. See --help.')
103
+ else await importPrompts({ apiUrl: values['api-url'], organizationId: values['organization-id'], assistantId: values['assistant-id'], token: process.env.XPERT_TOKEN, apply: values.apply })
104
+ } catch (error) {
105
+ console.error(error instanceof Error ? error.message : 'Prompt import failed.')
106
+ process.exitCode = 1
107
+ }
108
+ }