@tangle-network/agent-app 0.43.48 → 0.43.49

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.
@@ -1,4 +1,4 @@
1
- import { AgentProfileFileMount } from '@tangle-network/sandbox';
1
+ import { AgentProfileFileMount, AgentProfileResourceRef } from '@tangle-network/sandbox';
2
2
 
3
3
  /**
4
4
  * Unified skill + corpus mounter for agent products.
@@ -17,13 +17,30 @@ import { AgentProfileFileMount } from '@tangle-network/sandbox';
17
17
  * fs fallback), a registry adapter that tier-gates, and a single
18
18
  * `composeShellResources` that projects either onto the SDK file-mount shape.
19
19
  *
20
+ * A THIRD surface lives alongside those two: adoptable `SkillEntry`s sourced
21
+ * from `SKILL.md` frontmatter rather than hand-authored fields.
22
+ * `parseSkillFrontmatter` is the ONE frontmatter parser (hand-rolled, no YAML
23
+ * dependency — fail loud on a malformed block rather than silently mis-reading
24
+ * a field); `skillEntryFromMarkdown`/`parseCorpusSkills` turn raw markdown into
25
+ * `SkillEntry`s. From there a skill reaches the agent by one of two DELIVERY
26
+ * MODES: `inline` renders the skill body straight into the system prompt (every
27
+ * harness can read it, at prompt-byte cost), or `mounted` projects it onto the
28
+ * typed `resources.skills` channel (`AgentProfileResourceRef[]`) plus an index
29
+ * section that just names the file, and lets the platform materializer place it
30
+ * at the harness-native skill dir. `composeSkills` builds either shape;
31
+ * `mergeComposedSkills` combines batches and throws (via
32
+ * `assertSkillDeliveryDisjoint`) on a skill accidentally delivered both
33
+ * ways. Picking WHICH harnesses can take `mounted` delivery is platform-bound
34
+ * (see `@tangle-network/agent-app/skills-placement`) and deliberately kept out
35
+ * of this substrate-free module.
36
+ *
20
37
  * Substrate-free over storage, exact over the SDK boundary: the only inbound
21
38
  * seam is the glob-result map the consumer passes in (its call site keeps the
22
39
  * literal `import.meta.glob` Vite must static-analyze); the only outbound seam
23
- * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`, the exact shape the
24
- * agent profile's `resources.files` consumes. Node builtins are resolved lazily
25
- * via `process.getBuiltinModule` so a static `node:*` import never reaches the
26
- * Vite SSR bundle.
40
+ * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`/`AgentProfileResourceRef[]`,
41
+ * the exact shapes `resources.files`/`resources.skills` consume. Node builtins
42
+ * are resolved lazily via `process.getBuiltinModule` so a static `node:*`
43
+ * import never reaches the Vite SSR bundle.
27
44
  */
28
45
 
29
46
  /** A Vite eager `?raw` glob result: glob key -> raw file body. The consumer
@@ -63,8 +80,18 @@ interface SkillEntry {
63
80
  tier: string;
64
81
  skillMd: string;
65
82
  }
66
- /** Harness skill-discovery path the Claude Code / OpenCode backend reads
67
- * natively. The registry mounts here; the corpus mounts at its relative path. */
83
+ /** Harness skill-discovery path the Claude Code backend reads natively. The
84
+ * registry mounts here; the corpus mounts at its relative path.
85
+ *
86
+ * @deprecated Hardcodes the claude-code path (`~/.claude/skills/<id>/SKILL.md`).
87
+ * It is NOT a path other harnesses read — OpenCode discovers `.opencode/skills`,
88
+ * not `~/.claude/skills` (the doc comment here previously claimed otherwise);
89
+ * codex, kimi-code, and the rest each have their own dir or none at all. Use
90
+ * {@link skillRefs} to put a skill on the typed `resources.skills` channel and
91
+ * let the platform materializer place it correctly, or resolve the
92
+ * harness-native dir directly via `@tangle-network/agent-app/skills-placement`.
93
+ * Kept only for the pre-existing `registrySkills`/`userSkillMounts` callers;
94
+ * do not add new call sites. */
68
95
  declare function skillMountPath(id: string): string;
69
96
  /** Options for {@link loadMarkdownCorpus}. */
70
97
  interface LoadCorpusOptions {
@@ -132,5 +159,153 @@ interface ComposeShellResourcesInput {
132
159
  * `profile.resources.files` with no cast.
133
160
  */
134
161
  declare function composeShellResources(input: ComposeShellResourcesInput): AgentProfileFileMount[];
162
+ /** Fields a `SKILL.md` frontmatter block may declare. All optional — absent
163
+ * frontmatter (or an absent field within it) is legal; callers fill defaults
164
+ * (see {@link skillEntryFromMarkdown}). */
165
+ interface SkillFrontmatter {
166
+ id?: string;
167
+ name?: string;
168
+ description?: string;
169
+ author?: {
170
+ name: string;
171
+ url?: string;
172
+ };
173
+ source?: string;
174
+ category?: string;
175
+ tags?: string[];
176
+ tier?: string;
177
+ }
178
+ /** The result of {@link parseSkillFrontmatter}: the parsed fields, the body
179
+ * with the frontmatter block stripped, and the original untouched text. */
180
+ interface ParsedSkill {
181
+ frontmatter: SkillFrontmatter;
182
+ body: string;
183
+ raw: string;
184
+ }
185
+ /** THE one `SKILL.md` frontmatter parser — hand-rolled, no YAML dependency.
186
+ *
187
+ * Absent frontmatter (text does not open with a `---` delimiter line) is
188
+ * legal: returns `{frontmatter: {}, body: raw, raw}`. An OPENED block with no
189
+ * closing `---` is truncated input and throws. Inside the block: scalar
190
+ * `key: value` lines (value optionally double-quoted, decoded via
191
+ * `JSON.parse`); a nested `author:` block whose indented `name:`/`url:` lines
192
+ * are the only children it accepts; `tags:` as an inline `[a, b]` list or as
193
+ * an indented `- item` block. Unknown scalar keys are ignored (forward-compat)
194
+ * — but a line that matches NONE of these shapes (no colon, an orphaned
195
+ * indented line, a bad dash) throws naming the offending line. Silently
196
+ * mis-parsed metadata is the bug class this parser exists to kill; an
197
+ * unrecognized shape is never guessed at.
198
+ */
199
+ declare function parseSkillFrontmatter(raw: string): ParsedSkill;
200
+ /** Build a {@link SkillEntry} from a raw `SKILL.md` body. `id` comes from
201
+ * frontmatter, falling back to `fallbackId` (typically the corpus entry's
202
+ * slug/filename); neither present throws. `name` defaults to `id`,
203
+ * `description` to `''`, `tier` to `'free'`. `skillMd` is always the
204
+ * untouched `raw` input — the full file, frontmatter included, is what a
205
+ * `mounted` delivery writes to disk and what `renderInlineSkills` strips per
206
+ * render. */
207
+ declare function skillEntryFromMarkdown(raw: string, fallbackId?: string): SkillEntry;
208
+ /** Map a loaded corpus (see {@link loadMarkdownCorpus}) onto `SkillEntry`s,
209
+ * using each entry's `id` as the fallback when its `SKILL.md` carries no
210
+ * frontmatter `id` of its own. */
211
+ declare function parseCorpusSkills(corpus: CorpusEntry[]): SkillEntry[];
212
+ /** Project skills onto the typed `resources.skills` channel
213
+ * (`AgentProfileResourceRef[]`), tier-filtered (same `s.tier === tier`
214
+ * semantics as {@link registrySkills}) when `opts.tier` is given, sorted by
215
+ * id for determinism. `ref.name` MUST be the skill id: the platform
216
+ * materializer writes each ref to `${skillDir}/${name}/SKILL.md`. */
217
+ declare function skillRefs(skills: SkillEntry[], opts?: {
218
+ tier?: string;
219
+ }): AgentProfileResourceRef[];
220
+ /** Inputs to {@link renderInlineSkills}. */
221
+ interface RenderInlineSkillsInput {
222
+ skills: SkillEntry[];
223
+ /** Section heading. Default `'## Skills'`. */
224
+ heading?: string;
225
+ /** Tier filter — same semantics as {@link skillRefs}. */
226
+ tier?: string;
227
+ /** Per-skill body renderer. Default strips frontmatter and emits
228
+ * `### <name>\n\n<body>`. */
229
+ body?: (skill: SkillEntry) => string;
230
+ }
231
+ /**
232
+ * Render every (tier-filtered) skill's full body inline into the prompt — the
233
+ * `inline` delivery mode. Returns `''` when no skill survives the filter, else
234
+ * a section starting `\n\n<heading>\n\n` with each skill's body joined by
235
+ * `\n\n` — the same "already carries its own leading `\n\n`" shape
236
+ * `assembleSystemPrompt` expects from every section it concatenates.
237
+ */
238
+ declare function renderInlineSkills(input: RenderInlineSkillsInput): string;
239
+ /** Inputs to {@link renderSkillIndex}. */
240
+ interface RenderSkillIndexInput {
241
+ skills: SkillEntry[];
242
+ /** cwd-relative directory the skills are mounted under (e.g.
243
+ * `.opencode/skills`) — named in each index line so the agent knows where
244
+ * to read the full `SKILL.md`. */
245
+ skillDir: string;
246
+ /** Section heading. Default `'## Skills'`. */
247
+ heading?: string;
248
+ /** Tier filter — same semantics as {@link skillRefs}. */
249
+ tier?: string;
250
+ }
251
+ /**
252
+ * Render a one-line-per-skill INDEX (name, description, and the path to read
253
+ * the full body) — the `mounted` delivery mode's prompt section, paired with
254
+ * {@link skillRefs} putting the actual files on `resources.skills`. Same
255
+ * empty/section shape as {@link renderInlineSkills}.
256
+ */
257
+ declare function renderSkillIndex(input: RenderSkillIndexInput): string;
258
+ /** How a skill reaches the agent: `inline` renders its full body into the
259
+ * system prompt; `mounted` puts it on the typed `resources.skills` channel
260
+ * and renders only an index line. */
261
+ type SkillDeliveryMode = 'inline' | 'mounted';
262
+ /** The output of {@link composeSkills}: the refs to attach to
263
+ * `resources.skills` (empty for `inline`) and the prompt section to fold into
264
+ * the system prompt (already carries its own leading `\n\n`, or `''`).
265
+ * `inlineIds`/`mountedIds` record which (tier-filtered) skills were delivered
266
+ * in each mode so {@link mergeComposedSkills} can enforce the modes stay
267
+ * disjoint when batches are combined. */
268
+ interface ComposedSkills {
269
+ refs: AgentProfileResourceRef[];
270
+ promptSection: string;
271
+ inlineIds: string[];
272
+ mountedIds: string[];
273
+ }
274
+ /** Inputs to {@link composeSkills}. */
275
+ interface ComposeSkillsInput {
276
+ skills: SkillEntry[];
277
+ mode: SkillDeliveryMode;
278
+ tier?: string;
279
+ heading?: string;
280
+ /** REQUIRED (non-null) for `mode: 'mounted'` — the cwd-relative skill dir
281
+ * the harness reads. Resolve it with `@tangle-network/agent-app/skills-placement`
282
+ * rather than hardcoding it; omitted or `null` throws. */
283
+ skillDir?: string | null;
284
+ }
285
+ /**
286
+ * Build the {@link ComposedSkills} for one delivery mode. `inline` never
287
+ * touches `resources.skills` — `refs` is always `[]`. `mounted` requires a
288
+ * non-null `skillDir` (the harness must have a native skill-discovery
289
+ * directory); a null/absent one throws rather than silently falling back, so
290
+ * the caller resolves the fallback deliberately (see
291
+ * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,
292
+ * which does exactly that).
293
+ */
294
+ declare function composeSkills(input: ComposeSkillsInput): ComposedSkills;
295
+ /** Throw when the same skill id is delivered both `inline` and `mounted` —
296
+ * the agent would see it twice (once in the prompt body, once as a mounted
297
+ * file it's told to go read), doubling prompt bytes and inviting drift
298
+ * between the two copies. Lists every offending id in the message. */
299
+ declare function assertSkillDeliveryDisjoint(inlineIds: Iterable<string>, mountedIds: Iterable<string>): void;
300
+ /**
301
+ * Combine multiple {@link ComposedSkills} batches (e.g. a built-in corpus and
302
+ * an installed catalog) into one, concatenating refs and prompt sections in
303
+ * batch order. This is the seam where inline and mounted deliveries can first
304
+ * collide, so it applies {@link assertSkillDeliveryDisjoint} — a skill id
305
+ * delivered inline by one batch and mounted by another throws instead of
306
+ * reaching the agent twice. Merge through this rather than spreading batch
307
+ * fields by hand.
308
+ */
309
+ declare function mergeComposedSkills(batches: ComposedSkills[]): ComposedSkills;
135
310
 
136
- export { type ComposeShellResourcesInput, type CorpusEntry, type CorpusLoadResult, type GlobModules, type LoadCorpusOptions, type SkillEntry, composeShellResources, corpusSkills, loadMarkdownCorpus, registrySkills, skillMountPath };
311
+ export { type ComposeShellResourcesInput, type ComposeSkillsInput, type ComposedSkills, type CorpusEntry, type CorpusLoadResult, type GlobModules, type LoadCorpusOptions, type ParsedSkill, type RenderInlineSkillsInput, type RenderSkillIndexInput, type SkillDeliveryMode, type SkillEntry, type SkillFrontmatter, assertSkillDeliveryDisjoint, composeShellResources, composeSkills, corpusSkills, loadMarkdownCorpus, mergeComposedSkills, parseCorpusSkills, parseSkillFrontmatter, registrySkills, renderInlineSkills, renderSkillIndex, skillEntryFromMarkdown, skillMountPath, skillRefs };
@@ -1,15 +1,33 @@
1
1
  import {
2
+ assertSkillDeliveryDisjoint,
2
3
  composeShellResources,
4
+ composeSkills,
3
5
  corpusSkills,
4
6
  loadMarkdownCorpus,
7
+ mergeComposedSkills,
8
+ parseCorpusSkills,
9
+ parseSkillFrontmatter,
5
10
  registrySkills,
6
- skillMountPath
7
- } from "../chunk-KOG473C4.js";
11
+ renderInlineSkills,
12
+ renderSkillIndex,
13
+ skillEntryFromMarkdown,
14
+ skillMountPath,
15
+ skillRefs
16
+ } from "../chunk-NBSBRZ6F.js";
8
17
  export {
18
+ assertSkillDeliveryDisjoint,
9
19
  composeShellResources,
20
+ composeSkills,
10
21
  corpusSkills,
11
22
  loadMarkdownCorpus,
23
+ mergeComposedSkills,
24
+ parseCorpusSkills,
25
+ parseSkillFrontmatter,
12
26
  registrySkills,
13
- skillMountPath
27
+ renderInlineSkills,
28
+ renderSkillIndex,
29
+ skillEntryFromMarkdown,
30
+ skillMountPath,
31
+ skillRefs
14
32
  };
15
33
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,52 @@
1
+ import { Harness } from '../harness/index.js';
2
+ import { SkillEntry, ComposedSkills } from '../skills/index.js';
3
+ import '@tangle-network/agent-interface';
4
+ import '@tangle-network/sandbox';
5
+
6
+ /**
7
+ * Harness-native skill directory resolution — the one place agent-app binds
8
+ * to the platform's authoritative per-harness skill-dir map.
9
+ *
10
+ * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`
11
+ * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`
12
+ * skill lands at on a given harness — that mapping is owned by the platform
13
+ * materializer (`@tangle-network/agent-profile-materialize`'s
14
+ * `skillDirForHarness`), not by app-shell. This subpath exists so no product
15
+ * — and no other agent-app module — ever writes a skill path literal
16
+ * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges
17
+ * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the
18
+ * platform for the answer.
19
+ *
20
+ * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.
21
+ * Products that don't install it simply don't import this subpath — every
22
+ * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)
23
+ * works without it, falling back to `inline` delivery.
24
+ */
25
+
26
+ /** Resolve the cwd-relative skill dir `resources.skills` refs materialize
27
+ * into on `harness` — via the platform's `skillDirForHarness`. `null` when
28
+ * `harness` isn't bridged (see {@link HARNESS_BRIDGE}) or when the platform
29
+ * itself has no cwd skill primitive for it (e.g. `hermes`, user-dir-only). */
30
+ declare function resolveSkillDir(harness: Harness): string | null;
31
+ /** Filter `harnesses` down to those with no mounted skill dir (deduped,
32
+ * first-seen order preserved) — the set that must fall back to `inline`
33
+ * delivery, or that a caller should warn about before offering "mounted"
34
+ * install UX. */
35
+ declare function unsupportedSkillHarnesses(harnesses: Iterable<Harness>): Harness[];
36
+ /** Inputs to {@link composeSkillsForHarness}. */
37
+ interface ComposeSkillsForHarnessInput {
38
+ skills: SkillEntry[];
39
+ harness: Harness;
40
+ tier?: string;
41
+ heading?: string;
42
+ }
43
+ /**
44
+ * Compose {@link ComposedSkills} for `harness`: `mounted` delivery when the
45
+ * platform names a cwd skill dir for it, `inline` delivery (the automatic
46
+ * fallback that keeps every skill available on every harness) otherwise. The
47
+ * one function a product calls instead of hand-checking `resolveSkillDir`
48
+ * and branching between {@link composeSkills}'s two modes itself.
49
+ */
50
+ declare function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills;
51
+
52
+ export { type ComposeSkillsForHarnessInput, ComposedSkills, SkillEntry, composeSkillsForHarness, resolveSkillDir, unsupportedSkillHarnesses };
@@ -0,0 +1,44 @@
1
+ import {
2
+ composeSkills
3
+ } from "../chunk-NBSBRZ6F.js";
4
+
5
+ // src/skills-placement/index.ts
6
+ import { skillDirForHarness } from "@tangle-network/agent-profile-materialize";
7
+ var HARNESS_BRIDGE = {
8
+ opencode: "opencode",
9
+ "claude-code": "claude-code",
10
+ nanoclaw: "nanoclaw",
11
+ "kimi-code": "kimi-code",
12
+ codex: "codex",
13
+ pi: "pi",
14
+ hermes: "hermes",
15
+ openclaw: "openclaw"
16
+ };
17
+ function resolveSkillDir(harness) {
18
+ const bridged = HARNESS_BRIDGE[harness];
19
+ if (!bridged) return null;
20
+ return skillDirForHarness(bridged);
21
+ }
22
+ function unsupportedSkillHarnesses(harnesses) {
23
+ const seen = /* @__PURE__ */ new Set();
24
+ const out = [];
25
+ for (const harness of harnesses) {
26
+ if (resolveSkillDir(harness) !== null) continue;
27
+ if (seen.has(harness)) continue;
28
+ seen.add(harness);
29
+ out.push(harness);
30
+ }
31
+ return out;
32
+ }
33
+ function composeSkillsForHarness(input) {
34
+ const { skills, harness, tier, heading } = input;
35
+ const skillDir = resolveSkillDir(harness);
36
+ if (skillDir) return composeSkills({ skills, mode: "mounted", skillDir, tier, heading });
37
+ return composeSkills({ skills, mode: "inline", tier, heading });
38
+ }
39
+ export {
40
+ composeSkillsForHarness,
41
+ resolveSkillDir,
42
+ unsupportedSkillHarnesses
43
+ };
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/skills-placement/index.ts"],"sourcesContent":["/**\n * Harness-native skill directory resolution — the one place agent-app binds\n * to the platform's authoritative per-harness skill-dir map.\n *\n * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`\n * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`\n * skill lands at on a given harness — that mapping is owned by the platform\n * materializer (`@tangle-network/agent-profile-materialize`'s\n * `skillDirForHarness`), not by app-shell. This subpath exists so no product\n * — and no other agent-app module — ever writes a skill path literal\n * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges\n * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the\n * platform for the answer.\n *\n * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.\n * Products that don't install it simply don't import this subpath — every\n * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)\n * works without it, falling back to `inline` delivery.\n */\n\nimport type { Harness } from '../harness/index'\nimport { skillDirForHarness, type HarnessId } from '@tangle-network/agent-profile-materialize'\nimport { composeSkills, type ComposedSkills, type SkillEntry } from '../skills/index'\n\n/** agent-app `Harness` -> platform `HarnessId`, identity-mapped for exactly\n * the harnesses the platform map covers. Harnesses absent here (`amp`,\n * `factory-droids`, `forge`, `acp`, `cursor`, `cli-base`) resolve to `null` —\n * callers fall back to `inline` delivery. `cursor`'s adapter supports\n * `resources.skills` bespokely but isn't in the platform map yet; treating it\n * as unbridged (inline fallback) is the safe posture until the map covers\n * it, rather than guessing its cwd skill dir here. */\nconst HARNESS_BRIDGE: Partial<Record<Harness, HarnessId>> = {\n opencode: 'opencode',\n 'claude-code': 'claude-code',\n nanoclaw: 'nanoclaw',\n 'kimi-code': 'kimi-code',\n codex: 'codex',\n pi: 'pi',\n hermes: 'hermes',\n openclaw: 'openclaw',\n}\n\n/** Resolve the cwd-relative skill dir `resources.skills` refs materialize\n * into on `harness` — via the platform's `skillDirForHarness`. `null` when\n * `harness` isn't bridged (see {@link HARNESS_BRIDGE}) or when the platform\n * itself has no cwd skill primitive for it (e.g. `hermes`, user-dir-only). */\nexport function resolveSkillDir(harness: Harness): string | null {\n const bridged = HARNESS_BRIDGE[harness]\n if (!bridged) return null\n return skillDirForHarness(bridged)\n}\n\n/** Filter `harnesses` down to those with no mounted skill dir (deduped,\n * first-seen order preserved) — the set that must fall back to `inline`\n * delivery, or that a caller should warn about before offering \"mounted\"\n * install UX. */\nexport function unsupportedSkillHarnesses(harnesses: Iterable<Harness>): Harness[] {\n const seen = new Set<Harness>()\n const out: Harness[] = []\n for (const harness of harnesses) {\n if (resolveSkillDir(harness) !== null) continue\n if (seen.has(harness)) continue\n seen.add(harness)\n out.push(harness)\n }\n return out\n}\n\n/** Inputs to {@link composeSkillsForHarness}. */\nexport interface ComposeSkillsForHarnessInput {\n skills: SkillEntry[]\n harness: Harness\n tier?: string\n heading?: string\n}\n\n/**\n * Compose {@link ComposedSkills} for `harness`: `mounted` delivery when the\n * platform names a cwd skill dir for it, `inline` delivery (the automatic\n * fallback that keeps every skill available on every harness) otherwise. The\n * one function a product calls instead of hand-checking `resolveSkillDir`\n * and branching between {@link composeSkills}'s two modes itself.\n */\nexport function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills {\n const { skills, harness, tier, heading } = input\n const skillDir = resolveSkillDir(harness)\n if (skillDir) return composeSkills({ skills, mode: 'mounted', skillDir, tier, heading })\n return composeSkills({ skills, mode: 'inline', tier, heading })\n}\n\nexport type { ComposedSkills, SkillEntry } from '../skills/index'\n"],"mappings":";;;;;AAqBA,SAAS,0BAA0C;AAUnD,IAAM,iBAAsD;AAAA,EAC1D,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,aAAa;AAAA,EACb,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,UAAU;AACZ;AAMO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,UAAU,eAAe,OAAO;AACtC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,mBAAmB,OAAO;AACnC;AAMO,SAAS,0BAA0B,WAAyC;AACjF,QAAM,OAAO,oBAAI,IAAa;AAC9B,QAAM,MAAiB,CAAC;AACxB,aAAW,WAAW,WAAW;AAC/B,QAAI,gBAAgB,OAAO,MAAM,KAAM;AACvC,QAAI,KAAK,IAAI,OAAO,EAAG;AACvB,SAAK,IAAI,OAAO;AAChB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAiBO,SAAS,wBAAwB,OAAqD;AAC3F,QAAM,EAAE,QAAQ,SAAS,MAAM,QAAQ,IAAI;AAC3C,QAAM,WAAW,gBAAgB,OAAO;AACxC,MAAI,SAAU,QAAO,cAAc,EAAE,QAAQ,MAAM,WAAW,UAAU,MAAM,QAAQ,CAAC;AACvF,SAAO,cAAc,EAAE,QAAQ,MAAM,UAAU,MAAM,QAAQ,CAAC;AAChE;","names":[]}
@@ -68,7 +68,7 @@ import {
68
68
  useSmoothText,
69
69
  useThinkingSeconds,
70
70
  waterfallLayout
71
- } from "../chunk-PHIXZC6Y.js";
71
+ } from "../chunk-O6H2WD3I.js";
72
72
  import {
73
73
  tabTerminalConnectionId,
74
74
  useSandboxTerminalConnection
@@ -122,7 +122,7 @@ import {
122
122
  stampInteractionAnswers
123
123
  } from "../chunk-XAWFPMAR.js";
124
124
  import "../chunk-SIXYZ2FB.js";
125
- import "../chunk-E7QYOOON.js";
125
+ import "../chunk-MCJSS6SM.js";
126
126
  export {
127
127
  AgentActivityPanel,
128
128
  AgentSessionControls,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.43.48",
3
+ "version": "0.43.49",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [
@@ -92,6 +92,11 @@
92
92
  "import": "./dist/skills/index.js",
93
93
  "default": "./dist/skills/index.js"
94
94
  },
95
+ "./skills-placement": {
96
+ "types": "./dist/skills-placement/index.d.ts",
97
+ "import": "./dist/skills-placement/index.js",
98
+ "default": "./dist/skills-placement/index.js"
99
+ },
95
100
  "./profile": {
96
101
  "types": "./dist/profile/index.d.ts",
97
102
  "import": "./dist/profile/index.js",
@@ -411,17 +416,18 @@
411
416
  "@tangle-network/agent-integrations": "^0.44.0",
412
417
  "@tangle-network/agent-interface": "^0.15.0",
413
418
  "@tangle-network/agent-knowledge": "^1.7.0",
419
+ "@tangle-network/agent-profile-materialize": "^0.6.0",
414
420
  "@tangle-network/agent-runtime": "^0.79.3",
415
421
  "@tangle-network/brand": "^1.0.0",
416
422
  "@tangle-network/sandbox": "^0.10.5",
417
423
  "@tangle-network/sandbox-ui": "^0.72.0",
418
424
  "@tangle-network/ui": "^11.0.0",
419
425
  "@testing-library/dom": "^10.4.1",
420
- "@xyflow/react": "^12.0.0",
421
426
  "@testing-library/react": "^16.3.2",
422
427
  "@types/better-sqlite3": "^7.6.13",
423
428
  "@types/node": "^25.6.0",
424
429
  "@types/react": "^19.0.0",
430
+ "@xyflow/react": "^12.0.0",
425
431
  "better-auth": "^1.6.16",
426
432
  "better-sqlite3": "^12.10.0",
427
433
  "drizzle-orm": "^0.45.2",
@@ -446,6 +452,7 @@
446
452
  "@tangle-network/agent-integrations": ">=0.44.0",
447
453
  "@tangle-network/agent-interface": ">=0.15.0",
448
454
  "@tangle-network/agent-knowledge": ">=1.7.0",
455
+ "@tangle-network/agent-profile-materialize": ">=0.6.0",
449
456
  "@tangle-network/agent-runtime": ">=0.79.3",
450
457
  "@tangle-network/brand": ">=1.0.0",
451
458
  "@tangle-network/sandbox": ">=0.9.7",
@@ -470,6 +477,9 @@
470
477
  "@tangle-network/agent-knowledge": {
471
478
  "optional": true
472
479
  },
480
+ "@tangle-network/agent-profile-materialize": {
481
+ "optional": true
482
+ },
473
483
  "@tangle-network/agent-runtime": {
474
484
  "optional": true
475
485
  },
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list mirrors the sandbox SDK's `BackendType` as a\n * plain string union (no sandbox dependency). The consumer owns storage — which\n * harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n *\n * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it\n * comes from `@tangle-network/agent-interface`, the single source of truth shared with the\n * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the\n * session lock.\n */\n\nimport {\n harnessSupportsModel,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\n\n/** The known coding-agent backends. Mirrors `@tangle-network/sandbox`'s\n * `BackendType`; kept structural so this module needs no sandbox dependency. */\nexport const KNOWN_HARNESSES = [\n 'opencode',\n 'claude-code',\n 'kimi-code',\n 'codex',\n 'amp',\n 'factory-droids',\n 'pi',\n 'hermes',\n 'forge',\n 'openclaw',\n 'acp',\n 'cursor',\n 'cli-base',\n] as const\n\nexport type Harness = (typeof KNOWN_HARNESSES)[number]\n\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.\n *\n * agent-app's `Harness` taxonomy is a superset of agent-interface's `HarnessType` (it carries\n * `forge`/`cursor`, which agent-interface doesn't list). Those extra runners have no provider lock\n * there, so they resolve as router-backed (any model) — the correct behavior — which makes the\n * `as HarnessType` casts safe. The snap helpers only ever return a vendor-locked harness or\n * `opencode`, all of which are valid `Harness` values.\n */\n\nexport { modelProvider }\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness as HarnessType, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness as HarnessType, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n return aiSnapHarnessToModel(harness as HarnessType, modelId) as Harness\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * Call before dispatching a sandbox turn so a bypassed UI can't reach the sidecar\n * with an incompatible pair. */\nexport function assertHarnessModelCompatible(harness: Harness, modelId: string): void {\n if (!isModelCompatibleWithHarness(harness, modelId)) {\n const provider = modelProvider(modelId)\n const native = preferredHarnessForModel(modelId)\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${provider}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n"],"mappings":";AAqBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,OAEjB;AAIA,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,IAAM,kBAA2B;AAExC,IAAM,cAAmC,IAAI,IAAI,eAAe;AAEzD,SAAS,UAAU,OAAkC;AAC1D,SAAO,OAAO,UAAU,YAAY,YAAY,IAAI,KAAK;AAC3D;AAGO,SAAS,cAAc,OAAgB,WAAoB,iBAA0B;AAC1F,SAAO,UAAU,KAAK,IAAI,QAAQ;AACpC;AAgCO,SAAS,sBAAsB,QAAoC,CAAC,GAA2B;AACpG,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,UAAU,MAAM,cAAc,GAAG;AACnC,UAAM,SAAS,MAAM;AACrB,UAAM,gBAAgB,UAAU,MAAM,SAAS,KAAK,MAAM,cAAc;AACxE,WAAO,EAAE,SAAS,QAAQ,QAAQ,MAAM,cAAc;AAAA,EACxD;AACA,QAAM,UAAU,cAAc,MAAM,WAAW,cAAc,MAAM,kBAAkB,QAAQ,CAAC;AAC9F,SAAO,EAAE,SAAS,QAAQ,OAAO,eAAe,MAAM;AACxD;AAgBO,SAAS,6BAA6B,SAAkB,SAA0B;AACvF,SAAO,qBAAqB,SAAwB,OAAO;AAC7D;AAKO,SAAS,mBAAmB,SAAkB,SAAiB,cAAyC;AAC7G,SAAO,qBAAqB,SAAwB,SAAS,YAAY;AAC3E;AAIO,SAAS,mBAAmB,SAAkB,SAA0B;AAC7E,SAAO,qBAAqB,SAAwB,OAAO;AAC7D;AAKO,SAAS,6BAA6B,SAAkB,SAAuB;AACpF,MAAI,CAAC,6BAA6B,SAAS,OAAO,GAAG;AACnD,UAAM,WAAW,cAAc,OAAO;AACtC,UAAM,SAAS,yBAAyB,OAAO;AAC/C,UAAM,IAAI;AAAA,MACR,YAAY,OAAO,uBAAuB,OAAO,gBAAgB,QAAQ,WAChE,UAAU,oCAAoC;AAAA,IACzD;AAAA,EACF;AACF;","names":[]}
@@ -1,132 +0,0 @@
1
- // src/skills/index.ts
2
- function inlineResource(name, content) {
3
- return { kind: "inline", name, content };
4
- }
5
- function skillMountPath(id) {
6
- return `~/.claude/skills/${id}/SKILL.md`;
7
- }
8
- function normalizeKey(key, anchor) {
9
- const marker = `${anchor}/`;
10
- const at = key.lastIndexOf(marker);
11
- if (at >= 0) return key.slice(at);
12
- return key.startsWith("./") ? key.slice(2) : key;
13
- }
14
- function toCorpusId(normalizedKey, anchor) {
15
- const nested = normalizedKey.match(new RegExp(`${anchor}/([^/]+)/SKILL\\.md$`));
16
- if (nested) return nested[1];
17
- const flat = normalizedKey.match(new RegExp(`${anchor}/(.+)\\.md$`));
18
- if (flat) return flat[1];
19
- return normalizedKey;
20
- }
21
- function nodeBuiltins() {
22
- const getBuiltin = globalThis.process?.getBuiltinModule;
23
- if (typeof getBuiltin !== "function") return void 0;
24
- return {
25
- fs: getBuiltin("node:fs"),
26
- path: getBuiltin("node:path"),
27
- url: getBuiltin("node:url")
28
- };
29
- }
30
- function fsWalkFlat(builtins, root) {
31
- const { fs, path } = builtins;
32
- const out = {};
33
- if (!fs.existsSync(root)) return out;
34
- const walk = (dir) => {
35
- let entries;
36
- try {
37
- entries = fs.readdirSync(dir, { withFileTypes: true });
38
- } catch {
39
- return;
40
- }
41
- for (const entry of entries) {
42
- if (entry.name.startsWith(".")) continue;
43
- const full = path.join(dir, entry.name);
44
- if (entry.isDirectory()) walk(full);
45
- else if (entry.isFile() && entry.name.endsWith(".md")) out[full] = fs.readFileSync(full, "utf8");
46
- }
47
- };
48
- walk(root);
49
- return out;
50
- }
51
- function fsWalkNested(builtins, root) {
52
- const { fs, path } = builtins;
53
- const out = {};
54
- if (!fs.existsSync(root)) return out;
55
- let entries;
56
- try {
57
- entries = fs.readdirSync(root, { withFileTypes: true });
58
- } catch {
59
- return out;
60
- }
61
- for (const entry of entries) {
62
- if (!entry.isDirectory()) continue;
63
- const skillFile = path.join(root, entry.name, "SKILL.md");
64
- if (!fs.existsSync(skillFile)) continue;
65
- out[skillFile] = fs.readFileSync(skillFile, "utf8");
66
- }
67
- return out;
68
- }
69
- function resolveFsBase(builtins, fsBaseDir, importMetaUrl) {
70
- const { path, url } = builtins;
71
- if (path.isAbsolute(fsBaseDir)) return fsBaseDir;
72
- const here = importMetaUrl ? path.dirname(url.fileURLToPath(importMetaUrl)) : process.cwd();
73
- return path.join(here, fsBaseDir);
74
- }
75
- function loadMarkdownCorpus(options, importMetaUrl) {
76
- const { anchor, globModules, fsBaseDir, fsLayout = "flat", skip } = options;
77
- let modules;
78
- let source;
79
- if (globModules && Object.keys(globModules).length > 0) {
80
- modules = globModules;
81
- source = "vite";
82
- } else {
83
- const builtins = nodeBuiltins();
84
- if (builtins && fsBaseDir) {
85
- const root = resolveFsBase(builtins, fsBaseDir, importMetaUrl);
86
- modules = fsLayout === "nested" ? fsWalkNested(builtins, root) : fsWalkFlat(builtins, root);
87
- source = Object.keys(modules).length > 0 ? "fs" : "empty";
88
- } else {
89
- modules = {};
90
- source = "empty";
91
- }
92
- }
93
- const entries = [];
94
- for (const [rawKey, content] of Object.entries(modules)) {
95
- if (typeof content !== "string") continue;
96
- const key = normalizeKey(rawKey, anchor);
97
- if (skip && skip(key)) continue;
98
- entries.push({ id: toCorpusId(key, anchor), key, content });
99
- }
100
- entries.sort((a, b) => a.id.localeCompare(b.id));
101
- return { source, entries };
102
- }
103
- function corpusSkills(corpus, anchor) {
104
- return corpus.map(
105
- (entry) => ({
106
- path: `${anchor}/${entry.id}.md`,
107
- resource: inlineResource(`${anchor}-${entry.id}`, entry.content)
108
- })
109
- ).sort((a, b) => a.path.localeCompare(b.path));
110
- }
111
- function registrySkills(registry, tier = "free") {
112
- return registry.filter((s) => s.tier === tier).map(
113
- (s) => ({
114
- path: skillMountPath(s.id),
115
- resource: inlineResource(s.id, s.skillMd)
116
- })
117
- ).sort((a, b) => a.path.localeCompare(b.path));
118
- }
119
- function composeShellResources(input) {
120
- const { skills = [], knowledge = [], evolvable = [], registry = [], predicate } = input;
121
- const composed = [...skills, ...knowledge, ...evolvable, ...registry];
122
- return predicate ? composed.filter(predicate) : composed;
123
- }
124
-
125
- export {
126
- skillMountPath,
127
- loadMarkdownCorpus,
128
- corpusSkills,
129
- registrySkills,
130
- composeShellResources
131
- };
132
- //# sourceMappingURL=chunk-KOG473C4.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/skills/index.ts"],"sourcesContent":["/**\n * Unified skill + corpus mounter for agent products.\n *\n * Every agent product hand-rolls the same two file-mount systems and then\n * drifts on the seams between them. (1) An ALWAYS-MOUNTED markdown corpus —\n * (`skills/<slug>/SKILL.md`, `doctrine` and `knowledge` markdown trees) — discovered\n * by a Vite `?raw` glob in the Worker bundle and by Node `fs` under the eval\n * CLI, then projected into `resources.files`. (2) A TIER-GATED installable\n * registry — a hand-authored array of `SkillEntry` whose free tier mounts at\n * the harness skill-discovery path and whose paid tier is installed on demand.\n * Both ride the same `resources.files` channel but use different provenance\n * (file-backed vs inline), different mount paths (relative corpus path vs\n * `~/.claude/skills/<id>/SKILL.md`), and different selection rules. This module\n * makes both DATA: a corpus loader that accepts a Vite glob-result map (or an\n * fs fallback), a registry adapter that tier-gates, and a single\n * `composeShellResources` that projects either onto the SDK file-mount shape.\n *\n * Substrate-free over storage, exact over the SDK boundary: the only inbound\n * seam is the glob-result map the consumer passes in (its call site keeps the\n * literal `import.meta.glob` Vite must static-analyze); the only outbound seam\n * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`, the exact shape the\n * agent profile's `resources.files` consumes. Node builtins are resolved lazily\n * via `process.getBuiltinModule` so a static `node:*` import never reaches the\n * Vite SSR bundle.\n */\n\nimport type { AgentProfileFileMount, AgentProfileResourceRef } from '@tangle-network/sandbox'\n\n/** Construct the inline arm of the SDK's `AgentProfileResourceRef`. Inlined here\n * so this leaf subpath stays type-only over `@tangle-network/sandbox` — it\n * carries no runtime dependency on the SDK, just its file-mount type contract. */\nfunction inlineResource(name: string, content: string): AgentProfileResourceRef {\n return { kind: 'inline', name, content }\n}\n\n/** A Vite eager `?raw` glob result: glob key -> raw file body. The consumer\n * produces this by calling `import.meta.glob('<lit>', { eager: true, query:\n * '?raw', import: 'default' })` at its own call site — the literal must stay\n * literal so Vite can static-analyze it; passing the result here keeps that\n * constraint at the edge and the loader substrate-free. */\nexport type GlobModules = Record<string, string>\n\n/** One markdown document discovered from the corpus. */\nexport interface CorpusEntry {\n /** Slug derived from the glob key (folder slug for `SKILL.md` layouts, or the\n * normalized relative path for flat `*.md` layouts). */\n id: string\n /** Glob/fs key the entry was loaded from, normalized to a stable relative\n * form (leading `./` and absolute prefixes stripped). */\n key: string\n /** Raw markdown body (including any frontmatter). */\n content: string\n}\n\n/** A hand-authored, tier-gated installable skill. Mirrors the per-product\n * registry entry (gtm/insurance `SkillEntry`); the runtime's certified `skill`\n * artifact kind is unrelated. `skillMd` is the inline body — file provenance\n * does not apply to the registry. */\nexport interface SkillEntry {\n id: string\n name: string\n description: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n /** Gate keyword. `composeShellResources`/`registrySkills` treat `free` as\n * always-mounted; everything else is install-on-demand. */\n tier: string\n skillMd: string\n}\n\n/** Harness skill-discovery path the Claude Code / OpenCode backend reads\n * natively. The registry mounts here; the corpus mounts at its relative path. */\nexport function skillMountPath(id: string): string {\n return `~/.claude/skills/${id}/SKILL.md`\n}\n\n/** Strip a glob/fs key down to a stable relative form: drop a leading `./`,\n * and for an absolute fs path keep only the tail from the last anchor segment\n * the pattern implies. We normalize on the trailing `<dir>/.../*.md` so the\n * Vite key (`./skills/x/SKILL.md`) and the fs key (`/abs/.../skills/x/SKILL.md`)\n * collapse to the same value. */\nfunction normalizeKey(key: string, anchor: string): string {\n const marker = `${anchor}/`\n const at = key.lastIndexOf(marker)\n if (at >= 0) return key.slice(at)\n return key.startsWith('./') ? key.slice(2) : key\n}\n\n/** Folder-slug for a `<anchor>/<slug>/SKILL.md` layout; falls back to the\n * normalized key (sans `<anchor>/` prefix, sans `.md`) for flat layouts. */\nfunction toCorpusId(normalizedKey: string, anchor: string): string {\n const nested = normalizedKey.match(new RegExp(`${anchor}/([^/]+)/SKILL\\\\.md$`))\n if (nested) return nested[1]!\n const flat = normalizedKey.match(new RegExp(`${anchor}/(.+)\\\\.md$`))\n if (flat) return flat[1]!\n return normalizedKey\n}\n\n/** Resolve Node builtins lazily. `process.getBuiltinModule` (Node 22+) is\n * absent in workerd, so this returns undefined there and the fs path is never\n * taken — Workers always reach the loader through the Vite glob map. A static\n * `import 'node:fs'` would break Vite SSR bundling in the consumer apps, so it\n * is deliberately avoided. */\nfunction nodeBuiltins():\n | { fs: typeof import('node:fs'); path: typeof import('node:path'); url: typeof import('node:url') }\n | undefined {\n const getBuiltin = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } })\n .process?.getBuiltinModule\n if (typeof getBuiltin !== 'function') return undefined\n return {\n fs: getBuiltin('node:fs') as typeof import('node:fs'),\n path: getBuiltin('node:path') as typeof import('node:path'),\n url: getBuiltin('node:url') as typeof import('node:url'),\n }\n}\n\n/** Options for {@link loadMarkdownCorpus}. */\nexport interface LoadCorpusOptions {\n /** The anchor folder name that appears in both glob keys and fs paths\n * (`skills`, `doctrine`, `knowledge`). Used to normalize keys + derive ids. */\n anchor: string\n /** Vite glob-result map. When present and non-empty it is authoritative and\n * the fs path is skipped. Omit it (or pass an empty map) only outside Vite. */\n globModules?: GlobModules\n /** Absolute or `import.meta.url`-relative base dir the fs fallback walks when\n * `globModules` is empty. Required for the fs path to run; without it the fs\n * fallback returns no entries (Workers never need it). */\n fsBaseDir?: string\n /** Walk strategy for the fs fallback. `nested` finds `<dir>/<slug>/SKILL.md`\n * one level deep; `flat` recurses for every `*.md`. Default: `flat`. */\n fsLayout?: 'nested' | 'flat'\n /** Drop an entry by its normalized key after load. Covers the per-product\n * skip lists (corpus index/log files, scaffold templates, allow-lists). */\n skip?: (normalizedKey: string) => boolean\n}\n\n/** Outcome of {@link loadMarkdownCorpus}: the entries plus which path produced\n * them, so a caller can fail loud when both are empty rather than silently\n * mounting nothing. */\nexport interface CorpusLoadResult {\n source: 'vite' | 'fs' | 'empty'\n entries: CorpusEntry[]\n}\n\nfunction fsWalkFlat(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n const walk = (dir: string) => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const full = path.join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.isFile() && entry.name.endsWith('.md')) out[full] = fs.readFileSync(full, 'utf8')\n }\n }\n walk(root)\n return out\n}\n\nfunction fsWalkNested(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return out\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillFile = path.join(root, entry.name, 'SKILL.md')\n if (!fs.existsSync(skillFile)) continue\n out[skillFile] = fs.readFileSync(skillFile, 'utf8')\n }\n return out\n}\n\n/** Resolve `fsBaseDir` against `import.meta.url` when relative-looking, so a\n * consumer can pass a bare folder name (`'skills'`) and have it land beside\n * the calling module. Absolute paths pass through. */\nfunction resolveFsBase(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n fsBaseDir: string,\n importMetaUrl?: string,\n): string {\n const { path, url } = builtins\n if (path.isAbsolute(fsBaseDir)) return fsBaseDir\n const here = importMetaUrl\n ? path.dirname(url.fileURLToPath(importMetaUrl))\n : process.cwd()\n return path.join(here, fsBaseDir)\n}\n\n/**\n * Load a markdown corpus, preferring a Vite glob-result map and falling back to\n * a Node fs walk. Selection is by non-empty glob result — never an env flag.\n * Entries are normalized, optionally skip-filtered, and sorted by id for\n * determinism. The `import.meta.glob` literal stays at the CONSUMER call site\n * (passed in as `globModules`); this loader never constructs a glob.\n */\nexport function loadMarkdownCorpus(\n options: LoadCorpusOptions,\n importMetaUrl?: string,\n): CorpusLoadResult {\n const { anchor, globModules, fsBaseDir, fsLayout = 'flat', skip } = options\n\n let modules: GlobModules\n let source: CorpusLoadResult['source']\n if (globModules && Object.keys(globModules).length > 0) {\n modules = globModules\n source = 'vite'\n } else {\n const builtins = nodeBuiltins()\n if (builtins && fsBaseDir) {\n const root = resolveFsBase(builtins, fsBaseDir, importMetaUrl)\n modules = fsLayout === 'nested' ? fsWalkNested(builtins, root) : fsWalkFlat(builtins, root)\n source = Object.keys(modules).length > 0 ? 'fs' : 'empty'\n } else {\n modules = {}\n source = 'empty'\n }\n }\n\n const entries: CorpusEntry[] = []\n for (const [rawKey, content] of Object.entries(modules)) {\n if (typeof content !== 'string') continue\n const key = normalizeKey(rawKey, anchor)\n if (skip && skip(key)) continue\n entries.push({ id: toCorpusId(key, anchor), key, content })\n }\n entries.sort((a, b) => a.id.localeCompare(b.id))\n return { source, entries }\n}\n\n/** Project corpus entries onto SDK file mounts at a relative path under\n * `<anchor>/`. Always-mounted: the corpus is the agent's baseline knowledge. */\nexport function corpusSkills(corpus: CorpusEntry[], anchor: string): AgentProfileFileMount[] {\n return corpus\n .map(\n (entry) =>\n ({\n path: `${anchor}/${entry.id}.md`,\n resource: inlineResource(`${anchor}-${entry.id}`, entry.content),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Project the registry's free-tier (or `tier`-matched) entries onto SDK file\n * mounts at the harness skill-discovery path. Tier-gating is the registry's\n * only selection rule — paid skills are installed on demand, not at boot. */\nexport function registrySkills(\n registry: SkillEntry[],\n tier: string = 'free',\n): AgentProfileFileMount[] {\n return registry\n .filter((s) => s.tier === tier)\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: inlineResource(s.id, s.skillMd),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Inputs to {@link composeShellResources}. Each channel is optional so a\n * product mounts only the systems it has — corpus-only, registry-only, or\n * both — without conflating them. */\nexport interface ComposeShellResourcesInput {\n /** Corpus mounts (always-mounted baseline). Pass the result of\n * {@link corpusSkills}, or a hand-built mount list. */\n skills?: AgentProfileFileMount[]\n /** Knowledge-corpus mounts (a second always-mounted corpus, e.g. a domain\n * knowledge pack distinct from the skills corpus). */\n knowledge?: AgentProfileFileMount[]\n /** Evolvable / learned-guidance mounts (single-file corpora). */\n evolvable?: AgentProfileFileMount[]\n /** Registry mounts (tier-gated). Pass the result of {@link registrySkills}. */\n registry?: AgentProfileFileMount[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n predicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/**\n * Compose every mount channel into one `resources.files`-ready array. Corpus\n * channels come first (baseline), the tier-gated registry last (so a registry\n * entry can override a corpus entry that mounts at the same path). The result\n * is exactly `AgentProfileFileMount[]` — assign it straight into\n * `profile.resources.files` with no cast.\n */\nexport function composeShellResources(input: ComposeShellResourcesInput): AgentProfileFileMount[] {\n const { skills = [], knowledge = [], evolvable = [], registry = [], predicate } = input\n const composed = [...skills, ...knowledge, ...evolvable, ...registry]\n return predicate ? composed.filter(predicate) : composed\n}\n"],"mappings":"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","names":[]}