@junejs/server 0.1.2 → 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.
Files changed (130) hide show
  1. package/assets/icon-latin.LICENSE.txt +92 -0
  2. package/assets/icon-latin.ttf +0 -0
  3. package/dist/adapter-DcZ-W1-7.d.ts +62 -0
  4. package/dist/adapter.d.ts +2 -56
  5. package/dist/adapter.js +62 -5
  6. package/dist/agent-compile.d.ts +38 -0
  7. package/dist/agent-compile.js +145 -0
  8. package/dist/agent-discover.d.ts +4 -4
  9. package/dist/agent-discover.js +30 -44
  10. package/dist/agent-durable.d.ts +94 -19
  11. package/dist/agent-durable.js +647 -32
  12. package/dist/agent-native.d.ts +44 -13
  13. package/dist/agent-native.js +264 -27
  14. package/dist/app.d.ts +5 -5
  15. package/dist/app.js +42 -10
  16. package/dist/blob.d.ts +4 -5
  17. package/dist/build.d.ts +8 -29
  18. package/dist/build.js +138 -289
  19. package/dist/client-bundle.d.ts +8 -9
  20. package/dist/client-bundle.js +1 -1
  21. package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
  22. package/dist/config-freeze.d.ts +2 -0
  23. package/dist/config-freeze.js +54 -0
  24. package/dist/config-loader.d.ts +3 -4
  25. package/dist/connection-auth.d.ts +38 -0
  26. package/dist/connection-auth.js +31 -0
  27. package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
  28. package/dist/content-freeze.d.ts +2 -0
  29. package/dist/content-freeze.js +83 -0
  30. package/dist/content.d.ts +22 -9
  31. package/dist/content.js +43 -3
  32. package/dist/core-version.d.ts +4 -0
  33. package/dist/core-version.js +12 -0
  34. package/dist/css-modules-ambient.d.ts +2 -1
  35. package/dist/css-modules.d.ts +10 -11
  36. package/dist/css.d.ts +9 -10
  37. package/dist/d1-migrate.d.ts +6 -7
  38. package/dist/d1-migrate.js +3 -2
  39. package/dist/db.d.ts +2 -2
  40. package/dist/deploy.d.ts +3 -4
  41. package/dist/deploy.js +3 -2
  42. package/dist/dev-reload.d.ts +3 -4
  43. package/dist/dev.d.ts +8 -5
  44. package/dist/dev.js +40 -9
  45. package/dist/favicon.d.ts +37 -0
  46. package/dist/favicon.js +248 -0
  47. package/dist/host.d.ts +6 -5
  48. package/dist/host.js +2 -1
  49. package/dist/icon-letter.d.ts +3 -0
  50. package/dist/icon-letter.js +8 -0
  51. package/dist/index.d.ts +11 -5
  52. package/dist/index.js +9 -3
  53. package/dist/instrumentation.d.ts +2 -3
  54. package/dist/instrumentation.js +2 -1
  55. package/dist/island-registry.d.ts +6 -7
  56. package/dist/isolate-local.d.ts +5 -0
  57. package/dist/isolate-local.js +12 -0
  58. package/dist/kv.d.ts +3 -4
  59. package/dist/llms-links.d.ts +9 -0
  60. package/dist/llms-links.js +65 -0
  61. package/dist/manifest-CwTT1zVs.d.ts +11 -0
  62. package/dist/manifest.d.ts +2 -0
  63. package/dist/manifest.js +113 -0
  64. package/dist/negotiate.d.ts +4 -3
  65. package/dist/negotiate.js +24 -14
  66. package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
  67. package/dist/pipeline.d.ts +2 -2
  68. package/dist/pipeline.js +286 -35
  69. package/dist/resources.d.ts +5 -6
  70. package/dist/route-rank.d.ts +10 -0
  71. package/dist/route-rank.js +38 -0
  72. package/dist/route-scan-BMbBzPIB.d.ts +20 -0
  73. package/dist/route-scan.d.ts +2 -0
  74. package/dist/route-scan.js +100 -0
  75. package/dist/router.d.ts +12 -13
  76. package/dist/router.js +58 -44
  77. package/dist/rsc-build.d.ts +6 -7
  78. package/dist/rsc-bundle.d.ts +6 -7
  79. package/dist/rsc-manifest.d.ts +5 -6
  80. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  81. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  82. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  83. package/dist/rsc-runtime/flight-to-html.js +3 -2
  84. package/dist/rsc-runtime/rsc-worker.js +11 -8
  85. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  86. package/dist/segment.d.ts +4 -5
  87. package/dist/sqlite-driver.d.ts +11 -11
  88. package/dist/sqlite-driver.js +17 -4
  89. package/dist/static-files.d.ts +4 -5
  90. package/dist/tsconfig-jsx.d.ts +4 -4
  91. package/dist/tsconfig-jsx.js +1 -1
  92. package/dist/turn-events.d.ts +4 -0
  93. package/dist/turn-events.js +46 -0
  94. package/dist/web-manifest.d.ts +5 -0
  95. package/dist/web-manifest.js +42 -0
  96. package/dist/worker-2qbQjj2c.d.ts +64 -0
  97. package/dist/worker.d.ts +2 -56
  98. package/dist/worker.js +121 -103
  99. package/package.json +22 -9
  100. package/src/adapter.ts +84 -3
  101. package/src/agent-compile.ts +271 -0
  102. package/src/agent-discover.ts +77 -63
  103. package/src/agent-durable.ts +998 -30
  104. package/src/agent-native.ts +370 -35
  105. package/src/app.ts +75 -6
  106. package/src/build.ts +237 -444
  107. package/src/config-freeze.ts +109 -0
  108. package/src/connection-auth.ts +101 -0
  109. package/src/content-freeze.ts +151 -0
  110. package/src/content.ts +55 -4
  111. package/src/core-version.ts +34 -0
  112. package/src/dev.ts +41 -8
  113. package/src/favicon.ts +325 -0
  114. package/src/host.ts +4 -3
  115. package/src/icon-letter.ts +14 -0
  116. package/src/index.ts +22 -0
  117. package/src/isolate-local.ts +33 -0
  118. package/src/llms-links.ts +96 -0
  119. package/src/manifest.ts +130 -0
  120. package/src/negotiate.ts +39 -15
  121. package/src/pipeline.ts +494 -44
  122. package/src/route-rank.ts +60 -0
  123. package/src/route-scan.ts +161 -0
  124. package/src/router.ts +75 -58
  125. package/src/rsc-runtime/rsc-worker.tsx +5 -4
  126. package/src/sqlite-driver.ts +36 -6
  127. package/src/tsconfig-jsx.ts +2 -2
  128. package/src/turn-events.ts +43 -0
  129. package/src/web-manifest.ts +38 -0
  130. package/src/worker.ts +197 -117
@@ -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
+ }
@@ -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 → default-exports a plain config { name, model?, description? }
7
- // instructions.md → system prompt
8
- // tools/*.ts → each default-exports a defineAction (a tool)
9
- // skills/*.md → each a procedure, loaded on demand (progressive disclosure)
10
- // channels/*.ts → each default-exports a Channel (an inbound edge)
11
- // connections/*.ts → each default-exports a Connection (an outbound tool source)
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 defineAgent().
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 { AnyAction } from "@junejs/core/agent";
24
- import { defineAgent, resolveChannel, type AgentConfigFile, type AgentDefinition, type Channel, type ChannelFactory, type Skill } from "@junejs/core/agent-config";
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
- // Optional YAML-ish frontmatter: --- \n key: value \n --- \n body. Falls back to
37
- // the first non-empty line (minus a leading "# ") as the description.
38
- function parseSkill(name: string, text: string): Skill {
39
- if (text.startsWith("---")) {
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
- const tools: AnyAction[] = [];
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 AnyAction);
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). On native, env is
84
- // process.env (present at load), so resolve here; the plain form stays supported.
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.push(resolveChannel(mod.default as Channel | ChannelFactory, process.env));
95
+ if (mod.default) channels[basename(f, ".ts")] = mod.default as Channel | ChannelFactory;
89
96
  }
90
97
 
91
- // connections/*.ts — outbound: wire external MCP/OpenAPI servers, turning their
92
- // operations into `<connection>__<tool>` defineActions that join `tools`.
93
- const connectionDefs: Connection[] = [];
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) connectionDefs.push(mod.default as Connection);
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 defineAgent({
104
- name: config.name,
105
- model: config.model,
106
- description: config.description,
107
- instructions,
108
- tools,
109
- skills,
110
- channels,
111
- connections,
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
  }