@junejs/server 0.1.3 → 0.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/assets/icon-latin.LICENSE.txt +92 -0
- package/assets/icon-latin.ttf +0 -0
- package/dist/adapter-DcZ-W1-7.d.ts +62 -0
- package/dist/adapter.d.ts +2 -56
- package/dist/adapter.js +62 -5
- package/dist/agent-compile.d.ts +38 -0
- package/dist/agent-compile.js +145 -0
- package/dist/agent-discover.d.ts +4 -4
- package/dist/agent-discover.js +30 -44
- package/dist/agent-durable.d.ts +94 -19
- package/dist/agent-durable.js +647 -32
- package/dist/agent-native.d.ts +44 -13
- package/dist/agent-native.js +264 -27
- package/dist/app.d.ts +5 -5
- package/dist/app.js +42 -10
- package/dist/blob.d.ts +4 -5
- package/dist/build.d.ts +8 -30
- package/dist/build.js +135 -296
- package/dist/client-bundle.d.ts +8 -9
- package/dist/client-bundle.js +1 -1
- package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
- package/dist/config-freeze.d.ts +2 -0
- package/dist/config-freeze.js +54 -0
- package/dist/config-loader.d.ts +3 -4
- package/dist/connection-auth.d.ts +38 -0
- package/dist/connection-auth.js +31 -0
- package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
- package/dist/content-freeze.d.ts +2 -0
- package/dist/content-freeze.js +83 -0
- package/dist/content.d.ts +22 -9
- package/dist/content.js +43 -3
- package/dist/core-version.d.ts +4 -0
- package/dist/core-version.js +12 -0
- package/dist/css-modules-ambient.d.ts +2 -1
- package/dist/css-modules.d.ts +10 -11
- package/dist/css.d.ts +9 -10
- package/dist/d1-migrate.d.ts +6 -7
- package/dist/d1-migrate.js +3 -2
- package/dist/db.d.ts +2 -2
- package/dist/deploy.d.ts +3 -4
- package/dist/deploy.js +3 -2
- package/dist/dev-reload.d.ts +3 -4
- package/dist/dev.d.ts +8 -5
- package/dist/dev.js +40 -9
- package/dist/favicon.d.ts +37 -0
- package/dist/favicon.js +248 -0
- package/dist/host.d.ts +6 -5
- package/dist/host.js +2 -1
- package/dist/icon-letter.d.ts +3 -0
- package/dist/icon-letter.js +8 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +9 -3
- package/dist/instrumentation.d.ts +2 -3
- package/dist/instrumentation.js +2 -1
- package/dist/island-registry.d.ts +6 -7
- package/dist/isolate-local.d.ts +5 -0
- package/dist/isolate-local.js +12 -0
- package/dist/kv.d.ts +3 -4
- package/dist/llms-links.d.ts +9 -0
- package/dist/llms-links.js +65 -0
- package/dist/manifest-CwTT1zVs.d.ts +11 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +113 -0
- package/dist/negotiate.d.ts +4 -3
- package/dist/negotiate.js +24 -14
- package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.js +286 -35
- package/dist/resources.d.ts +5 -6
- package/dist/route-rank.d.ts +6 -7
- package/dist/route-scan-BMbBzPIB.d.ts +20 -0
- package/dist/route-scan.d.ts +2 -0
- package/dist/route-scan.js +100 -0
- package/dist/router.d.ts +12 -13
- package/dist/router.js +51 -38
- package/dist/rsc-build.d.ts +6 -7
- package/dist/rsc-bundle.d.ts +6 -7
- package/dist/rsc-manifest.d.ts +5 -6
- package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
- package/dist/rsc-runtime/flight-render.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.js +3 -2
- package/dist/rsc-runtime/rsc-worker.js +11 -8
- package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
- package/dist/segment.d.ts +4 -5
- package/dist/sqlite-driver.d.ts +11 -11
- package/dist/sqlite-driver.js +17 -4
- package/dist/static-files.d.ts +4 -5
- package/dist/tsconfig-jsx.d.ts +4 -4
- package/dist/tsconfig-jsx.js +1 -1
- package/dist/turn-events.d.ts +4 -0
- package/dist/turn-events.js +46 -0
- package/dist/web-manifest.d.ts +5 -0
- package/dist/web-manifest.js +42 -0
- package/dist/worker-2qbQjj2c.d.ts +64 -0
- package/dist/worker.d.ts +2 -57
- package/dist/worker.js +69 -27
- package/package.json +22 -9
- package/src/adapter.ts +84 -3
- package/src/agent-compile.ts +271 -0
- package/src/agent-discover.ts +77 -63
- package/src/agent-durable.ts +998 -30
- package/src/agent-native.ts +370 -35
- package/src/app.ts +75 -6
- package/src/build.ts +230 -446
- package/src/config-freeze.ts +109 -0
- package/src/connection-auth.ts +101 -0
- package/src/content-freeze.ts +151 -0
- package/src/content.ts +55 -4
- package/src/core-version.ts +34 -0
- package/src/dev.ts +41 -8
- package/src/favicon.ts +325 -0
- package/src/host.ts +4 -3
- package/src/icon-letter.ts +14 -0
- package/src/index.ts +22 -0
- package/src/isolate-local.ts +33 -0
- package/src/llms-links.ts +96 -0
- package/src/manifest.ts +130 -0
- package/src/negotiate.ts +39 -15
- package/src/pipeline.ts +494 -44
- package/src/route-scan.ts +161 -0
- package/src/router.ts +66 -54
- package/src/rsc-runtime/rsc-worker.tsx +5 -4
- package/src/sqlite-driver.ts +36 -6
- package/src/tsconfig-jsx.ts +2 -2
- package/src/turn-events.ts +43 -0
- package/src/web-manifest.ts +38 -0
- package/src/worker.ts +131 -35
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
// instructions.<source>.md at the agent root — per-surface variants (#149).
|
|
35
|
+
surfaceVariants: { source: string; raw: string }[];
|
|
36
|
+
tools: string[]; // relative specifiers, e.g. "./tools/create_order.ts"
|
|
37
|
+
skills: { name: string; raw: string }[];
|
|
38
|
+
channels: string[]; // "./channels/slack.ts"
|
|
39
|
+
channelOverlays: { source: string; raw: string }[]; // channels/<source>.md
|
|
40
|
+
connections: string[]; // "./connections/teachify.ts"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// `_`-prefixed files are private by convention — most importantly the generated
|
|
44
|
+
// module itself, which must never be scanned back in. Sorted for determinism.
|
|
45
|
+
function list(dir: string, ext: string): string[] {
|
|
46
|
+
if (!existsSync(dir)) return [];
|
|
47
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
48
|
+
.filter((e) => e.isFile() && e.name.endsWith(ext) && !e.name.startsWith("_"))
|
|
49
|
+
.map((e) => e.name)
|
|
50
|
+
.sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Scan an agent directory (fs only — no dynamic imports; the point is to defer
|
|
54
|
+
// module loading to the bundler). Returns null when the directory holds nothing
|
|
55
|
+
// agent-shaped, so callers can probe candidate paths cheaply.
|
|
56
|
+
export function scanAgentDir(dir: string): AgentDirScan | null {
|
|
57
|
+
if (!existsSync(dir)) return null;
|
|
58
|
+
const instructionsFile = join(dir, "instructions.md");
|
|
59
|
+
const scan: AgentDirScan = {
|
|
60
|
+
dir,
|
|
61
|
+
name: basename(dir),
|
|
62
|
+
hasConfig: existsSync(join(dir, "agent.ts")),
|
|
63
|
+
instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
|
|
64
|
+
surfaceVariants: readdirSync(dir, { withFileTypes: true })
|
|
65
|
+
.filter((e) => e.isFile())
|
|
66
|
+
.map((e) => e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/))
|
|
67
|
+
.filter((m): m is RegExpMatchArray => m !== null)
|
|
68
|
+
.sort((a, b) => a[1]!.localeCompare(b[1]!))
|
|
69
|
+
.map((m) => ({ source: m[1]!, raw: readFileSync(join(dir, m[0]!), "utf8") })),
|
|
70
|
+
tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
|
|
71
|
+
skills: list(join(dir, "skills"), ".md").map((f) => ({
|
|
72
|
+
name: basename(f, ".md"),
|
|
73
|
+
raw: readFileSync(join(dir, "skills", f), "utf8"),
|
|
74
|
+
})),
|
|
75
|
+
channels: list(join(dir, "channels"), ".ts").map((f) => `./channels/${f}`),
|
|
76
|
+
channelOverlays: list(join(dir, "channels"), ".md").map((f) => ({
|
|
77
|
+
source: basename(f, ".md"),
|
|
78
|
+
raw: readFileSync(join(dir, "channels", f), "utf8"),
|
|
79
|
+
})),
|
|
80
|
+
connections: list(join(dir, "connections"), ".ts").map((f) => `./connections/${f}`),
|
|
81
|
+
};
|
|
82
|
+
const empty =
|
|
83
|
+
!scan.hasConfig &&
|
|
84
|
+
scan.instructions === null &&
|
|
85
|
+
!scan.tools.length &&
|
|
86
|
+
!scan.skills.length &&
|
|
87
|
+
!scan.channels.length &&
|
|
88
|
+
!scan.surfaceVariants.length &&
|
|
89
|
+
!scan.channelOverlays.length &&
|
|
90
|
+
!scan.connections.length;
|
|
91
|
+
return empty ? null : scan;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A stable, readable import identifier from a file specifier: "./tools/create_order.ts"
|
|
95
|
+
// → "tool_create_order". Collisions (foo-bar vs foo_bar) get a numeric suffix.
|
|
96
|
+
function identFor(prefix: string, spec: string, taken: Set<string>): string {
|
|
97
|
+
const stem = basename(spec).replace(/\.ts$/, "").replace(/[^A-Za-z0-9]/g, "_");
|
|
98
|
+
let id = `${prefix}_${stem}`;
|
|
99
|
+
for (let i = 2; taken.has(id); i++) id = `${prefix}_${stem}_${i}`;
|
|
100
|
+
taken.add(id);
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type EmitOptions = {
|
|
105
|
+
// Keep ".ts" on relative import specifiers. Required by consumers that run
|
|
106
|
+
// the module under Node's native type stripping (Node never resolves
|
|
107
|
+
// extensionless imports); rejected (TS5097) by tsconfigs without
|
|
108
|
+
// allowImportingTsExtensions. generateAgentModule sniffs the consumer's
|
|
109
|
+
// tsconfig; pass explicitly to override.
|
|
110
|
+
tsExtensions?: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Emit the module source for a scan. Pure — the golden test pins this output.
|
|
114
|
+
export function emitAgentModule(scan: AgentDirScan, opts?: EmitOptions): string {
|
|
115
|
+
const spec = (s: string) => JSON.stringify(opts?.tsExtensions ? s : s.replace(/\.ts$/, ""));
|
|
116
|
+
const taken = new Set<string>();
|
|
117
|
+
const imports: string[] = [];
|
|
118
|
+
const toolIds = scan.tools.map((s) => {
|
|
119
|
+
const id = identFor("tool", s, taken);
|
|
120
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
121
|
+
return id;
|
|
122
|
+
});
|
|
123
|
+
const channelIds = scan.channels.map((s) => {
|
|
124
|
+
const id = identFor("channel", s, taken);
|
|
125
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
126
|
+
return { key: basename(s, ".ts"), id };
|
|
127
|
+
});
|
|
128
|
+
const connectionIds = scan.connections.map((s) => {
|
|
129
|
+
const id = identFor("connection", s, taken);
|
|
130
|
+
imports.push(`import ${id} from ${spec(s)};`);
|
|
131
|
+
return id;
|
|
132
|
+
});
|
|
133
|
+
if (scan.hasConfig) imports.push(`import config from ${spec("./agent.ts")};`);
|
|
134
|
+
|
|
135
|
+
const lines: string[] = [
|
|
136
|
+
"// AUTO-GENERATED by June — do not edit. Compiled from this agent/ directory so",
|
|
137
|
+
"// the definition mounts without filesystem discovery (workerd has no fs).",
|
|
138
|
+
"// Regenerate with `june gen` after adding, removing, or editing agent files.",
|
|
139
|
+
...imports,
|
|
140
|
+
scan.skills.length
|
|
141
|
+
? `import { parseSkill, type AgentModule } from "@junejs/core/agent-config";`
|
|
142
|
+
: `import type { AgentModule } from "@junejs/core/agent-config";`,
|
|
143
|
+
"",
|
|
144
|
+
];
|
|
145
|
+
if (!scan.hasConfig) lines.push(`const config = { name: ${JSON.stringify(scan.name)} };`, "");
|
|
146
|
+
|
|
147
|
+
lines.push(
|
|
148
|
+
"export const agentModule: AgentModule = {",
|
|
149
|
+
" config,",
|
|
150
|
+
` instructions: ${JSON.stringify(scan.instructions ?? "")},`,
|
|
151
|
+
scan.surfaceVariants.length
|
|
152
|
+
? ` surfaceInstructions: {\n${scan.surfaceVariants.map((v) => ` ${JSON.stringify(v.source)}: ${JSON.stringify(v.raw)},`).join("\n")}\n },`
|
|
153
|
+
: " surfaceInstructions: {},",
|
|
154
|
+
` tools: [${toolIds.join(", ")}],`,
|
|
155
|
+
scan.skills.length
|
|
156
|
+
? ` skills: [\n${scan.skills.map((s) => ` parseSkill(${JSON.stringify(s.name)}, ${JSON.stringify(s.raw)}),`).join("\n")}\n ],`
|
|
157
|
+
: " skills: [],",
|
|
158
|
+
channelIds.length
|
|
159
|
+
? ` channels: { ${channelIds.map((c) => `${JSON.stringify(c.key)}: ${c.id}`).join(", ")} },`
|
|
160
|
+
: " channels: {},",
|
|
161
|
+
scan.channelOverlays.length
|
|
162
|
+
? ` channelInstructions: {\n${scan.channelOverlays.map((o) => ` ${JSON.stringify(o.source)}: ${JSON.stringify(o.raw)},`).join("\n")}\n },`
|
|
163
|
+
: " channelInstructions: {},",
|
|
164
|
+
` connections: [${connectionIds.join(", ")}],`,
|
|
165
|
+
"};",
|
|
166
|
+
"export default agentModule;",
|
|
167
|
+
"",
|
|
168
|
+
);
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Locate the agent directory for a project root: a June app keeps it at
|
|
173
|
+
// app/<dir> (the app.ts convention); a wrangler-first worker keeps it at
|
|
174
|
+
// <root>/<dir>. `dirName` follows june.config's agent.runtime.dir (default
|
|
175
|
+
// "agent").
|
|
176
|
+
export function findAgentDir(root: string, dirName = "agent"): string | null {
|
|
177
|
+
for (const candidate of [join(root, "app", dirName), join(root, dirName)]) {
|
|
178
|
+
if (existsSync(candidate)) return candidate;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Whether the consumer's tsconfig keeps ".ts" on import specifiers
|
|
184
|
+
// (allowImportingTsExtensions): the nearest tsconfig.json walking up from the
|
|
185
|
+
// agent dir, parsed as JSONC (same stripJsonc as tsconfig-jsx.ts — a commented-
|
|
186
|
+
// out option must not count) and resolved through `extends` (relative, bare
|
|
187
|
+
// package specifiers, and the TS 5 array form; own options win, later array
|
|
188
|
+
// entries beat earlier ones). Explicit opts.tsExtensions overrides the sniff.
|
|
189
|
+
type TsconfigLite = { extends?: string | string[]; compilerOptions?: { allowImportingTsExtensions?: boolean } };
|
|
190
|
+
|
|
191
|
+
function readTsconfig(path: string): TsconfigLite | undefined {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(stripJsonc(readFileSync(path, "utf8"))) as TsconfigLite;
|
|
194
|
+
} catch {
|
|
195
|
+
return undefined; // absent or unparsable — treat as "not declared"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Resolve an `extends` specifier from the extending config's directory: relative
|
|
200
|
+
// paths directly, bare specifiers through the nearest node_modules walking up.
|
|
201
|
+
// tsc appends ".json" when the target has no extension — mirror that.
|
|
202
|
+
function resolveExtends(fromDir: string, spec: string): string | undefined {
|
|
203
|
+
const withJson = (p: string) => (existsSync(p) ? p : !p.endsWith(".json") && existsSync(`${p}.json`) ? `${p}.json` : undefined);
|
|
204
|
+
if (spec.startsWith(".")) return withJson(resolve(fromDir, spec));
|
|
205
|
+
for (let d = fromDir; ; ) {
|
|
206
|
+
const candidate = withJson(join(d, "node_modules", spec));
|
|
207
|
+
if (candidate) return candidate;
|
|
208
|
+
const parent = dirname(d);
|
|
209
|
+
if (parent === d) return undefined;
|
|
210
|
+
d = parent;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveAllowTsExtensions(path: string, depth: number): boolean | undefined {
|
|
215
|
+
if (depth > 8) return undefined; // extends cycles/degenerate chains — give up
|
|
216
|
+
const tc = readTsconfig(path);
|
|
217
|
+
if (!tc) return undefined;
|
|
218
|
+
const own = tc.compilerOptions?.allowImportingTsExtensions;
|
|
219
|
+
if (own !== undefined) return own;
|
|
220
|
+
// TS extends-array semantics: later entries override earlier ones.
|
|
221
|
+
const bases = typeof tc.extends === "string" ? [tc.extends] : [...(tc.extends ?? [])].reverse();
|
|
222
|
+
for (const spec of bases) {
|
|
223
|
+
const base = resolveExtends(dirname(path), spec);
|
|
224
|
+
const inherited = base ? resolveAllowTsExtensions(base, depth + 1) : undefined;
|
|
225
|
+
if (inherited !== undefined) return inherited;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sniffTsExtensions(dir: string): boolean {
|
|
231
|
+
for (let d = dir; ; ) {
|
|
232
|
+
const candidate = join(d, "tsconfig.json");
|
|
233
|
+
if (existsSync(candidate)) return resolveAllowTsExtensions(candidate, 0) ?? false;
|
|
234
|
+
const parent = dirname(d);
|
|
235
|
+
if (parent === d) return false;
|
|
236
|
+
d = parent;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Generate <dir>/_agent.gen.ts. With `check: true` nothing is written — `stale`
|
|
241
|
+
// reports whether the file on disk differs from what would be generated (the CI
|
|
242
|
+
// gate; `june gen && git diff --exit-code` works too). Returns null when the
|
|
243
|
+
// directory holds nothing agent-shaped.
|
|
244
|
+
export function generateAgentModule(
|
|
245
|
+
dir: string,
|
|
246
|
+
opts?: { check?: boolean } & EmitOptions,
|
|
247
|
+
): { file: string; code: string; stale: boolean; written: boolean } | null {
|
|
248
|
+
const scan = scanAgentDir(dir);
|
|
249
|
+
if (!scan) return null;
|
|
250
|
+
for (const o of scan.channelOverlays) {
|
|
251
|
+
console.warn(
|
|
252
|
+
`[june] deprecated: channels/${o.source}.md — move it to instructions.${o.source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// A variant-shaped file the grammar doesn't parse yet must never be silently
|
|
256
|
+
// inert (the same failure class this whole convention exists to kill).
|
|
257
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
258
|
+
if (e.isFile() && /^instructions\..+\.md$/.test(e.name) && !/^instructions\.[a-z0-9_-]+\.md$/.test(e.name)) {
|
|
259
|
+
console.warn(
|
|
260
|
+
`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
|
|
265
|
+
const file = join(dir, AGENT_MODULE_FILE);
|
|
266
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
267
|
+
const stale = existing !== code;
|
|
268
|
+
if (opts?.check) return { file, code, stale, written: false };
|
|
269
|
+
if (stale) writeFileSync(file, code);
|
|
270
|
+
return { file, code, stale, written: stale };
|
|
271
|
+
}
|
package/src/agent-discover.ts
CHANGED
|
@@ -3,75 +3,82 @@
|
|
|
3
3
|
// An agent is a DIRECTORY, not a config object. Location determines function:
|
|
4
4
|
//
|
|
5
5
|
// agent/
|
|
6
|
-
// agent.ts
|
|
7
|
-
// instructions.md
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// agent.ts → default-exports a plain config { name, model?, surfaces? }
|
|
7
|
+
// instructions.md → base system prompt
|
|
8
|
+
// instructions.<source>.md → per-surface instruction variant (#149)
|
|
9
|
+
// tools/*.ts → each default-exports a defineAction (a tool),
|
|
10
|
+
// OR an array of defineActions (an integration
|
|
11
|
+
// shipping several tools, e.g. googleDriveTools())
|
|
12
|
+
// skills/*.md → each a procedure, loaded on demand (progressive disclosure)
|
|
13
|
+
// channels/*.ts → each default-exports a Channel (an inbound edge — pure transport)
|
|
14
|
+
// channels/*.md → DEPRECATED overlay location (warned; use instructions.<source>.md)
|
|
15
|
+
// connections/*.ts → each default-exports a Connection (an outbound tool source)
|
|
12
16
|
//
|
|
13
17
|
// There is no central registry to keep in sync — the directory IS the manifest.
|
|
14
18
|
// Because tools are `defineAction`s, the SAME directory is also an MCP server and
|
|
15
19
|
// 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
|
|
20
|
+
// package; the pure assembly is @junejs/core/agent-config's assembleAgent() — the
|
|
21
|
+
// SAME entry point a compiled agent module (agent-compile.ts, the edge path) goes
|
|
22
|
+
// through, so native discovery and the generated module cannot drift.
|
|
17
23
|
|
|
18
24
|
import { existsSync } from "node:fs";
|
|
19
25
|
import { readdir, readFile } from "node:fs/promises";
|
|
20
26
|
import { basename, extname, join } from "node:path";
|
|
21
27
|
import { pathToFileURL } from "node:url";
|
|
22
28
|
|
|
23
|
-
import type
|
|
24
|
-
import
|
|
25
|
-
import { connectAll, type Connection } from "@junejs/core/connections";
|
|
29
|
+
import { assembleAgent, parseSkill, type AgentConfigFile, type AgentDefinition, type AgentModule, type Channel, type ChannelFactory, type Skill, type ToolEntry } from "@junejs/core/agent-config";
|
|
30
|
+
import type { Connection } from "@junejs/core/connections";
|
|
26
31
|
|
|
32
|
+
// `_`-prefixed files are private by convention (mirrors the app/ router) — most
|
|
33
|
+
// importantly the generated `_agent.gen.ts` itself, which must never be scanned
|
|
34
|
+
// back in as a tool/channel.
|
|
27
35
|
async function scan(dir: string, ext: string): Promise<string[]> {
|
|
28
36
|
if (!existsSync(dir)) return [];
|
|
29
37
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
30
38
|
return entries
|
|
31
|
-
.filter((e) => e.isFile() && extname(e.name) === ext)
|
|
39
|
+
.filter((e) => e.isFile() && extname(e.name) === ext && !e.name.startsWith("_"))
|
|
32
40
|
.map((e) => join(dir, e.name))
|
|
33
41
|
.sort();
|
|
34
42
|
}
|
|
35
43
|
|
|
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> {
|
|
44
|
+
// Read an agent directory into its raw, unassembled shape (fs + dynamic import —
|
|
45
|
+
// native only). The pure sibling for the edge is agent-compile.ts, which emits
|
|
46
|
+
// the same AgentModule as static code.
|
|
47
|
+
export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
61
48
|
const configFile = join(dir, "agent.ts");
|
|
62
49
|
const config: AgentConfigFile = existsSync(configFile)
|
|
63
50
|
? ((await import(pathToFileURL(configFile).href)).default as AgentConfigFile)
|
|
64
51
|
: { name: basename(dir) };
|
|
65
52
|
|
|
66
53
|
const instructionsFile = join(dir, "instructions.md");
|
|
67
|
-
const instructions = existsSync(instructionsFile)
|
|
68
|
-
? await readFile(instructionsFile, "utf8")
|
|
69
|
-
: (config.instructions ?? "");
|
|
54
|
+
const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
|
|
70
55
|
|
|
71
|
-
|
|
56
|
+
// instructions.<source>.md — the agent's per-surface instruction variants
|
|
57
|
+
// (#149): applied to turns arriving through that source, composed per
|
|
58
|
+
// config.surfaces (append by default, "replace" to stand alone).
|
|
59
|
+
const surfaceInstructions: Record<string, string> = {};
|
|
60
|
+
if (existsSync(dir)) {
|
|
61
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
62
|
+
if (!e.isFile() || !/^instructions\..+\.md$/.test(e.name)) continue;
|
|
63
|
+
const m = e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/);
|
|
64
|
+
if (m) {
|
|
65
|
+
surfaceInstructions[m[1]!] = await readFile(join(dir, e.name), "utf8");
|
|
66
|
+
} else {
|
|
67
|
+
// A variant-shaped file the grammar doesn't parse yet (e.g. a locale
|
|
68
|
+
// suffix, instructions.slack.zh-TW.md) must never be SILENTLY inert.
|
|
69
|
+
console.warn(
|
|
70
|
+
`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A tool file default-exports one tool OR an array of tools (an integration
|
|
77
|
+
// like googleDriveTools()); assembly flattens arrays.
|
|
78
|
+
const tools: ToolEntry[] = [];
|
|
72
79
|
for (const f of await scan(join(dir, "tools"), ".ts")) {
|
|
73
80
|
const mod = await import(pathToFileURL(f).href);
|
|
74
|
-
if (mod.default) tools.push(mod.default as
|
|
81
|
+
if (mod.default) tools.push(mod.default as ToolEntry);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
const skills: Skill[] = [];
|
|
@@ -80,34 +87,41 @@ export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
// 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
|
|
90
|
+
// form workerd needs — secrets live in env, not at module scope). Resolution is
|
|
91
|
+
// assembleAgent's job (native: process.env; a DO: its own env) — keep raw here.
|
|
92
|
+
const channels: Record<string, Channel | ChannelFactory> = {};
|
|
86
93
|
for (const f of await scan(join(dir, "channels"), ".ts")) {
|
|
87
94
|
const mod = await import(pathToFileURL(f).href);
|
|
88
|
-
if (mod.default) channels.
|
|
95
|
+
if (mod.default) channels[basename(f, ".ts")] = mod.default as Channel | ChannelFactory;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
98
|
+
// channels/<source>.md — the DEPRECATED overlay convention (#149): behavior
|
|
99
|
+
// prose belongs at the agent level (instructions.<source>.md), not beside
|
|
100
|
+
// transport code. Still honored for one dev-series, loudly.
|
|
101
|
+
const channelInstructions: Record<string, string> = {};
|
|
102
|
+
for (const f of await scan(join(dir, "channels"), ".md")) {
|
|
103
|
+
const source = basename(f, ".md");
|
|
104
|
+
console.warn(
|
|
105
|
+
`[june] deprecated: channels/${source}.md — move it to instructions.${source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
|
|
106
|
+
);
|
|
107
|
+
channelInstructions[source] = await readFile(f, "utf8");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// connections/*.ts — outbound: definitions of external MCP/OpenAPI servers;
|
|
111
|
+
// assembleAgent wires them (connectAll) where the agent actually runs.
|
|
112
|
+
const connections: Connection[] = [];
|
|
94
113
|
for (const f of await scan(join(dir, "connections"), ".ts")) {
|
|
95
114
|
const mod = await import(pathToFileURL(f).href);
|
|
96
|
-
if (mod.default)
|
|
115
|
+
if (mod.default) connections.push(mod.default as Connection);
|
|
97
116
|
}
|
|
98
|
-
const { actions: connectionActions, report: connections } = connectionDefs.length
|
|
99
|
-
? await connectAll(connectionDefs)
|
|
100
|
-
: { actions: [], report: [] };
|
|
101
|
-
tools.push(...connectionActions);
|
|
102
117
|
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
118
|
+
return { config, instructions, surfaceInstructions, tools, skills, channels, channelInstructions, connections };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Discover an agent from its directory, returning the assembled AgentDefinition
|
|
122
|
+
// (tools already adapted from defineActions; read_skill added if any skills
|
|
123
|
+
// exist). Mount it on a runtime with createNativeRuntime({ [agent.name]:
|
|
124
|
+
// toAgentDef(agent, model) }).
|
|
125
|
+
export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
126
|
+
return assembleAgent(await discoverAgentModule(dir), process.env);
|
|
113
127
|
}
|