@entrinsik/vite-plugin-informer 2.7.0-beta.0 → 2.10.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 ADDED
@@ -0,0 +1,129 @@
1
+ # @entrinsik/vite-plugin-informer
2
+
3
+ Vite plugin and deploy tool for Informer App development: a local dev server wired to a live Informer, and a deploy pipeline that assembles your project and ships it to an App's library.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm create vite@latest my-app -- --template react
9
+ cd my-app
10
+ npm install -D @entrinsik/vite-plugin-informer
11
+ npx informer-init # wires vite config, informer.yaml, .env, deploy scripts
12
+ npm run dev # local dev against the Informer in .env
13
+ npm run deploy # assemble + upload + deploy
14
+ ```
15
+
16
+ ## What a deploy ships
17
+
18
+ The assembler uploads your built frontend plus these source trees, matching how an App's library is structured in Informer:
19
+
20
+ | Folder | Becomes |
21
+ |---|---|
22
+ | `server/` | API routes, dispatched under the App's `/api` surface |
23
+ | `webhooks/` | Publicly callable webhook routes |
24
+ | `tools/` | AI tools, private to the App's agents |
25
+ | `mcp/` | Tools exposed over the App's MCP endpoint |
26
+ | `channels/` | Live channel join/leave handlers |
27
+ | `migrations/` | Workspace database migrations, run in order on deploy |
28
+ | `embeddings/` | Declarative embedding use cases (see below) |
29
+ | `lib/`, `shared/` | Modules importable by the trees above |
30
+
31
+ `informer.yaml` declares dependencies, raw API access, roles, and (optionally) a minimum Informer version.
32
+
33
+ ## Server compatibility
34
+
35
+ A deploy begins by asking the target server what it is (`GET /api/about`, one unauthenticated request) and adapts to the answer, so one version of this plugin can drive a fleet on mixed releases.
36
+
37
+ Everything below needs **Informer 2026.1.3**: App Channels (`channels/`, the `channels:` block, `broadcast()`), the embedding pump (`embeddings/`, `embed()`), staged uploads/downloads (`uploads`/`downloads` on the handler bag, `__INFORMER__.upload()`), and the `platform` descriptor itself.
38
+
39
+ Against an older server the CLI:
40
+
41
+ - **Leaves `channels/` and `embeddings/` on your disk.** Those releases don't recognise the folders as server-side, so anything uploaded there is served to any App viewer — your workspace SQL and channel authorization logic included. The rest of the App deploys normally; the warning names what was skipped and why.
42
+ - **Names every feature that would be inert**, so a green deploy can't be mistaken for a working one.
43
+ - **Enforces `requires: { informer: … }` itself.** Releases before 2026.1.3 ignore the block, so without this an App that declared a floor would deploy clean onto a server that can't run it. The deploy is refused before the App's library is touched.
44
+
45
+ A server whose version isn't comparable (a `dev` build, or an `/about` behind a proxy) is treated as current and warned about — gating it would break deploys to builds made from source.
46
+
47
+ Feature-detect at runtime with optional chaining, since `platform` is absent entirely before 2026.1.3:
48
+
49
+ ```javascript
50
+ if (platform?.capabilities?.channels) broadcast('tickets', { … });
51
+ ```
52
+
53
+ A marketplace publish is unaffected: the archive always carries every source tree, because the server that installs it isn't known at publish time.
54
+
55
+ ## Embeddings
56
+
57
+ Apps can maintain vector embeddings over their own workspace data declaratively. You ship an `embeddings/` folder with **one file per use case**; the Informer platform acts as an *embedding pump* that asks your App what is pending, chunks and embeds the content in billed batches, and hands the vectors back for your App to store in its own workspace tables. You never call an embedding provider yourself, and the platform never keeps a copy of your corpus — your own tables are the ledger.
58
+
59
+ Each use case file exports `GET` and `POST` (a missing handler fails the deploy) and, optionally, a `config` literal; omitted keys take the defaults below, and a use case with no triggers in its config only runs on deploy and on a manual run:
60
+
61
+ ```javascript
62
+ // embeddings/tickets.js
63
+ export const config = { chunking: 'prose', on: ['ticket.created'], cron: '0 * * * *', revision: 1 };
64
+
65
+ // GET: the rows that still need embedding. The query IS the watermark:
66
+ // never embedded, stale, or written under an older revision.
67
+ export async function GET({ query, batch }) {
68
+ return await query(`
69
+ SELECT t.id, t.subject || ' ' || t.body AS content
70
+ FROM tickets t
71
+ LEFT JOIN ticket_embeddings e ON e.ticket_id = t.id AND e.seq = 0
72
+ WHERE e.ticket_id IS NULL OR e.embedded_at < t.updated_at OR e.revision <> $2
73
+ ORDER BY t.id
74
+ LIMIT $1
75
+ `, [batch.limit, batch.revision]);
76
+ }
77
+
78
+ // POST: store the finished vectors wherever your migrations put them.
79
+ export async function POST({ query, batch }) {
80
+ for (const doc of batch.docs) {
81
+ await query('DELETE FROM ticket_embeddings WHERE ticket_id = $1', [doc.id]);
82
+ for (const c of doc.chunks) {
83
+ await query(
84
+ 'INSERT INTO ticket_embeddings (ticket_id, seq, embedding, content, revision) VALUES ($1, $2, $3::vector, $4, $5)',
85
+ [doc.id, c.seq, JSON.stringify(c.embedding), c.content, batch.revision]
86
+ );
87
+ }
88
+ }
89
+ return { stored: batch.docs.length };
90
+ }
91
+ ```
92
+
93
+ How it works, briefly:
94
+
95
+ - **Your storage is the ledger.** Because `GET` anti-joins your own embedding table, the pump is idempotent and crash-tolerant: a run that dies mid-way finds the same rows pending next time.
96
+ - **Triggers converge.** Deploy pokes each use case (initial backfill), `emit()` events matching `on:` poke, `cron` desugars into a scheduled poke, and the platform sweep runs whatever is poked. A manual run claims the same single-flight lease directly, so nothing double-embeds.
97
+ - **Failures are bounded.** A run that fails for a reason the next attempt might not hit (a provider blip, a handler timeout) retries with a doubling delay and is parked after five consecutive failures; a run that cannot succeed as-is (a rejected key, an exhausted budget, a contract violation) is parked at once. Every park keeps the error on the use case's status row; the next trigger gives it one more attempt, and a successful run resets the count.
98
+ - **`revision` is the invalidation.** The string the pump hands you couples your config (chunking profile, budgets, `config.revision`) to the resolved platform embedding model. Store it beside each vector and compare it in `GET`: bumping the config, changing chunking, or an admin repointing the embedding model all surface as pending re-embed work automatically.
99
+ - **Unembeddable documents are tombstoned** platform-side by content hash and re-reported with `skipped: true` on every run — stamp them in your own table and exclude them from `GET`.
100
+ - **Workspace pgvector.** Apps with the embeddings capability get the `vector` extension provisioned where the server may create extensions (otherwise the deploy log names the cause and a migration using the `vector` type fails), so migrations can declare real `vector(...)` columns; `ON DELETE CASCADE` foreign keys are the cleanup story.
101
+ - **Query time.** Every handler bag (`server/`, `webhooks/`, `tools/`, `mcp/`) gets `embed(name, text)`, which returns `{ embedding, revision }`. `name` must be a declared use case. Filter on the revision as well as ordering by distance — an admin repointing the embedding model puts your query vectors in a new space while the stored rows are still in the old one, and an unfiltered `ORDER BY` then returns near-random neighbours with no error:
102
+
103
+ ```javascript
104
+ const { embedding, revision } = await embed('tickets', q);
105
+ const hits = await query(
106
+ `SELECT ticket_id, content
107
+ FROM ticket_embeddings
108
+ WHERE revision = $2
109
+ ORDER BY embedding <=> $1::vector
110
+ LIMIT 10`,
111
+ [JSON.stringify(embedding), revision]
112
+ );
113
+ ```
114
+ - **Billing.** Embedding calls bill to the App, one usage entry per batch of up to 128 chunks regardless of how many provider requests run underneath; pump compute meters against the App's compute budget.
115
+ - **`config` is read as a literal.** Deploy lifts `export const config = { … }` out of the source and evaluates it with none of the file's imports in scope, so it must be self-contained: no imported constants, no spreads of imported objects, no `export { config }` re-export. Other modules may import the literal from the use-case file; the reverse fails the deploy. The same self-contained-literal rule applies to `export const config`, `schema`, and `description` in `server/`, `webhooks/`, `tools/`, and `mcp/`; there a `config` violation fails the deploy while a `schema` or `description` violation is a deploy warning (nothing is published for that export).
116
+
117
+ ### Dev loop
118
+
119
+ `npm run deploy` uploads `embeddings/` like any other source tree — there is no manifest block to add (plugin ≥ 2.10.0; earlier versions never upload the folder, so the use case silently does not exist on the server). `npm run dev` never runs the pump, and its `embed()` throws an explanation rather than embedding (the dev mirror reports `platform.capabilities.embeddings: false`), so a search route is exercised against a deployed App. Feature-detect with `platform?.capabilities?.embeddings` — `typeof embed === 'function'` is true on both, since the binding exists either way. Then, in the App admin panel, the **Embeddings** tab lists each use case with its pump status (queued, running, up to date, last run, last error, skipped docs) and a **Run now** action; the same surface exists as API routes (`GET /apps/{id}/embeddings`, `POST /apps/{id}/embeddings/{name}/_run`). Deploy, run, read the error, fix, repeat.
120
+
121
+ ### Upgrading an App that already has an `embeddings/` folder
122
+
123
+ `embeddings/` is a server-side folder from this release on, like `server/`: its files are uploaded with the library, never served to browsers, and scanned as pump handlers. An App that kept anything else there (assets, data files) loses those files in the browser after redeploying with plugin 2.10.0 or later. A `.js` file there that exports `GET` or `POST` but not both fails the deploy; one that exports neither is never scanned as a use case at all and lands as a warning on an otherwise successful deploy. Move such content elsewhere before upgrading; the deploy names every stray entry as a warning.
124
+
125
+ ### Older Informer releases
126
+
127
+ The `embeddings/` folder, `embed()`, and the pump routes exist only on Informer releases that ship the feature. An App that must also run on older servers feature-detects with optional chaining (`platform?.capabilities?.embeddings` on the handler bag, `window.__INFORMER__.platform?.capabilities?.embeddings` in the browser) — releases before 2026.1.3 inject no `platform` descriptor at all, so a bare `platform.capabilities.embeddings` throws there instead of reporting `false`. It gates its vector DDL with a `-- requires: embeddings` header so the migration is skipped where pgvector is absent, and states a floor in `informer.yaml` (`requires: { informer: '>=…' }`) only if it cannot work without the feature.
128
+
129
+ Full reference: the **Embeddings** article under Developer → API → App in the Informer docs.
package/bin/ci.js CHANGED
@@ -14,7 +14,9 @@
14
14
  * INFORMER_MARKETPLACES: ${{ vars.INFORMER_MARKETPLACES }}
15
15
  *
16
16
  * Everything interesting lives here rather than in YAML, so fixing it is an
17
- * npm release rather than a pull request into every customer's repo.
17
+ * npm release rather than a pull request into every customer's repo. And
18
+ * everything testable lives in src/fan-out.js rather than here — this file
19
+ * is only argv/env handling, console output, and the exit code.
18
20
  *
19
21
  * For a single marketplace with a `lmpub_` key — locally, or on CI that is not
20
22
  * GitHub Actions — use `informer-publish` instead. This command needs the OIDC
@@ -24,8 +26,8 @@
24
26
  import { appendFile } from 'node:fs/promises';
25
27
  import { resolve } from 'node:path';
26
28
  import { publish } from '../src/publish.js';
27
- import { preparePublish, tagVersion } from '../src/publish-payload.js';
28
- import { parseMarketplaces, normalizeMarketplaceUrl } from '../src/marketplaces.js';
29
+ import { preparePublish, tagVersion, isVersionTag, looksLikeVersion } from '../src/publish-payload.js';
30
+ import { resolveTargets, fanOut, summaryTable, failedTargets } from '../src/fan-out.js';
29
31
  import { canMintIdToken, mintIdToken } from '../src/github-oidc.js';
30
32
  import { loadEnv, parseModeArg } from '../src/env.js';
31
33
 
@@ -34,12 +36,12 @@ loadEnv({ mode });
34
36
 
35
37
  const projectRoot = resolve('.');
36
38
 
37
- // --- Targets: explicit flags replace the configured list entirely, so a
38
- // workflow_dispatch can re-publish to a subset without editing config. ---
39
39
  let targets;
40
40
  try {
41
- const explicit = argValues('--marketplace').map(normalizeMarketplaceUrl);
42
- targets = explicit.length ? [...new Set(explicit)] : parseMarketplaces(process.env.INFORMER_MARKETPLACES);
41
+ targets = resolveTargets({
42
+ explicit: argValues('--marketplace'),
43
+ configured: process.env.INFORMER_MARKETPLACES
44
+ });
43
45
  } catch (err) {
44
46
  fail(err.message);
45
47
  }
@@ -60,13 +62,28 @@ if (!canMintIdToken()) {
60
62
  );
61
63
  }
62
64
 
63
- // --- Build the payload ONCE. Every marketplace gets identical bytes. ---
65
+ // The version comes from the tag that triggered the run. On a branch push or
66
+ // a workflow_dispatch against a branch, GITHUB_REF_NAME is the branch name —
67
+ // and without this guard the run would publish a version literally called
68
+ // "main", on the STABLE channel (no prerelease dash), to every marketplace.
69
+ // The predicate is shared with informer-publish (src/publish-payload.js).
70
+ const explicitVersion = argValue('--version');
71
+ if (explicitVersion && !looksLikeVersion(explicitVersion)) {
72
+ fail(`--version "${explicitVersion}" doesn't look like a version (x.y.z, or x.y.z-beta.n).`);
73
+ }
74
+ if (!explicitVersion && !isVersionTag(process.env.GITHUB_REF_TYPE, process.env.GITHUB_REF_NAME)) {
75
+ fail(
76
+ `informer-ci publishes the version named by the tag that triggered the run, ` +
77
+ `but this run's ref is ${process.env.GITHUB_REF_TYPE || 'ref'} "${process.env.GITHUB_REF_NAME || '(unset)'}" — not a version tag.\n` +
78
+ ' Run the workflow against a vX.Y.Z tag, or pass --version <x.y.z>.'
79
+ );
80
+ }
81
+ const refVersion = tagVersion(process.env.GITHUB_REF_NAME);
82
+
83
+ // --- Build the payload once: every marketplace gets the same file set. ---
64
84
  let payload;
65
85
  try {
66
- payload = await preparePublish({
67
- projectRoot,
68
- version: argValue('--version') || tagVersion(process.env.GITHUB_REF_NAME)
69
- });
86
+ payload = await preparePublish({ projectRoot, version: explicitVersion || refVersion });
70
87
  } catch (err) {
71
88
  fail(err.message);
72
89
  }
@@ -79,28 +96,30 @@ console.log(
79
96
  `${files.length} files${screenshots.length ? `, ${screenshots.length} screenshot${screenshots.length === 1 ? '' : 's'}` : ''}`
80
97
  );
81
98
 
82
- // --- Fan out. One target failing must not stop the others: a marketplace that
83
- // is down should not withhold the release from the ones that are up. ---
84
- const results = [];
85
- for (const marketplaceUrl of targets) {
86
- const label = hostOf(marketplaceUrl);
87
- try {
88
- // Audience is the marketplace itself, so a token minted for one
89
- // License Manager is not replayable at another.
90
- const token = await mintIdToken(marketplaceUrl);
91
- const result = await publish({ marketplaceUrl, token, files, icon, screenshots, fields: payload.fields });
99
+ const results = await fanOut({
100
+ targets,
101
+ mint: mintIdToken,
102
+ publishOne: ({ marketplaceUrl, token }) =>
103
+ publish({ marketplaceUrl, token, files, icon, screenshots, fields: payload.fields }),
104
+ onSuccess: (label, result) => {
92
105
  for (const w of result.warnings || []) console.warn(` warning (${label}): ${w}`);
93
106
  console.log(` ✓ ${label} — published ${slug} v${version} (${result.channel || channel})`);
94
- results.push({ label, ok: true });
95
- } catch (err) {
96
- console.error(` ✗ ${label} — ${err.message}`);
97
- results.push({ label, ok: false, error: err.message });
107
+ },
108
+ onFailure: (label, message) => {
109
+ console.error(` ✗ ${label} — ${message}`);
98
110
  }
99
- }
111
+ });
100
112
 
101
113
  await writeSummary(results);
102
114
 
103
- const failed = results.filter(r => !r.ok);
115
+ // A row that published but whose reporting threw: the ✓ line above is missing
116
+ // for it, so say what happened rather than leaving a silent gap. Not a failure
117
+ // — the release is live, and exiting 1 here invites a re-run that 409s.
118
+ for (const r of results.filter(r => r.reportError)) {
119
+ console.warn(` warning (${r.label}): published, but reporting failed — ${r.reportError}`);
120
+ }
121
+
122
+ const failed = failedTargets(results);
104
123
  if (failed.length) {
105
124
  console.error(`\n${failed.length} of ${results.length} marketplaces failed.`);
106
125
  process.exit(1);
@@ -122,32 +141,14 @@ function argValues(flag) {
122
141
  return out;
123
142
  }
124
143
 
125
- function hostOf(url) {
126
- try {
127
- return new URL(url).host;
128
- } catch {
129
- return url;
130
- }
131
- }
132
-
133
- /**
134
- * A per-target line in the job summary. Without this the Actions UI shows one
135
- * opaque step for what is really N independent publishes — the observability
136
- * the old matrix-of-jobs gave up when fan-out moved in here.
137
- */
138
144
  async function writeSummary(rows) {
139
145
  const path = process.env.GITHUB_STEP_SUMMARY;
140
146
  if (!path || !rows.length) return;
141
- const body = [
142
- `### ${name} v${version} (${channel})`,
143
- '',
144
- '| Marketplace | Result |',
145
- '| --- | --- |',
146
- ...rows.map(r => `| ${r.label} | ${r.ok ? 'published' : `failed — ${r.error.replace(/\|/g, '\\|')}`} |`),
147
- ''
148
- ].join('\n');
149
- await appendFile(path, body).catch(() => {
150
- // A summary is a nicety; never fail a good publish over it.
147
+ const body = summaryTable({ title: `${name} v${version} (${channel})`, rows });
148
+ await appendFile(path, body).catch(err => {
149
+ // A summary is a nicety; never fail a good publish over it — but say
150
+ // why it's missing rather than leaving its absence a mystery.
151
+ console.warn(` warning: could not write the job summary: ${err.message}`);
151
152
  });
152
153
  }
153
154
 
package/bin/publish.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { resolve } from 'node:path';
12
12
  import { publish } from '../src/publish.js';
13
- import { preparePublish, tagVersion } from '../src/publish-payload.js';
13
+ import { preparePublish, tagVersion, isVersionTag } from '../src/publish-payload.js';
14
14
  import { loadEnv, parseModeArg } from '../src/env.js';
15
15
 
16
16
  const mode = parseModeArg(process.argv);
@@ -39,12 +39,19 @@ if (process.env.INFORMER_MARKETPLACES && process.env.GITHUB_ACTIONS) {
39
39
 
40
40
  const projectRoot = resolve('.');
41
41
 
42
+ // Prefer an explicit --version, then the git tag (CI: GITHUB_REF_NAME=vX.Y.Z),
43
+ // then package.json. A branch ref is NOT a version tag: without the gate, a
44
+ // branch push on Actions published a version literally called "main" — so a
45
+ // non-tag ref falls through to package.json, and says so.
46
+ const refIsTag = isVersionTag(process.env.GITHUB_REF_TYPE, process.env.GITHUB_REF_NAME);
47
+ if (process.env.GITHUB_REF_NAME && !refIsTag && !argValue('--version')) {
48
+ console.warn(`Note: ref "${process.env.GITHUB_REF_NAME}" is not a version tag — using the package.json version.`);
49
+ }
50
+
42
51
  let payload;
43
52
  try {
44
53
  payload = await preparePublish({
45
- // Prefer an explicit --version, then the git tag (CI: GITHUB_REF_NAME=vX.Y.Z),
46
- // then package.json.
47
- version: argValue('--version') || tagVersion(process.env.GITHUB_REF_NAME),
54
+ version: argValue('--version') || (refIsTag ? tagVersion(process.env.GITHUB_REF_NAME) : undefined),
48
55
  projectRoot
49
56
  });
50
57
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrinsik/vite-plugin-informer",
3
- "version": "2.7.0-beta.0",
3
+ "version": "2.10.0",
4
4
  "description": "Vite plugin and deploy tool for Informer App development",
5
5
  "scripts": {
6
6
  "test": "node --test"
package/src/agent-dev.js CHANGED
@@ -2,7 +2,18 @@ import { readFile, readdir, stat, access } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { parse as parseUrl } from 'node:url';
4
4
  import yaml from 'yaml';
5
- import { loadDependencies, buildDevContext, loadAppEnv, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
5
+ import { manifestBlock, buildDevContext, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
6
+ import { createDevChannels, createDevEmit } from './dev-channels.js';
7
+ import { devPlatform } from './dev-platform.js';
8
+
9
+ // embed() is present on a real install even without the embeddings
10
+ // capability, where it throws a written explanation. Mirroring that here
11
+ // keeps the dev failure the same lesson as the deployed one, instead of a
12
+ // bare "embed is not a function" that reads like a missing binding.
13
+ const embed = async () => {
14
+ throw new Error('embed() is not available in the dev mirror: the embeddings capability needs a real Informer (platform.capabilities.embeddings is false)');
15
+ };
16
+
6
17
  const parseYaml = yaml.parse;
7
18
 
8
19
  const MAX_STEPS = 20;
@@ -149,9 +160,11 @@ async function readSSE(response) {
149
160
  *
150
161
  * @param {Object} viteServer - Vite dev server instance
151
162
  * @param {Object} opts - Configuration
163
+ * @param {ReturnType<typeof createDevChannels>} [opts.channels] - the dev channels hub
164
+ * behind `broadcast()` and the `channels:` relay (a private one when omitted)
152
165
  * @returns {Function} Connect middleware
153
166
  */
154
- export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken }) {
167
+ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken, channels = createDevChannels() }) {
155
168
 
156
169
  // query() — proxies to the workspace _sql endpoint (same as server-routes.js)
157
170
  async function query(sql, params) {
@@ -226,12 +239,6 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
226
239
  ? async (path, opts = {}) => await fetchAs(appAuth, path, { ...opts, headers: { ...(opts.headers || {}), 'x-informer-app-depth': '1' } })
227
240
  : null;
228
241
 
229
- // emit() — no-op in dev mode (logs to console)
230
- function emit(event, payload) {
231
- console.log(`[agent-dev] emit("${event}",`, JSON.stringify(payload), ')');
232
- return { ok: true };
233
- }
234
-
235
242
  // notify/email — delivery is a console-logged no-op in dev, but required-field
236
243
  // validation mirrors prod so a tool that passes here won't 500 in production.
237
244
  const { notify, email } = buildDevMessaging('[agent-dev]');
@@ -319,10 +326,17 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
319
326
  // Build the typed dependency context + env the same way the
320
327
  // server-routes dev middleware does, so tools using
321
328
  // `context.<slot>.<method>(...)` and `env` work locally and match
322
- // the prod sandbox bag.
323
- const deps = await loadDependencies(projectRoot);
329
+ // the prod sandbox bag. The manifest is already in hand.
330
+ const deps = manifestBlock(yaml, 'dependencies');
324
331
  const context = buildDevContext({ deps, apiFetch, devBindings, appFetch });
325
- const env = await loadAppEnv(projectRoot);
332
+ const env = manifestBlock(yaml, 'env');
333
+
334
+ // emit() writes no app_event row in dev, but still relays a
335
+ // listed event to its `channels:` channel; broadcast() publishes
336
+ // a live frame to the page (see dev-channels.js).
337
+ const manifestChannels = manifestBlock(yaml, 'channels');
338
+ const emit = createDevEmit({ channels, manifestChannels, logPrefix: '[agent-dev]' });
339
+ const { broadcast } = channels;
326
340
 
327
341
  // Load tool handlers via ssrLoadModule
328
342
  const localTools = await scanLocalTools(projectRoot);
@@ -441,7 +455,7 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
441
455
 
442
456
  if (tool) {
443
457
  try {
444
- result = await tool.handler({ args: tc.input, run: { agentName, trigger: triggerEvent }, context, query, fetch: apiFetch, emit, notify, email, crypto: cryptoHelper, markdown, log, env });
458
+ result = await tool.handler({ args: tc.input, run: { agentName, trigger: triggerEvent }, context, query, fetch: apiFetch, emit, broadcast, notify, email, embed, crypto: cryptoHelper, markdown, log, env, platform: devPlatform() });
445
459
  } catch (err) {
446
460
  console.error(`[agent-dev] Tool "${tc.name}" failed:`, err.message);
447
461
  result = { error: err.message };
package/src/assemble.js CHANGED
@@ -22,7 +22,11 @@ import { join, relative, posix } from 'node:path';
22
22
  // the Vite build lands it at the library root). Without README.md here, a
23
23
  // CLI-deployed app could never light those surfaces up via its README.
24
24
  const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml', 'API.md', 'README.md'];
25
- const SOURCE_DIRS = ['migrations', 'tools', 'mcp', 'server', 'webhooks', 'lib', 'shared'];
25
+ // channels/ holds live-channel join/leave handlers (App Channels); the server
26
+ // scans and bundles it like server/ and webhooks/. `embeddings` holds the pump
27
+ // use-case files: server-side like server/, so they ship with the library and
28
+ // the platform can scan them.
29
+ const SOURCE_DIRS = ['migrations', 'tools', 'mcp', 'server', 'webhooks', 'channels', 'embeddings', 'lib', 'shared'];
26
30
 
27
31
  // Entries never worth shipping in an app's server-side library: OS/editor
28
32
  // dotfiles (.DS_Store, .env), nested dependency trees, and test files. Applied