house-lights 1.1.0 → 1.2.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 (98) hide show
  1. package/README.md +7 -3
  2. package/cli.mjs +3 -2
  3. package/package.json +4 -3
  4. package/skills/cast-list/assets/VERSION +2 -0
  5. package/skills/cast-list/assets/lesson.html +182 -0
  6. package/skills/cast-list/assets/map.html +244 -0
  7. package/skills/cast-list/examples/LICENSE-microsoft-skills-for-copilot-studio +21 -0
  8. package/skills/cast-list/examples/NOTICE.md +27 -0
  9. package/skills/cast-list/examples/agents/bodega-cupons/actions/ConsultarCuponsAtivos.mcs.yml +34 -0
  10. package/skills/cast-list/examples/agents/bodega-cupons/actions/EnviarCupomEmail.mcs.yml +34 -0
  11. package/skills/cast-list/examples/agents/bodega-cupons/actions/PromocoesMCP.mcs.yml +20 -0
  12. package/skills/cast-list/examples/agents/bodega-cupons/agent.mcs.yml +27 -0
  13. package/skills/cast-list/examples/agents/bodega-cupons/connectionreferences.mcs.yml +17 -0
  14. package/skills/cast-list/examples/agents/bodega-cupons/knowledge/RegulamentoCupons.knowledge.mcs.yml +7 -0
  15. package/skills/cast-list/examples/agents/bodega-cupons/settings.mcs.yml +13 -0
  16. package/skills/cast-list/examples/agents/bodega-cupons/topics/ConsultarRegulamentoCupons.topic.mcs.yml +56 -0
  17. package/skills/cast-list/examples/agents/bodega-cupons/topics/Escalate.topic.mcs.yml +20 -0
  18. package/skills/cast-list/examples/agents/bodega-cupons/topics/Fallback.topic.mcs.yml +32 -0
  19. package/skills/cast-list/examples/agents/bodega-cupons/topics/Greeting.topic.mcs.yml +14 -0
  20. package/skills/cast-list/examples/agents/bodega-cupons/topics/OnError.topic.mcs.yml +48 -0
  21. package/skills/cast-list/examples/agents/bodega-cupons/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
  22. package/skills/cast-list/examples/agents/bodega-cupons/variables/CategoriaSolicitacao.mcs.yml +10 -0
  23. package/skills/cast-list/examples/agents/bodega-devolucoes/actions/AbrirSolicitacaoDevolucao.mcs.yml +32 -0
  24. package/skills/cast-list/examples/agents/bodega-devolucoes/agent.mcs.yml +26 -0
  25. package/skills/cast-list/examples/agents/bodega-devolucoes/connectionreferences.mcs.yml +6 -0
  26. package/skills/cast-list/examples/agents/bodega-devolucoes/knowledge/PoliticaTrocasDevolucoes.knowledge.mcs.yml +7 -0
  27. package/skills/cast-list/examples/agents/bodega-devolucoes/settings.mcs.yml +13 -0
  28. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/ConsultarPoliticaDevolucoes.topic.mcs.yml +56 -0
  29. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Escalate.topic.mcs.yml +20 -0
  30. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Fallback.topic.mcs.yml +32 -0
  31. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Greeting.topic.mcs.yml +14 -0
  32. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/OnError.topic.mcs.yml +48 -0
  33. package/skills/cast-list/examples/agents/bodega-devolucoes/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
  34. package/skills/cast-list/examples/agents/bodega-devolucoes/variables/CategoriaSolicitacao.mcs.yml +10 -0
  35. package/skills/cast-list/examples/agents/bodega-frete/actions/EnviarOpcoesFrete.mcs.yml +34 -0
  36. package/skills/cast-list/examples/agents/bodega-frete/agent.mcs.yml +26 -0
  37. package/skills/cast-list/examples/agents/bodega-frete/connectionreferences.mcs.yml +6 -0
  38. package/skills/cast-list/examples/agents/bodega-frete/knowledge/TabelaFreteEntregas.knowledge.mcs.yml +7 -0
  39. package/skills/cast-list/examples/agents/bodega-frete/settings.mcs.yml +13 -0
  40. package/skills/cast-list/examples/agents/bodega-frete/topics/ConsultarTabelaFrete.topic.mcs.yml +56 -0
  41. package/skills/cast-list/examples/agents/bodega-frete/topics/Fallback.topic.mcs.yml +31 -0
  42. package/skills/cast-list/examples/agents/bodega-frete/topics/Greeting.topic.mcs.yml +14 -0
  43. package/skills/cast-list/examples/agents/bodega-frete/topics/OnError.topic.mcs.yml +48 -0
  44. package/skills/cast-list/examples/agents/bodega-frete/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
  45. package/skills/cast-list/examples/agents/bodega-frete/variables/CategoriaSolicitacao.mcs.yml +10 -0
  46. package/skills/cast-list/examples/agents/bodega-loja-fisica/actions/EnviarInstrucoesRetirada.mcs.yml +34 -0
  47. package/skills/cast-list/examples/agents/bodega-loja-fisica/agent.mcs.yml +27 -0
  48. package/skills/cast-list/examples/agents/bodega-loja-fisica/connectionreferences.mcs.yml +6 -0
  49. package/skills/cast-list/examples/agents/bodega-loja-fisica/knowledge/GuiaLojaFisica.knowledge.mcs.yml +8 -0
  50. package/skills/cast-list/examples/agents/bodega-loja-fisica/settings.mcs.yml +13 -0
  51. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/ConsultarGuiaLojaFisica.topic.mcs.yml +56 -0
  52. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Escalate.topic.mcs.yml +20 -0
  53. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Fallback.topic.mcs.yml +32 -0
  54. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Greeting.topic.mcs.yml +14 -0
  55. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/OnError.topic.mcs.yml +43 -0
  56. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
  57. package/skills/cast-list/examples/agents/bodega-loja-fisica/variables/CategoriaSolicitacao.mcs.yml +10 -0
  58. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteCupons.mcs.yml +24 -0
  59. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteDevolucoes.mcs.yml +24 -0
  60. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFrete.mcs.yml +24 -0
  61. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteLojaFisica.mcs.yml +24 -0
  62. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgentePedidos.mcs.yml +24 -0
  63. package/skills/cast-list/examples/agents/bodega-orquestrador/agent.mcs.yml +35 -0
  64. package/skills/cast-list/examples/agents/bodega-orquestrador/agents/SobreBodegaAzul/agent.mcs.yml +32 -0
  65. package/skills/cast-list/examples/agents/bodega-orquestrador/settings.mcs.yml +12 -0
  66. package/skills/cast-list/examples/agents/bodega-orquestrador/topics/ClassificarSolicitacao.topic.mcs.yml +35 -0
  67. package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Escalate.topic.mcs.yml +20 -0
  68. package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Fallback.topic.mcs.yml +32 -0
  69. package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Greeting.topic.mcs.yml +14 -0
  70. package/skills/cast-list/examples/agents/bodega-orquestrador/topics/OnError.topic.mcs.yml +48 -0
  71. package/skills/cast-list/examples/agents/bodega-orquestrador/variables/CategoriaSolicitacao.mcs.yml +9 -0
  72. package/skills/cast-list/examples/agents/bodega-pedidos/actions/EnviarResumoPedido.mcs.yml +34 -0
  73. package/skills/cast-list/examples/agents/bodega-pedidos/agent.mcs.yml +26 -0
  74. package/skills/cast-list/examples/agents/bodega-pedidos/connectionreferences.mcs.yml +6 -0
  75. package/skills/cast-list/examples/agents/bodega-pedidos/knowledge/PoliticasPedidos.knowledge.mcs.yml +7 -0
  76. package/skills/cast-list/examples/agents/bodega-pedidos/settings.mcs.yml +13 -0
  77. package/skills/cast-list/examples/agents/bodega-pedidos/topics/ConsultarPoliticasPedidos.topic.mcs.yml +56 -0
  78. package/skills/cast-list/examples/agents/bodega-pedidos/topics/Escalate.topic.mcs.yml +20 -0
  79. package/skills/cast-list/examples/agents/bodega-pedidos/topics/Fallback.topic.mcs.yml +32 -0
  80. package/skills/cast-list/examples/agents/bodega-pedidos/topics/Greeting.topic.mcs.yml +14 -0
  81. package/skills/cast-list/examples/agents/bodega-pedidos/topics/OnError.topic.mcs.yml +48 -0
  82. package/skills/cast-list/examples/agents/bodega-pedidos/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
  83. package/skills/cast-list/examples/agents/bodega-pedidos/variables/CategoriaSolicitacao.mcs.yml +10 -0
  84. package/skills/cast-list/examples/demo.cast.yaml +10 -0
  85. package/skills/cast-list/examples/profile.cast.yaml +15 -0
  86. package/skills/cast-list/references/format.md +50 -0
  87. package/skills/cast-list/references/keys.md +107 -0
  88. package/skills/cast-list/references/profile.md +32 -0
  89. package/skills/cast-list/scripts/browser.mjs +198 -0
  90. package/skills/cast-list/scripts/build.mjs +179 -0
  91. package/skills/cast-list/scripts/engine.mjs +155 -0
  92. package/skills/cast-list/scripts/page.mjs +15 -0
  93. package/skills/cast-list/scripts/read-xlsx.mjs +67 -0
  94. package/skills/cast-list/vendor/microsoft/LICENSE +21 -0
  95. package/skills/cast-list/vendor/microsoft/NOTICE +9 -0
  96. package/skills/cast-list/vendor/microsoft/package.json +1 -0
  97. package/skills/cast-list/vendor/microsoft/reference/bot.schema.yaml-authoring.json +27605 -0
  98. package/skills/cast-list/vendor/microsoft/scripts/schema-lookup.bundle.js +3495 -0
@@ -0,0 +1,198 @@
1
+ // A Chrome, Chromium or Edge without a window: opens a built page in one of its modes (#review, #snapshots, #svg…) and
2
+ // reads back what the page wrote in <pre id="chalk-<mode>">. It talks to the browser over the DevTools protocol on a
3
+ // pipe (--remote-debugging-pipe) and waits for that <pre> itself: --dump-dom hangs on some machines, the pipe does not.
4
+ // The browsers are tried in order; one that does not start or does not answer in time gives way to the next.
5
+ // Part of the skills kit (chalk-talk/kit): copied into each skill by its tools/assets.mjs. Node only, no dependency.
6
+ import { execFileSync, spawn } from 'node:child_process'
7
+ import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
8
+ import { createRequire } from 'node:module'
9
+ import { homedir, tmpdir } from 'node:os'
10
+ import { join } from 'node:path'
11
+ import { pathToFileURL } from 'node:url'
12
+
13
+ /** Playwright's own Chromium: the package's answer when it is installed here, else what it downloaded. */
14
+ function playwright() {
15
+ const found = []
16
+ const req = createRequire(join(process.cwd(), 'noop.js'))
17
+ for (const name of ['playwright', 'playwright-core']) {
18
+ try {
19
+ const p = req(name).chromium.executablePath()
20
+ if (p) found.push(p)
21
+ } catch {}
22
+ }
23
+ const caches = [process.env.PLAYWRIGHT_BROWSERS_PATH, join(homedir(), '.cache/ms-playwright'), join(homedir(), 'Library/Caches/ms-playwright'), '/opt/pw-browsers'].filter(Boolean)
24
+ const inside = ['chrome-linux64/chrome', 'chrome-linux/chrome', 'chrome-linux-arm64/chrome', 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', 'chrome-mac/Chromium.app/Contents/MacOS/Chromium']
25
+ for (const cache of caches) {
26
+ let dirs = []
27
+ try {
28
+ dirs = readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort().reverse()
29
+ } catch {}
30
+ for (const d of dirs) for (const p of inside) found.push(join(cache, d, p))
31
+ }
32
+ return found
33
+ }
34
+
35
+ /** Every browser worth trying, in order: CHROME_PATH, PUPPETEER_EXECUTABLE_PATH, Playwright's Chromium, the usual places
36
+ * on macOS and Windows, then `which` on Linux. Only paths that exist, each once. */
37
+ export function candidates() {
38
+ const paths = [process.env.CHROME_PATH, process.env.PUPPETEER_EXECUTABLE_PATH, ...playwright(),
39
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
40
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
41
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
42
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
43
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe']
44
+ if (process.platform !== 'win32') {
45
+ for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'microsoft-edge']) {
46
+ try {
47
+ paths.push(execFileSync('which', [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim())
48
+ } catch {}
49
+ }
50
+ }
51
+ return [...new Set(paths.filter((p) => p && existsSync(p)))]
52
+ }
53
+
54
+ /** The first browser there is, or null. */
55
+ export function browser() {
56
+ return candidates()[0] ?? null
57
+ }
58
+
59
+ /** What the browser needs besides the usual: on Linux no sandbox as root (Chromium refuses to start otherwise) and no
60
+ * /dev/shm (small in containers). */
61
+ export function launchFlags(platform = process.platform, uid = process.getuid?.()) {
62
+ const flags = ['--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', '--allow-file-access-from-files',
63
+ '--disable-background-networking', '--disable-component-update', '--disable-sync', '--remote-debugging-pipe']
64
+ if (platform === 'linux') flags.push('--disable-dev-shm-usage')
65
+ if (platform === 'linux' && uid === 0) flags.push('--no-sandbox')
66
+ return flags
67
+ }
68
+
69
+ class BrowserFailed extends Error {}
70
+
71
+ // A wait that never keeps Node running by itself.
72
+ const pause = (ms) => new Promise((ok) => setTimeout(ok, ms).unref())
73
+
74
+ /** One browser, started with a profile of its own, spoken to over the pipe. */
75
+ async function start(exe) {
76
+ const profile = mkdtempSync(join(tmpdir(), 'chalk-talk-'))
77
+ const child = spawn(exe, [...launchFlags(), `--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] })
78
+ const waiting = new Map()
79
+ let id = 0
80
+ let dead
81
+ // a failure of the whole browser rejects every call still waiting
82
+ const fail = (why) => {
83
+ dead ??= why
84
+ for (const { reject } of waiting.values()) reject(new BrowserFailed(dead))
85
+ waiting.clear()
86
+ }
87
+ child.on('error', (e) => fail(e.message))
88
+ const exited = new Promise((ok) => child.once('exit', (code) => (fail(`it stopped (exit ${code})`), ok())))
89
+ let buf = Buffer.alloc(0)
90
+ child.stdio[4].on('data', (d) => {
91
+ buf = Buffer.concat([buf, d])
92
+ let at
93
+ while ((at = buf.indexOf(0)) >= 0) {
94
+ const msg = JSON.parse(buf.subarray(0, at).toString('utf8'))
95
+ buf = buf.subarray(at + 1)
96
+ const call = msg.id && waiting.get(msg.id)
97
+ if (!call) continue
98
+ waiting.delete(msg.id)
99
+ if (msg.error) call.reject(new Error(msg.error.message))
100
+ else call.ok(msg.result)
101
+ }
102
+ })
103
+ child.stdio[3].on('error', (e) => fail(e.message))
104
+ const send = (method, params = {}, sessionId) => new Promise((ok, reject) => {
105
+ if (dead) return reject(new BrowserFailed(dead))
106
+ const n = ++id
107
+ waiting.set(n, { ok, reject })
108
+ child.stdio[3].write(JSON.stringify({ id: n, method, params, ...(sessionId ? { sessionId } : {}) }) + '\0')
109
+ })
110
+ const close = async () => {
111
+ if (!dead) {
112
+ send('Browser.close').catch(() => {})
113
+ await Promise.race([exited, pause(3000)])
114
+ }
115
+ child.kill()
116
+ await Promise.race([exited, pause(2000)])
117
+ rmSync(profile, { recursive: true, force: true })
118
+ }
119
+ return { send, close }
120
+ }
121
+
122
+ /** Opens `file` at `#mode` (with `query`) in a tab of its own and returns what the page wrote, parsed — { error } when
123
+ * the page could not be opened or wrote nothing in `ms`. */
124
+ async function open(b, file, mode, query, ms, shot) {
125
+ const until = Date.now() + ms
126
+ let sessionId
127
+ // Every call waits at most until the page's time is up: a page busy forever does not hold the build.
128
+ const ask = (method, params, wait = until - Date.now()) => {
129
+ let timer
130
+ const late = new Promise((_, reject) => (timer = setTimeout(() => reject(new BrowserFailed(`no answer in ${Math.round(ms / 1000)} s`)), Math.max(0, wait))))
131
+ return Promise.race([b.send(method, params, sessionId), late]).finally(() => clearTimeout(timer))
132
+ }
133
+ const { targetId } = await ask('Target.createTarget', { url: 'about:blank' })
134
+ try {
135
+ ;({ sessionId } = await ask('Target.attachToTarget', { targetId, flatten: true }))
136
+ if (shot) await ask('Emulation.setDeviceMetricsOverride', { width: shot.width, height: shot.height, deviceScaleFactor: 1, mobile: false })
137
+ if (shot?.scheme) await ask('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: shot.scheme }] })
138
+ const nav = await ask('Page.navigate', { url: `${pathToFileURL(file).href}${query ? `?${query}` : ''}${mode ? `#${mode}` : ''}` })
139
+ if (nav.errorText) return { error: `the page did not open (${nav.errorText})` }
140
+ // A picture of the page as it is once loaded, its fonts in and (when it says so) data-ready on the body.
141
+ if (shot) {
142
+ const ready = "document.readyState === 'complete' && document.fonts.status === 'loaded' && (document.body?.dataset.ready ?? '1') === '1'"
143
+ while (Date.now() < until) {
144
+ const r = await ask('Runtime.evaluate', { expression: ready, returnByValue: true })
145
+ if (r.result?.value === true) {
146
+ await pause(shot.settle ?? 400)
147
+ const { data } = await ask('Page.captureScreenshot', { format: 'png' })
148
+ return { png: `data:image/png;base64,${data}` }
149
+ }
150
+ await pause(50)
151
+ }
152
+ throw new BrowserFailed(`the page was not ready in ${Math.round(ms / 1000)} s`)
153
+ }
154
+ while (Date.now() < until) {
155
+ const r = await ask('Runtime.evaluate', { expression: `document.getElementById('chalk-${mode}')?.textContent ?? null`, returnByValue: true })
156
+ const text = r.result?.value
157
+ if (typeof text === 'string' && text) return JSON.parse(decodeURIComponent(text))
158
+ await pause(50)
159
+ }
160
+ const at = await ask('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, 1000).catch(() => undefined)
161
+ throw new BrowserFailed(`no answer in ${Math.round(ms / 1000)} s${at?.result?.value ? ` at ${at.result.value}` : ''}`)
162
+ } finally {
163
+ sessionId = undefined
164
+ b.send('Target.closeTarget', { targetId }).catch(() => {})
165
+ }
166
+ }
167
+
168
+ /** Runs `jobs` ({ file, mode, query, shot }) in one browser — `shot: { width, height }` answers with { png } (a
169
+ * picture of the page once loaded) instead of what the page wrote — and returns their results in order, with the browser used —
170
+ * `results` null when there is no browser at all. A browser that fails (does not start, stops, or a page gets no answer
171
+ * in `timeout` seconds) gives way to the next one, which runs every job again; when none works, each result is
172
+ * { error } naming what was tried. */
173
+ export async function runPages(jobs, { timeout = 20 } = {}) {
174
+ const list = candidates()
175
+ if (!list.length) return { results: null, tried: [] }
176
+ const tried = []
177
+ for (const exe of list) {
178
+ let b
179
+ try {
180
+ b = await start(exe)
181
+ const results = []
182
+ for (const { file, mode = '', query = '', shot } of jobs) results.push(await open(b, file, mode, query, timeout * 1000, shot))
183
+ return { results, used: exe, tried }
184
+ } catch (e) {
185
+ tried.push(`${exe}: ${e instanceof BrowserFailed ? e.message : e.message.split('\n')[0]}`)
186
+ } finally {
187
+ await b?.close()
188
+ }
189
+ }
190
+ const error = `no browser answered — ${tried.join('; ')}. Point CHROME_PATH at another one, or give it more time with --review-timeout`
191
+ return { results: jobs.map(() => ({ error })), tried }
192
+ }
193
+
194
+ /** One page: what it wrote, parsed; null without a browser, { error } when it failed. */
195
+ export async function runPage(file, mode, query = '', opts) {
196
+ const { results } = await runPages([{ file, mode, query }], opts)
197
+ return results ? results[0] : null
198
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ // Compares the Copilot Studio agents cloned in a folder: what most of them do (a pattern), who is off it, what only
3
+ // one has, the team's rules not met — and makes the map (one HTML page that opens with no internet), the workbook
4
+ // (.xlsx) and the cast file (what people said, what was decided). No model, no install, no network: Node 18 or later.
5
+ //
6
+ // node build.mjs <agents-folder> --check [--profile <p.cast.yaml>] [--lang en|pt] only compare: nothing is written
7
+ // node build.mjs <agents-folder> [--profile <p.cast.yaml>] [--html] [--xlsx] [--lesson] [--all] [--lang en|pt] [--out <folder>]
8
+ // node build.mjs <agents-folder> --facts [--out <folder>] only the facts, for a model: <id>.facts.json
9
+ // node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]
10
+ // what was filled in the workbook (Declared, the Decision column): listed with a key each,
11
+ // and with --take written to the cast file
12
+ //
13
+ // With no --html/--xlsx/--lesson it makes the map. Files go in --out (or beside the folder), named by the folder:
14
+ // <id>.html, <id>.xlsx, <id>.lesson.html (a chalk-talk lesson), and <id>.cast.yaml, kept up to date (read again on every
15
+ // run). Exit codes: 0 done (what to look at is printed, and is not an error); 1 the profile or the cast file breaks its
16
+ // format (each problem with its line), or no agent in the folder; 2 bad arguments; 3 made, but the lesson's reviewer
17
+ // found something to fix. The outputs hold the client's data: keep them in the client's environment. The lesson's
18
+ // review uses Chrome, Chromium or Edge when there is one.
19
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
20
+ import { basename, dirname, join, resolve } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { runPage } from './browser.mjs'
23
+ import { agentsIn, applyChanges, compare, DEFAULT_PROFILE, extractAgent, fromSheets, knownKinds, labelOf, lessonOf, problemText, readCast, readProfile, reviewCast, validateScene, workbookFile, workbookOf, writeCast } from './engine.mjs'
24
+ import { readXlsx } from './read-xlsx.mjs'
25
+ import { fill, inline } from './page.mjs'
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url))
28
+ const argv = process.argv.slice(2)
29
+ const valued = new Set(['--lang', '--out', '--profile', '--from', '--take'])
30
+ const flags = new Set(['--check', '--html', '--xlsx', '--lesson', '--all', '--facts', ...valued])
31
+ const option = (name) => {
32
+ const at = argv.indexOf(name)
33
+ return at >= 0 ? argv[at + 1] : undefined
34
+ }
35
+ const args = argv.filter((a, i) => !a.startsWith('--') && !valued.has(argv[i - 1]))
36
+ const usage = `usage: node build.mjs <agents-folder> --check [--profile <p.cast.yaml>] [--lang en|pt]
37
+ node build.mjs <agents-folder> [--profile <p.cast.yaml>] [--html] [--xlsx] [--lesson] [--all] [--lang en|pt] [--out <folder>]
38
+ node build.mjs <agents-folder> --facts [--out <folder>]
39
+ node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]`
40
+ const lang = option('--lang') ?? 'en'
41
+ const fail = (msg) => {
42
+ console.error(`${msg}\n${usage}`)
43
+ process.exit(2)
44
+ }
45
+ if (args.length !== 1 || !existsSync(args[0]) || !statSync(args[0]).isDirectory()) fail('✗ one folder with the cloned agents')
46
+ const unknown = argv.find((a) => a.startsWith('--') && !flags.has(a))
47
+ if (unknown) fail(`✗ unknown option ${unknown}`)
48
+ if (!['en', 'pt'].includes(lang)) fail('✗ --lang is en or pt')
49
+ for (const v of valued) if (argv.includes(v) && !option(v)) fail(`✗ ${v} needs a value`)
50
+ if (argv.includes('--take') && !argv.includes('--from')) fail('✗ --take goes with --from')
51
+
52
+ const T = {
53
+ en: {
54
+ found: (n, o, s) => `✓ ${n} in the cast: ${o} orchestrator${o === 1 ? '' : 's'}, ${s} specialist${s === 1 ? '' : 's'}`,
55
+ result: (p, off, solos) => `✓ ${p} patterns · ${off} off a pattern · ${solos} only one has`,
56
+ look: 'to look at', nothing: '✓ nothing to look at', facts: (f) => `✓ facts ${f}`,
57
+ changes: (n) => (n ? `${n} change${n === 1 ? '' : 's'} in the workbook (take them with --take <key,…> or --take all):` : '✓ the workbook changes nothing'),
58
+ took: (n, f) => `✓ took ${n} change${n === 1 ? '' : 's'} into ${f}`, unknownKey: (k) => `✗ no change with the key «${k}»`,
59
+ declared: 'declared', decision: 'decision', add: '+', set: '~', remove: '−', accepted: 'accepted', rejected: 'rejected', platform: 'platform', specialist: 'specialist', orchestrator: 'orchestrator', none: (f) => `✗ ${f} has no folder with agent.mcs.yml`, privacy: '• the files hold the client’s data: keep them in the client’s environment',
60
+ },
61
+ pt: {
62
+ found: (n, o, s) => `✓ ${n} no elenco: ${o} ${o === 1 ? 'orquestrador' : 'orquestradores'}, ${s} ${s === 1 ? 'especialista' : 'especialistas'}`,
63
+ result: (p, off, solos) => `✓ ${p} padrões · ${off} fora do padrão · ${solos} só um tem`,
64
+ look: 'para olhar', nothing: '✓ nada para olhar', facts: (f) => `✓ fatos ${f}`,
65
+ changes: (n) => (n ? `${n} ${n === 1 ? 'mudança' : 'mudanças'} na planilha (aceite com --take <chave,…> ou --take all):` : '✓ a planilha não muda nada'),
66
+ took: (n, f) => `✓ ${n} ${n === 1 ? 'mudança gravada' : 'mudanças gravadas'} em ${f}`, unknownKey: (k) => `✗ nenhuma mudança com a chave «${k}»`,
67
+ declared: 'declarado', decision: 'decisão', add: '+', set: '~', remove: '−', accepted: 'aceito', rejected: 'rejeitado', platform: 'plataforma', specialist: 'especialista', orchestrator: 'orquestrador', none: (f) => `✗ ${f} não tem nenhuma pasta com agent.mcs.yml`, privacy: '• os arquivos têm dados do cliente: deixe-os no ambiente dele',
68
+ },
69
+ }[lang]
70
+
71
+ const folder = resolve(args[0])
72
+ const id = basename(folder)
73
+ const read = (file, reader) => {
74
+ const r = reader(readFileSync(file, 'utf8'))
75
+ if (r.problems.length) {
76
+ for (const p of r.problems) console.error(`✗ ${basename(file)}:${p.line}: ${problemText(p, lang)}`)
77
+ process.exit(1)
78
+ }
79
+ return r
80
+ }
81
+ const profile = option('--profile') ? read(resolve(option('--profile')), readProfile).profile : DEFAULT_PROFILE
82
+ const out = resolve(option('--out') ?? dirname(folder))
83
+ const castFile = join(out, `${id}.cast.yaml`)
84
+ const castText = existsSync(castFile) ? readFileSync(castFile, 'utf8') : undefined
85
+ const cast = castText !== undefined ? read(castFile, readCast).cast : { ...(profile.name ? { name: profile.name } : {}), agents: [], declared: [], decisions: [] }
86
+
87
+ // ---- the way back: an edited workbook as the changes it proposes ----
88
+ if (argv.includes('--from')) {
89
+ const edited = fromSheets(cast, readXlsx(readFileSync(resolve(option('--from')))))
90
+ if (edited.problems.length) {
91
+ for (const p of edited.problems) console.error(`✗ ${basename(option('--from'))}: ${p}`)
92
+ process.exit(1)
93
+ }
94
+ const line = (c) => {
95
+ const x = c.after ?? c.before
96
+ // a key with no kind («kpi») is something only people know: declared:<key>
97
+ if (c.kind === 'declared') return `${T[c.op]} ${T.declared} ${x.agent} · ${labelOf(x.key.includes(':') ? x.key : `declared:${x.key}`, lang)}${x.value ? ` = «${x.value}»` : ''}${x.source ? ` (${x.source})` : ''}`
98
+ return `${T[c.op]} ${T.decision} ${labelOf(x.key, lang)}: ${T[x.status]} (${T[x.scope]})${x.why ? ` — ${x.why}` : ''}`
99
+ }
100
+ const take = option('--take')
101
+ if (!take) {
102
+ console.log(T.changes(edited.changes.length))
103
+ for (const c of edited.changes) console.log(` ${c.key} ${line(c)}`)
104
+ process.exit(0)
105
+ }
106
+ const keys = take === 'all' ? edited.changes.map((c) => c.key) : take.split(',').map((k) => k.trim()).filter(Boolean)
107
+ const missing = keys.find((k) => !edited.changes.some((c) => c.key === k))
108
+ if (missing) {
109
+ console.error(T.unknownKey(missing))
110
+ process.exit(2)
111
+ }
112
+ mkdirSync(out, { recursive: true })
113
+ writeFileSync(castFile, writeCast(applyChanges(cast, edited.changes, keys), castText))
114
+ console.log(T.took(keys.length, basename(castFile)))
115
+ process.exit(0)
116
+ }
117
+
118
+ // the schema's kinds, to tell a made-up one; without the schema every kind is taken as real
119
+ const schema = join(here, '../vendor/microsoft/reference/bot.schema.yaml-authoring.json')
120
+ const kinds = existsSync(schema) ? knownKinds(JSON.parse(readFileSync(schema, 'utf8'))) : undefined
121
+ const found = agentsIn(folder).map((a) => extractAgent(a.id, a.files, kinds))
122
+ if (!found.length) {
123
+ console.error(T.none(args[0]))
124
+ process.exit(1)
125
+ }
126
+ // only the facts, for a model to read: what was seen in each agent, with the file and line
127
+ if (argv.includes('--facts')) {
128
+ mkdirSync(out, { recursive: true })
129
+ const file = join(out, `${id}.facts.json`)
130
+ writeFileSync(file, `${JSON.stringify({ format: 'cast-list/facts-v1', agents: found }, null, 2)}\n`)
131
+ console.log(T.facts(file))
132
+ console.log(T.privacy)
133
+ process.exit(0)
134
+ }
135
+ const result = compare(found, profile, cast)
136
+ const looks = reviewCast(result)
137
+ const roles = Object.values(result.roles)
138
+ console.log(T.found(found.length, roles.filter((r) => r === 'orchestrator').length, roles.filter((r) => r === 'specialist').length))
139
+ console.log(T.result(result.patterns.length, new Set(result.offPattern.map((o) => o.agent)).size, looks.filter((l) => l.key === 'solo').length))
140
+ if (looks.length) for (const l of looks) console.log(`! ${T.look}: ${l[lang]}`)
141
+ else console.log(T.nothing)
142
+ if (argv.includes('--check')) process.exit(0)
143
+
144
+ const all = argv.includes('--all')
145
+ const want = { html: all || argv.includes('--html'), xlsx: all || argv.includes('--xlsx'), lesson: all || argv.includes('--lesson') }
146
+ if (!want.xlsx && !want.lesson) want.html = true
147
+ mkdirSync(out, { recursive: true })
148
+ // the cast file keeps this run's roles, under what people wrote in it
149
+ const kept = { ...cast, agents: result.agents.map((a) => ({ id: a.id, role: a.role })) }
150
+ const castYaml = writeCast(kept, castText)
151
+ writeFileSync(castFile, castYaml)
152
+ console.log(`✓ cast ${castFile}`)
153
+ if (want.html) {
154
+ const page = join(out, `${id}.html`)
155
+ const map = readFileSync(join(here, '../assets/map.html'), 'utf8')
156
+ writeFileSync(page, fill(map, { CAST: inline({ id, lang, result, looks, cast: kept, castYaml, focusAreas: profile.focusAreas, threshold: profile.threshold }) }))
157
+ console.log(`✓ map ${page}`)
158
+ }
159
+ if (want.xlsx) {
160
+ const book = join(out, `${id}.xlsx`)
161
+ writeFileSync(book, workbookFile(workbookOf(result, looks, kept, lang)))
162
+ console.log(`✓ workbook ${book}`)
163
+ }
164
+ let exit = 0
165
+ if (want.lesson) {
166
+ const page = join(out, `${id}.lesson.html`)
167
+ writeFileSync(page, fill(readFileSync(join(here, '../assets/lesson.html'), 'utf8'), { RECIPE: inline(validateScene(lessonOf(result, looks, lang))) }))
168
+ console.log(`✓ lesson ${page}`)
169
+ const r = await runPage(page, 'review')
170
+ if (r === null) console.log('• review no Chrome, Chromium or Edge here: the lesson checks itself when opened')
171
+ else if (r.error) console.log(`✗ review not checked: ${r.error}`)
172
+ else if (!r.problems.length) console.log(`✓ review ${r.combos} ${r.combos === 1 ? 'setting' : 'settings'}: nothing off the paper, no text over text`)
173
+ else {
174
+ for (const p of r.problems) console.log(`! review ${p}`)
175
+ exit = 3
176
+ }
177
+ }
178
+ console.log(T.privacy)
179
+ process.exit(exit)