@junejs/server 1.0.0-dev.14 → 1.0.0-dev.17
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/dist/adapter.d.ts +7 -1
- package/dist/adapter.js +62 -5
- package/dist/agent-compile.d.ts +35 -0
- package/dist/agent-compile.js +139 -0
- package/dist/agent-discover.d.ts +3 -2
- package/dist/agent-discover.js +18 -44
- package/dist/agent-durable.d.ts +5 -2
- package/dist/agent-durable.js +18 -8
- package/dist/build.js +73 -8
- package/dist/client-bundle.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/tsconfig-jsx.d.ts +2 -1
- package/dist/tsconfig-jsx.js +1 -1
- package/package.json +13 -3
- package/src/adapter.ts +84 -3
- package/src/agent-compile.ts +245 -0
- package/src/agent-discover.ts +39 -54
- package/src/agent-durable.ts +33 -12
- package/src/build.ts +124 -9
- package/src/index.ts +8 -0
- package/src/tsconfig-jsx.ts +2 -2
package/dist/client-bundle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { rolldownCssModulesPlugin } from "./css-modules.js";
|
|
2
1
|
import { jsxTransform } from "./tsconfig-jsx.js";
|
|
2
|
+
import { rolldownCssModulesPlugin } from "./css-modules.js";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, staticSite, vercel, workers } from "./adapter.js";
|
|
2
|
+
import { AGENT_MODULE_FILE, AgentDirScan, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir } from "./agent-compile.js";
|
|
2
3
|
import { CreateAppOptions, JuneApp, createApp } from "./app.js";
|
|
3
4
|
import { R2Bucket, localBlob, r2 } from "./blob.js";
|
|
4
5
|
import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-cCR6MGNK.js";
|
|
@@ -24,4 +25,4 @@ import { Negotiated, negotiate } from "./negotiate.js";
|
|
|
24
25
|
import { blob, currentServices, db, kv } from "@junejs/db";
|
|
25
26
|
import { ServicesConfig, defineServices } from "@junejs/core/config";
|
|
26
27
|
import { I18nConfig, LOCALE_COOKIE, LocaleConfig, LocaleMatch, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
|
|
27
|
-
export { type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type ServicesConfig, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
|
|
28
|
+
export { AGENT_MODULE_FILE, type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type AgentDirScan, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type ServicesConfig, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, emitAgentModule, entry, findAgentDir, freezeConfig, generateAgentModule, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanAgentDir, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deno, staticSite, vercel, workers } from "./adapter.js";
|
|
2
|
+
import { AGENT_MODULE_FILE, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir } from "./agent-compile.js";
|
|
2
3
|
import { listRoutes, matchRoute, matchRouteTree, resolveNotFound } from "./router.js";
|
|
3
4
|
import { negotiate } from "./negotiate.js";
|
|
4
5
|
import { createPipeline } from "./pipeline.js";
|
|
@@ -24,4 +25,4 @@ import { memoryKv, redisKv } from "./kv.js";
|
|
|
24
25
|
import { blob, currentServices, db, kv } from "@junejs/db";
|
|
25
26
|
import { defineServices } from "@junejs/core/config";
|
|
26
27
|
import { LOCALE_COOKIE, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
|
|
27
|
-
export { LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
|
|
28
|
+
export { AGENT_MODULE_FILE, LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, emitAgentModule, entry, findAgentDir, freezeConfig, generateAgentModule, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanAgentDir, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
|
package/dist/tsconfig-jsx.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/tsconfig-jsx.d.ts
|
|
2
|
+
declare const stripJsonc: (s: string) => string;
|
|
2
3
|
/** Read the app's tsconfig.json and return compilerOptions.jsxImportSource.
|
|
3
4
|
* Follows one level of `extends` so apps that inherit from a base tsconfig
|
|
4
5
|
* (e.g. "@kurajs/docs/tsconfig.kura.json") are handled correctly.
|
|
@@ -12,4 +13,4 @@ declare function jsxTransform(appRoot: string): Promise<{
|
|
|
12
13
|
importSource?: string;
|
|
13
14
|
}>;
|
|
14
15
|
//#endregion
|
|
15
|
-
export { appJsxImportSource, jsxTransform };
|
|
16
|
+
export { appJsxImportSource, jsxTransform, stripJsonc };
|
package/dist/tsconfig-jsx.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.17",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"types": "./dist/agent-discover.d.ts",
|
|
37
37
|
"default": "./dist/agent-discover.js"
|
|
38
38
|
},
|
|
39
|
+
"./agent-compile": {
|
|
40
|
+
"source": "./src/agent-compile.ts",
|
|
41
|
+
"bun": "./src/agent-compile.ts",
|
|
42
|
+
"types": "./dist/agent-compile.d.ts",
|
|
43
|
+
"default": "./dist/agent-compile.js"
|
|
44
|
+
},
|
|
39
45
|
"./db": {
|
|
40
46
|
"source": "./src/db.ts",
|
|
41
47
|
"bun": "./src/db.ts",
|
|
@@ -132,13 +138,14 @@
|
|
|
132
138
|
"rolldown": "^1.1.0"
|
|
133
139
|
},
|
|
134
140
|
"peerDependencies": {
|
|
135
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
141
|
+
"@junejs/core": ">=0.2.0-dev.33",
|
|
136
142
|
"react": "^19.2.0",
|
|
137
143
|
"react-dom": "^19.2.0",
|
|
138
144
|
"react-server-dom-webpack": "19.2.7",
|
|
139
145
|
"pg": "^8.0.0",
|
|
140
146
|
"mysql2": "^3.0.0",
|
|
141
|
-
"@libsql/client": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0"
|
|
147
|
+
"@libsql/client": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
|
|
148
|
+
"@anthropic-ai/sdk": ">=0.60.0"
|
|
142
149
|
},
|
|
143
150
|
"peerDependenciesMeta": {
|
|
144
151
|
"react-server-dom-webpack": {
|
|
@@ -152,6 +159,9 @@
|
|
|
152
159
|
},
|
|
153
160
|
"@libsql/client": {
|
|
154
161
|
"optional": true
|
|
162
|
+
},
|
|
163
|
+
"@anthropic-ai/sdk": {
|
|
164
|
+
"optional": true
|
|
155
165
|
}
|
|
156
166
|
},
|
|
157
167
|
"repository": {
|
package/src/adapter.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// `workers()` is the zero-config default (this file, no import). Other targets
|
|
8
8
|
// are explicit `deploy: { adapter: vercel() }` from their own package.
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { cp, copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { cp, copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { basename, dirname, join } from "node:path";
|
|
12
12
|
|
|
13
13
|
import type { JuneConfig } from "@junejs/core/config";
|
|
14
|
+
import { stripJsonc } from "./tsconfig-jsx";
|
|
14
15
|
|
|
15
16
|
export type AdapterCapabilities = {
|
|
16
17
|
runtime: "edge" | "node" | "serverless" | "native" | "static";
|
|
@@ -20,6 +21,9 @@ export type AdapterCapabilities = {
|
|
|
20
21
|
persistentConnections: boolean;
|
|
21
22
|
// Who serves prerendered static files: the platform CDN, the server, or none.
|
|
22
23
|
assets: "platform" | "server" | "none";
|
|
24
|
+
// Durable Objects (the per-session agent actor). Only workers() today; the
|
|
25
|
+
// build skips the durable-agent mount (with a notice) when absent.
|
|
26
|
+
durableObjects?: boolean;
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
// What the adapter contributes to the generated worker entry: extra imports and
|
|
@@ -37,6 +41,11 @@ export type ResourcePlan = {
|
|
|
37
41
|
// A declared `db` → a D1 binding named `binding`. The runtime provider
|
|
38
42
|
// (bindWorkerResources) reads env[binding]; keep in sync (default "DB").
|
|
39
43
|
db?: { binding: string; databaseName: string };
|
|
44
|
+
// A durable agent (an app/agent directory on a DO-capable target) → the DO
|
|
45
|
+
// namespace binding createWorker reads (env.AGENT) and the class the
|
|
46
|
+
// generated entry exports. workers() emits durable_objects + the SQLite-class
|
|
47
|
+
// migration from it.
|
|
48
|
+
agent?: { binding: string; className: string };
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
export type AdapterEmitContext = {
|
|
@@ -77,13 +86,60 @@ export interface JuneAdapter {
|
|
|
77
86
|
emit(ctx: AdapterEmitContext): Promise<void>;
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
// Whether an app-owned wrangler config actually BINDS the generated DO class
|
|
90
|
+
// under the binding name createWorker reads (env.AGENT) — not merely mentions
|
|
91
|
+
// the class (a migrations entry does that too, and a binding under another
|
|
92
|
+
// name never reaches the chat route). JSONC is parsed for the real
|
|
93
|
+
// durable_objects.bindings entry; TOML is scanned per
|
|
94
|
+
// [[durable_objects.bindings]] table with comments stripped, so a
|
|
95
|
+
// commented-out binding never counts. The TOML scan covers wrangler's
|
|
96
|
+
// canonical table form; an exotic inline-table config may draw a spurious
|
|
97
|
+
// (advisory) warning, never a suppressed one. Pure — unit-tested directly.
|
|
98
|
+
export function hasDurableBinding(text: string, file: string, binding: string, className: string): boolean {
|
|
99
|
+
if (file.endsWith(".toml")) {
|
|
100
|
+
// Strip comments: '#' to end of line, unless inside a double-quoted string.
|
|
101
|
+
const lines = text.split("\n").map((line) => {
|
|
102
|
+
let inString = false;
|
|
103
|
+
for (let i = 0; i < line.length; i++) {
|
|
104
|
+
const ch = line[i];
|
|
105
|
+
if (ch === '"' && line[i - 1] !== "\\") inString = !inString;
|
|
106
|
+
else if (ch === "#" && !inString) return line.slice(0, i);
|
|
107
|
+
}
|
|
108
|
+
return line;
|
|
109
|
+
});
|
|
110
|
+
let inBindings = false;
|
|
111
|
+
let name: string | undefined;
|
|
112
|
+
let cls: string | undefined;
|
|
113
|
+
const matches = () => name === binding && cls === className;
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const t = line.trim();
|
|
116
|
+
if (t.startsWith("[")) {
|
|
117
|
+
if (inBindings && matches()) return true; // the table that just ended bound it
|
|
118
|
+
inBindings = /^\[\[\s*durable_objects\.bindings\s*\]\]$/.test(t);
|
|
119
|
+
name = cls = undefined;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!inBindings) continue;
|
|
123
|
+
const kv = t.match(/^(name|class_name)\s*=\s*"([^"]*)"/);
|
|
124
|
+
if (kv) kv[1] === "name" ? (name = kv[2]) : (cls = kv[2]);
|
|
125
|
+
}
|
|
126
|
+
return inBindings && matches();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(stripJsonc(text)) as { durable_objects?: { bindings?: { name?: string; class_name?: string }[] } };
|
|
130
|
+
return (parsed.durable_objects?.bindings ?? []).some((b) => b.name === binding && b.class_name === className);
|
|
131
|
+
} catch {
|
|
132
|
+
return text.includes(className); // unparsable — loose fallback, never crash the build
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
80
136
|
// The built-in default. Reproduces exactly what build.ts emitted inline:
|
|
81
137
|
// withAssets(pipeline) as the entry, and a wrangler.jsonc with the assets
|
|
82
138
|
// binding (run_worker_first) + optional custom domain.
|
|
83
139
|
export function workers(opts?: { name?: string; domain?: string }): JuneAdapter {
|
|
84
140
|
return {
|
|
85
141
|
name: "workers",
|
|
86
|
-
capabilities: { runtime: "edge", persistentConnections: true, assets: "platform" },
|
|
142
|
+
capabilities: { runtime: "edge", persistentConnections: true, assets: "platform", durableObjects: true },
|
|
87
143
|
conditions: ["source", "workerd", "edge", "import", "default"],
|
|
88
144
|
// workers-og is a workerd-native WASM package: rolldown cannot bundle its
|
|
89
145
|
// .wasm assets (wrangler's own bundler applies the CompiledWasm rule). Mark
|
|
@@ -100,7 +156,23 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
100
156
|
|
|
101
157
|
async emit({ appRoot, outDir, hasAssets, config, plan, defaultName }) {
|
|
102
158
|
// An app that manages its own wrangler config wins — don't overwrite it.
|
|
103
|
-
|
|
159
|
+
// But a durable agent NEEDS its DO binding: warn when the app-owned config
|
|
160
|
+
// doesn't BIND the generated class under the name createWorker reads
|
|
161
|
+
// (hasDurableBinding above — a migrations-only mention, a binding under
|
|
162
|
+
// another name, or a commented-out table must all still warn), instead of
|
|
163
|
+
// silently shipping a worker whose chat surface 404s.
|
|
164
|
+
const own = ["wrangler.toml", "wrangler.jsonc"].map((f) => join(appRoot, f)).find(existsSync);
|
|
165
|
+
if (own) {
|
|
166
|
+
if (plan.agent) {
|
|
167
|
+
const cls = plan.agent.className;
|
|
168
|
+
const bound = hasDurableBinding(await readFile(own, "utf8"), own, plan.agent.binding, cls);
|
|
169
|
+
if (!bound) {
|
|
170
|
+
const snippet = own.endsWith(".toml")
|
|
171
|
+
? `[[durable_objects.bindings]]\nname = "${plan.agent.binding}"\nclass_name = "${cls}"\n\n[[migrations]]\ntag = "v1"\nnew_sqlite_classes = ["${cls}"]`
|
|
172
|
+
: `"durable_objects": { "bindings": [{ "name": "${plan.agent.binding}", "class_name": "${cls}" }] },\n"migrations": [{ "tag": "v1", "new_sqlite_classes": ["${cls}"] }]`;
|
|
173
|
+
console.warn(`[june build] durable agent built, but ${basename(own)} doesn't bind it — add:\n${snippet}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
104
176
|
return;
|
|
105
177
|
}
|
|
106
178
|
const deployCfg = config.deploy;
|
|
@@ -133,6 +205,15 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
133
205
|
],
|
|
134
206
|
}
|
|
135
207
|
: {}),
|
|
208
|
+
// The durable agent: one SQLite-backed DO class = the per-session
|
|
209
|
+
// AgentSession actor (its ctx.storage.sql is the durable loop's
|
|
210
|
+
// synchronous store). The binding name is what createWorker reads.
|
|
211
|
+
...(plan.agent
|
|
212
|
+
? {
|
|
213
|
+
durable_objects: { bindings: [{ name: plan.agent.binding, class_name: plan.agent.className }] },
|
|
214
|
+
migrations: [{ tag: "v1", new_sqlite_classes: [plan.agent.className] }],
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
136
217
|
// config deploy.domain → a Workers custom domain; without it the
|
|
137
218
|
// regenerated file would silently drop a hand-attached domain.
|
|
138
219
|
...(opts?.domain ?? deployCfg?.domain
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// agent-compile.ts — compile an agent/ directory into a static module
|
|
2
|
+
// (_agent.gen.ts) so the SAME directory convention mounts where filesystem
|
|
3
|
+
// discovery cannot run: workerd has no fs, so discoverAgent (dynamic import +
|
|
4
|
+
// readFile) is native-only. The generated module is plain erasable TypeScript —
|
|
5
|
+
// static imports of tools/channels/connections, markdown inlined as string
|
|
6
|
+
// literals — bundleable by wrangler or Rolldown, importable under Node type
|
|
7
|
+
// stripping and bun test alike. No [[rules]] Text hack, no md loader plugins,
|
|
8
|
+
// no hand-maintained tool registry.
|
|
9
|
+
//
|
|
10
|
+
// Mirrors the content freeze (content.ts): a pure emitter (emitAgentModule)
|
|
11
|
+
// over a plain scan (scanAgentDir), with a thin fs wrapper
|
|
12
|
+
// (generateAgentModule) — the emitter is unit-testable without touching disk.
|
|
13
|
+
// Assembly logic is NOT duplicated here: the generated module exports the raw
|
|
14
|
+
// AgentModule shape, and both runtimes assemble it through
|
|
15
|
+
// @junejs/core/agent-config (assembleAgent natively, assembleDurable in a DO
|
|
16
|
+
// shell). Skill files are inlined raw and parsed at module init via core's
|
|
17
|
+
// parseSkill, so a parser improvement never requires regeneration.
|
|
18
|
+
|
|
19
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { stripJsonc } from "./tsconfig-jsx";
|
|
23
|
+
|
|
24
|
+
export const AGENT_MODULE_FILE = "_agent.gen.ts";
|
|
25
|
+
|
|
26
|
+
// The raw scan of an agent directory: file paths for code (imported statically
|
|
27
|
+
// by the emitted module), file contents for prose (inlined as literals).
|
|
28
|
+
export type AgentDirScan = {
|
|
29
|
+
dir: string;
|
|
30
|
+
// basename fallback when agent.ts is absent (mirrors discoverAgent).
|
|
31
|
+
name: string;
|
|
32
|
+
hasConfig: boolean;
|
|
33
|
+
instructions: string | null;
|
|
34
|
+
tools: string[]; // relative specifiers, e.g. "./tools/create_order.ts"
|
|
35
|
+
skills: { name: string; raw: string }[];
|
|
36
|
+
channels: string[]; // "./channels/slack.ts"
|
|
37
|
+
channelOverlays: { source: string; raw: string }[]; // channels/<source>.md
|
|
38
|
+
connections: string[]; // "./connections/teachify.ts"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// `_`-prefixed files are private by convention — most importantly the generated
|
|
42
|
+
// module itself, which must never be scanned back in. Sorted for determinism.
|
|
43
|
+
function list(dir: string, ext: string): string[] {
|
|
44
|
+
if (!existsSync(dir)) return [];
|
|
45
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
46
|
+
.filter((e) => e.isFile() && e.name.endsWith(ext) && !e.name.startsWith("_"))
|
|
47
|
+
.map((e) => e.name)
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Scan an agent directory (fs only — no dynamic imports; the point is to defer
|
|
52
|
+
// module loading to the bundler). Returns null when the directory holds nothing
|
|
53
|
+
// agent-shaped, so callers can probe candidate paths cheaply.
|
|
54
|
+
export function scanAgentDir(dir: string): AgentDirScan | null {
|
|
55
|
+
if (!existsSync(dir)) return null;
|
|
56
|
+
const instructionsFile = join(dir, "instructions.md");
|
|
57
|
+
const scan: AgentDirScan = {
|
|
58
|
+
dir,
|
|
59
|
+
name: basename(dir),
|
|
60
|
+
hasConfig: existsSync(join(dir, "agent.ts")),
|
|
61
|
+
instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
|
|
62
|
+
tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
|
|
63
|
+
skills: list(join(dir, "skills"), ".md").map((f) => ({
|
|
64
|
+
name: basename(f, ".md"),
|
|
65
|
+
raw: readFileSync(join(dir, "skills", f), "utf8"),
|
|
66
|
+
})),
|
|
67
|
+
channels: list(join(dir, "channels"), ".ts").map((f) => `./channels/${f}`),
|
|
68
|
+
channelOverlays: list(join(dir, "channels"), ".md").map((f) => ({
|
|
69
|
+
source: basename(f, ".md"),
|
|
70
|
+
raw: readFileSync(join(dir, "channels", f), "utf8"),
|
|
71
|
+
})),
|
|
72
|
+
connections: list(join(dir, "connections"), ".ts").map((f) => `./connections/${f}`),
|
|
73
|
+
};
|
|
74
|
+
const empty =
|
|
75
|
+
!scan.hasConfig &&
|
|
76
|
+
scan.instructions === null &&
|
|
77
|
+
!scan.tools.length &&
|
|
78
|
+
!scan.skills.length &&
|
|
79
|
+
!scan.channels.length &&
|
|
80
|
+
!scan.channelOverlays.length &&
|
|
81
|
+
!scan.connections.length;
|
|
82
|
+
return empty ? null : scan;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A stable, readable import identifier from a file specifier: "./tools/create_order.ts"
|
|
86
|
+
// → "tool_create_order". Collisions (foo-bar vs foo_bar) get a numeric suffix.
|
|
87
|
+
function identFor(prefix: string, spec: string, taken: Set<string>): string {
|
|
88
|
+
const stem = basename(spec).replace(/\.ts$/, "").replace(/[^A-Za-z0-9]/g, "_");
|
|
89
|
+
let id = `${prefix}_${stem}`;
|
|
90
|
+
for (let i = 2; taken.has(id); i++) id = `${prefix}_${stem}_${i}`;
|
|
91
|
+
taken.add(id);
|
|
92
|
+
return id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type EmitOptions = {
|
|
96
|
+
// Keep ".ts" on relative import specifiers. Required by consumers that run
|
|
97
|
+
// the module under Node's native type stripping (Node never resolves
|
|
98
|
+
// extensionless imports); rejected (TS5097) by tsconfigs without
|
|
99
|
+
// allowImportingTsExtensions. generateAgentModule sniffs the consumer's
|
|
100
|
+
// tsconfig; pass explicitly to override.
|
|
101
|
+
tsExtensions?: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Emit the module source for a scan. Pure — the golden test pins this output.
|
|
105
|
+
export function emitAgentModule(scan: AgentDirScan, opts?: EmitOptions): string {
|
|
106
|
+
const spec = (s: string) => JSON.stringify(opts?.tsExtensions ? s : s.replace(/\.ts$/, ""));
|
|
107
|
+
const taken = new Set<string>();
|
|
108
|
+
const imports: string[] = [];
|
|
109
|
+
const toolIds = scan.tools.map((s) => {
|
|
110
|
+
const id = identFor("tool", s, taken);
|
|
111
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
112
|
+
return id;
|
|
113
|
+
});
|
|
114
|
+
const channelIds = scan.channels.map((s) => {
|
|
115
|
+
const id = identFor("channel", s, taken);
|
|
116
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
117
|
+
return { key: basename(s, ".ts"), id };
|
|
118
|
+
});
|
|
119
|
+
const connectionIds = scan.connections.map((s) => {
|
|
120
|
+
const id = identFor("connection", s, taken);
|
|
121
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
122
|
+
return id;
|
|
123
|
+
});
|
|
124
|
+
if (scan.hasConfig) imports.push(`import config from ${spec("./agent.ts")};`);
|
|
125
|
+
|
|
126
|
+
const lines: string[] = [
|
|
127
|
+
"// AUTO-GENERATED by June — do not edit. Compiled from this agent/ directory so",
|
|
128
|
+
"// the definition mounts without filesystem discovery (workerd has no fs).",
|
|
129
|
+
"// Regenerate with `june gen` after adding, removing, or editing agent files.",
|
|
130
|
+
...imports,
|
|
131
|
+
scan.skills.length
|
|
132
|
+
? `import { parseSkill, type AgentModule } from "@junejs/core/agent-config";`
|
|
133
|
+
: `import type { AgentModule } from "@junejs/core/agent-config";`,
|
|
134
|
+
"",
|
|
135
|
+
];
|
|
136
|
+
if (!scan.hasConfig) lines.push(`const config = { name: ${JSON.stringify(scan.name)} };`, "");
|
|
137
|
+
|
|
138
|
+
lines.push(
|
|
139
|
+
"export const agentModule: AgentModule = {",
|
|
140
|
+
" config,",
|
|
141
|
+
` instructions: ${JSON.stringify(scan.instructions ?? "")},`,
|
|
142
|
+
` tools: [${toolIds.join(", ")}],`,
|
|
143
|
+
scan.skills.length
|
|
144
|
+
? ` skills: [\n${scan.skills.map((s) => ` parseSkill(${JSON.stringify(s.name)}, ${JSON.stringify(s.raw)}),`).join("\n")}\n ],`
|
|
145
|
+
: " skills: [],",
|
|
146
|
+
channelIds.length
|
|
147
|
+
? ` channels: { ${channelIds.map((c) => `${JSON.stringify(c.key)}: ${c.id}`).join(", ")} },`
|
|
148
|
+
: " channels: {},",
|
|
149
|
+
scan.channelOverlays.length
|
|
150
|
+
? ` channelInstructions: {\n${scan.channelOverlays.map((o) => ` ${JSON.stringify(o.source)}: ${JSON.stringify(o.raw)},`).join("\n")}\n },`
|
|
151
|
+
: " channelInstructions: {},",
|
|
152
|
+
` connections: [${connectionIds.join(", ")}],`,
|
|
153
|
+
"};",
|
|
154
|
+
"export default agentModule;",
|
|
155
|
+
"",
|
|
156
|
+
);
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Locate the agent directory for a project root: a June app keeps it at
|
|
161
|
+
// app/<dir> (the app.ts convention); a wrangler-first worker keeps it at
|
|
162
|
+
// <root>/<dir>. `dirName` follows june.config's agent.runtime.dir (default
|
|
163
|
+
// "agent").
|
|
164
|
+
export function findAgentDir(root: string, dirName = "agent"): string | null {
|
|
165
|
+
for (const candidate of [join(root, "app", dirName), join(root, dirName)]) {
|
|
166
|
+
if (existsSync(candidate)) return candidate;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Whether the consumer's tsconfig keeps ".ts" on import specifiers
|
|
172
|
+
// (allowImportingTsExtensions): the nearest tsconfig.json walking up from the
|
|
173
|
+
// agent dir, parsed as JSONC (same stripJsonc as tsconfig-jsx.ts — a commented-
|
|
174
|
+
// out option must not count) and resolved through `extends` (relative, bare
|
|
175
|
+
// package specifiers, and the TS 5 array form; own options win, later array
|
|
176
|
+
// entries beat earlier ones). Explicit opts.tsExtensions overrides the sniff.
|
|
177
|
+
type TsconfigLite = { extends?: string | string[]; compilerOptions?: { allowImportingTsExtensions?: boolean } };
|
|
178
|
+
|
|
179
|
+
function readTsconfig(path: string): TsconfigLite | undefined {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(stripJsonc(readFileSync(path, "utf8"))) as TsconfigLite;
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined; // absent or unparsable — treat as "not declared"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Resolve an `extends` specifier from the extending config's directory: relative
|
|
188
|
+
// paths directly, bare specifiers through the nearest node_modules walking up.
|
|
189
|
+
// tsc appends ".json" when the target has no extension — mirror that.
|
|
190
|
+
function resolveExtends(fromDir: string, spec: string): string | undefined {
|
|
191
|
+
const withJson = (p: string) => (existsSync(p) ? p : !p.endsWith(".json") && existsSync(`${p}.json`) ? `${p}.json` : undefined);
|
|
192
|
+
if (spec.startsWith(".")) return withJson(resolve(fromDir, spec));
|
|
193
|
+
for (let d = fromDir; ; ) {
|
|
194
|
+
const candidate = withJson(join(d, "node_modules", spec));
|
|
195
|
+
if (candidate) return candidate;
|
|
196
|
+
const parent = dirname(d);
|
|
197
|
+
if (parent === d) return undefined;
|
|
198
|
+
d = parent;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveAllowTsExtensions(path: string, depth: number): boolean | undefined {
|
|
203
|
+
if (depth > 8) return undefined; // extends cycles/degenerate chains — give up
|
|
204
|
+
const tc = readTsconfig(path);
|
|
205
|
+
if (!tc) return undefined;
|
|
206
|
+
const own = tc.compilerOptions?.allowImportingTsExtensions;
|
|
207
|
+
if (own !== undefined) return own;
|
|
208
|
+
// TS extends-array semantics: later entries override earlier ones.
|
|
209
|
+
const bases = typeof tc.extends === "string" ? [tc.extends] : [...(tc.extends ?? [])].reverse();
|
|
210
|
+
for (const spec of bases) {
|
|
211
|
+
const base = resolveExtends(dirname(path), spec);
|
|
212
|
+
const inherited = base ? resolveAllowTsExtensions(base, depth + 1) : undefined;
|
|
213
|
+
if (inherited !== undefined) return inherited;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sniffTsExtensions(dir: string): boolean {
|
|
219
|
+
for (let d = dir; ; ) {
|
|
220
|
+
const candidate = join(d, "tsconfig.json");
|
|
221
|
+
if (existsSync(candidate)) return resolveAllowTsExtensions(candidate, 0) ?? false;
|
|
222
|
+
const parent = dirname(d);
|
|
223
|
+
if (parent === d) return false;
|
|
224
|
+
d = parent;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Generate <dir>/_agent.gen.ts. With `check: true` nothing is written — `stale`
|
|
229
|
+
// reports whether the file on disk differs from what would be generated (the CI
|
|
230
|
+
// gate; `june gen && git diff --exit-code` works too). Returns null when the
|
|
231
|
+
// directory holds nothing agent-shaped.
|
|
232
|
+
export function generateAgentModule(
|
|
233
|
+
dir: string,
|
|
234
|
+
opts?: { check?: boolean } & EmitOptions,
|
|
235
|
+
): { file: string; code: string; stale: boolean; written: boolean } | null {
|
|
236
|
+
const scan = scanAgentDir(dir);
|
|
237
|
+
if (!scan) return null;
|
|
238
|
+
const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
|
|
239
|
+
const file = join(dir, AGENT_MODULE_FILE);
|
|
240
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
241
|
+
const stale = existing !== code;
|
|
242
|
+
if (opts?.check) return { file, code, stale, written: false };
|
|
243
|
+
if (stale) writeFileSync(file, code);
|
|
244
|
+
return { file, code, stale, written: stale };
|
|
245
|
+
}
|
package/src/agent-discover.ts
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
// tools/*.ts → each default-exports a defineAction (a tool)
|
|
9
9
|
// skills/*.md → each a procedure, loaded on demand (progressive disclosure)
|
|
10
10
|
// channels/*.ts → each default-exports a Channel (an inbound edge)
|
|
11
|
+
// channels/*.md → source-keyed system overlay for the same-named channel
|
|
11
12
|
// connections/*.ts → each default-exports a Connection (an outbound tool source)
|
|
12
13
|
//
|
|
13
14
|
// There is no central registry to keep in sync — the directory IS the manifest.
|
|
14
15
|
// Because tools are `defineAction`s, the SAME directory is also an MCP server and
|
|
15
16
|
// a `.agent` manifest (surfaces land in a later step). fs lives here in the host
|
|
16
|
-
// package; the pure assembly is @junejs/core/agent-config's
|
|
17
|
+
// package; the pure assembly is @junejs/core/agent-config's assembleAgent() — the
|
|
18
|
+
// SAME entry point a compiled agent module (agent-compile.ts, the edge path) goes
|
|
19
|
+
// through, so native discovery and the generated module cannot drift.
|
|
17
20
|
|
|
18
21
|
import { existsSync } from "node:fs";
|
|
19
22
|
import { readdir, readFile } from "node:fs/promises";
|
|
@@ -21,52 +24,32 @@ import { basename, extname, join } from "node:path";
|
|
|
21
24
|
import { pathToFileURL } from "node:url";
|
|
22
25
|
|
|
23
26
|
import type { AnyAction } from "@junejs/core/agent";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
27
|
+
import { assembleAgent, parseSkill, type AgentConfigFile, type AgentDefinition, type AgentModule, type Channel, type ChannelFactory, type Skill } from "@junejs/core/agent-config";
|
|
28
|
+
import type { Connection } from "@junejs/core/connections";
|
|
26
29
|
|
|
30
|
+
// `_`-prefixed files are private by convention (mirrors the app/ router) — most
|
|
31
|
+
// importantly the generated `_agent.gen.ts` itself, which must never be scanned
|
|
32
|
+
// back in as a tool/channel.
|
|
27
33
|
async function scan(dir: string, ext: string): Promise<string[]> {
|
|
28
34
|
if (!existsSync(dir)) return [];
|
|
29
35
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
30
36
|
return entries
|
|
31
|
-
.filter((e) => e.isFile() && extname(e.name) === ext)
|
|
37
|
+
.filter((e) => e.isFile() && extname(e.name) === ext && !e.name.startsWith("_"))
|
|
32
38
|
.map((e) => join(dir, e.name))
|
|
33
39
|
.sort();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const end = text.indexOf("\n---", 3);
|
|
41
|
-
if (end !== -1) {
|
|
42
|
-
const front = text.slice(3, end);
|
|
43
|
-
const body = text.slice(end + 4).replace(/^\s*\n/, "");
|
|
44
|
-
const meta: Record<string, string> = {};
|
|
45
|
-
for (const line of front.split("\n")) {
|
|
46
|
-
const m = line.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/);
|
|
47
|
-
if (m) meta[m[1]!] = m[2]!;
|
|
48
|
-
}
|
|
49
|
-
return { name: meta.name ?? name, description: meta.description ?? "", body };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const firstLine = text.split("\n").find((l) => l.trim()) ?? "";
|
|
53
|
-
return { name, description: firstLine.replace(/^#\s*/, ""), body: text };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Discover an agent from its directory, returning the assembled AgentDefinition
|
|
57
|
-
// (tools already adapted from defineActions; read_skill added if any skills
|
|
58
|
-
// exist). Mount it on a runtime with createNativeRuntime({ [agent.name]: { model,
|
|
59
|
-
// tools: agent.tools } }).
|
|
60
|
-
export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
42
|
+
// Read an agent directory into its raw, unassembled shape (fs + dynamic import —
|
|
43
|
+
// native only). The pure sibling for the edge is agent-compile.ts, which emits
|
|
44
|
+
// the same AgentModule as static code.
|
|
45
|
+
export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
61
46
|
const configFile = join(dir, "agent.ts");
|
|
62
47
|
const config: AgentConfigFile = existsSync(configFile)
|
|
63
48
|
? ((await import(pathToFileURL(configFile).href)).default as AgentConfigFile)
|
|
64
49
|
: { name: basename(dir) };
|
|
65
50
|
|
|
66
51
|
const instructionsFile = join(dir, "instructions.md");
|
|
67
|
-
const instructions = existsSync(instructionsFile)
|
|
68
|
-
? await readFile(instructionsFile, "utf8")
|
|
69
|
-
: (config.instructions ?? "");
|
|
52
|
+
const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
|
|
70
53
|
|
|
71
54
|
const tools: AnyAction[] = [];
|
|
72
55
|
for (const f of await scan(join(dir, "tools"), ".ts")) {
|
|
@@ -80,34 +63,36 @@ export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
|
80
63
|
}
|
|
81
64
|
|
|
82
65
|
// A channel module default-exports a Channel OR a `(env) => Channel` factory (the
|
|
83
|
-
// form workerd needs — secrets live in env, not at module scope).
|
|
84
|
-
// process.env
|
|
85
|
-
const channels: Channel
|
|
66
|
+
// form workerd needs — secrets live in env, not at module scope). Resolution is
|
|
67
|
+
// assembleAgent's job (native: process.env; a DO: its own env) — keep raw here.
|
|
68
|
+
const channels: Record<string, Channel | ChannelFactory> = {};
|
|
86
69
|
for (const f of await scan(join(dir, "channels"), ".ts")) {
|
|
87
70
|
const mod = await import(pathToFileURL(f).href);
|
|
88
|
-
if (mod.default) channels.
|
|
71
|
+
if (mod.default) channels[basename(f, ".ts")] = mod.default as Channel | ChannelFactory;
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const
|
|
74
|
+
// channels/<source>.md — a system overlay applied when a turn's inbound event
|
|
75
|
+
// source matches the file's basename (see AgentDefinition.channelInstructions).
|
|
76
|
+
const channelInstructions: Record<string, string> = {};
|
|
77
|
+
for (const f of await scan(join(dir, "channels"), ".md")) {
|
|
78
|
+
channelInstructions[basename(f, ".md")] = await readFile(f, "utf8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// connections/*.ts — outbound: definitions of external MCP/OpenAPI servers;
|
|
82
|
+
// assembleAgent wires them (connectAll) where the agent actually runs.
|
|
83
|
+
const connections: Connection[] = [];
|
|
94
84
|
for (const f of await scan(join(dir, "connections"), ".ts")) {
|
|
95
85
|
const mod = await import(pathToFileURL(f).href);
|
|
96
|
-
if (mod.default)
|
|
86
|
+
if (mod.default) connections.push(mod.default as Connection);
|
|
97
87
|
}
|
|
98
|
-
const { actions: connectionActions, report: connections } = connectionDefs.length
|
|
99
|
-
? await connectAll(connectionDefs)
|
|
100
|
-
: { actions: [], report: [] };
|
|
101
|
-
tools.push(...connectionActions);
|
|
102
88
|
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
89
|
+
return { config, instructions, tools, skills, channels, channelInstructions, connections };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Discover an agent from its directory, returning the assembled AgentDefinition
|
|
93
|
+
// (tools already adapted from defineActions; read_skill added if any skills
|
|
94
|
+
// exist). Mount it on a runtime with createNativeRuntime({ [agent.name]: { model,
|
|
95
|
+
// tools: agent.tools } }).
|
|
96
|
+
export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
97
|
+
return assembleAgent(await discoverAgentModule(dir), process.env);
|
|
113
98
|
}
|