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.
- package/README.md +7 -3
- package/cli.mjs +3 -2
- package/package.json +4 -3
- package/skills/cast-list/assets/VERSION +2 -0
- package/skills/cast-list/assets/lesson.html +182 -0
- package/skills/cast-list/assets/map.html +244 -0
- package/skills/cast-list/examples/LICENSE-microsoft-skills-for-copilot-studio +21 -0
- package/skills/cast-list/examples/NOTICE.md +27 -0
- package/skills/cast-list/examples/agents/bodega-cupons/actions/ConsultarCuponsAtivos.mcs.yml +34 -0
- package/skills/cast-list/examples/agents/bodega-cupons/actions/EnviarCupomEmail.mcs.yml +34 -0
- package/skills/cast-list/examples/agents/bodega-cupons/actions/PromocoesMCP.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-cupons/agent.mcs.yml +27 -0
- package/skills/cast-list/examples/agents/bodega-cupons/connectionreferences.mcs.yml +17 -0
- package/skills/cast-list/examples/agents/bodega-cupons/knowledge/RegulamentoCupons.knowledge.mcs.yml +7 -0
- package/skills/cast-list/examples/agents/bodega-cupons/settings.mcs.yml +13 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/ConsultarRegulamentoCupons.topic.mcs.yml +56 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/Escalate.topic.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/Fallback.topic.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/OnError.topic.mcs.yml +48 -0
- package/skills/cast-list/examples/agents/bodega-cupons/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
- package/skills/cast-list/examples/agents/bodega-cupons/variables/CategoriaSolicitacao.mcs.yml +10 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/actions/AbrirSolicitacaoDevolucao.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/agent.mcs.yml +26 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/connectionreferences.mcs.yml +6 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/knowledge/PoliticaTrocasDevolucoes.knowledge.mcs.yml +7 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/settings.mcs.yml +13 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/ConsultarPoliticaDevolucoes.topic.mcs.yml +56 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Escalate.topic.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Fallback.topic.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/OnError.topic.mcs.yml +48 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/variables/CategoriaSolicitacao.mcs.yml +10 -0
- package/skills/cast-list/examples/agents/bodega-frete/actions/EnviarOpcoesFrete.mcs.yml +34 -0
- package/skills/cast-list/examples/agents/bodega-frete/agent.mcs.yml +26 -0
- package/skills/cast-list/examples/agents/bodega-frete/connectionreferences.mcs.yml +6 -0
- package/skills/cast-list/examples/agents/bodega-frete/knowledge/TabelaFreteEntregas.knowledge.mcs.yml +7 -0
- package/skills/cast-list/examples/agents/bodega-frete/settings.mcs.yml +13 -0
- package/skills/cast-list/examples/agents/bodega-frete/topics/ConsultarTabelaFrete.topic.mcs.yml +56 -0
- package/skills/cast-list/examples/agents/bodega-frete/topics/Fallback.topic.mcs.yml +31 -0
- package/skills/cast-list/examples/agents/bodega-frete/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-frete/topics/OnError.topic.mcs.yml +48 -0
- package/skills/cast-list/examples/agents/bodega-frete/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
- package/skills/cast-list/examples/agents/bodega-frete/variables/CategoriaSolicitacao.mcs.yml +10 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/actions/EnviarInstrucoesRetirada.mcs.yml +34 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/agent.mcs.yml +27 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/connectionreferences.mcs.yml +6 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/knowledge/GuiaLojaFisica.knowledge.mcs.yml +8 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/settings.mcs.yml +13 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/ConsultarGuiaLojaFisica.topic.mcs.yml +56 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Escalate.topic.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Fallback.topic.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/OnError.topic.mcs.yml +43 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/variables/CategoriaSolicitacao.mcs.yml +10 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteCupons.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteDevolucoes.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFrete.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteLojaFisica.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgentePedidos.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/agent.mcs.yml +35 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/agents/SobreBodegaAzul/agent.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/settings.mcs.yml +12 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/topics/ClassificarSolicitacao.topic.mcs.yml +35 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Escalate.topic.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Fallback.topic.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/topics/OnError.topic.mcs.yml +48 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/variables/CategoriaSolicitacao.mcs.yml +9 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/actions/EnviarResumoPedido.mcs.yml +34 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/agent.mcs.yml +26 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/connectionreferences.mcs.yml +6 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/knowledge/PoliticasPedidos.knowledge.mcs.yml +7 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/settings.mcs.yml +13 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/ConsultarPoliticasPedidos.topic.mcs.yml +56 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/Escalate.topic.mcs.yml +20 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/Fallback.topic.mcs.yml +32 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/Greeting.topic.mcs.yml +14 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/OnError.topic.mcs.yml +48 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/RecebimentoOrquestrador.topic.mcs.yml +16 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/variables/CategoriaSolicitacao.mcs.yml +10 -0
- package/skills/cast-list/examples/demo.cast.yaml +10 -0
- package/skills/cast-list/examples/profile.cast.yaml +15 -0
- package/skills/cast-list/references/format.md +50 -0
- package/skills/cast-list/references/keys.md +107 -0
- package/skills/cast-list/references/profile.md +32 -0
- package/skills/cast-list/scripts/browser.mjs +198 -0
- package/skills/cast-list/scripts/build.mjs +179 -0
- package/skills/cast-list/scripts/engine.mjs +155 -0
- package/skills/cast-list/scripts/page.mjs +15 -0
- package/skills/cast-list/scripts/read-xlsx.mjs +67 -0
- package/skills/cast-list/vendor/microsoft/LICENSE +21 -0
- package/skills/cast-list/vendor/microsoft/NOTICE +9 -0
- package/skills/cast-list/vendor/microsoft/package.json +1 -0
- package/skills/cast-list/vendor/microsoft/reference/bot.schema.yaml-authoring.json +27605 -0
- 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)
|