@tangle-network/agent-app 0.43.48 → 0.43.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/assistant/index.js +2 -2
- package/dist/chat-routes/index.js +5 -5
- package/dist/{chunk-UHXQ3KNX.js → chunk-54GSK2KK.js} +4 -4
- package/dist/{chunk-5RJNEEO2.js → chunk-HRJJH3RX.js} +2 -2
- package/dist/{chunk-E7QYOOON.js → chunk-MCJSS6SM.js} +2 -1
- package/dist/chunk-MCJSS6SM.js.map +1 -0
- package/dist/chunk-NBSBRZ6F.js +337 -0
- package/dist/chunk-NBSBRZ6F.js.map +1 -0
- package/dist/{chunk-PHIXZC6Y.js → chunk-O6H2WD3I.js} +2 -2
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/index.js +81 -81
- package/dist/preset-cloudflare/index.js +2 -2
- package/dist/profile/index.d.ts +13 -2
- package/dist/profile/index.js +25 -4
- package/dist/profile/index.js.map +1 -1
- package/dist/prompt/index.d.ts +2 -1
- package/dist/prompt/index.js.map +1 -1
- package/dist/run/index.js +1 -1
- package/dist/sandbox/index.js +2 -2
- package/dist/skills/index.d.ts +183 -8
- package/dist/skills/index.js +21 -3
- package/dist/skills-placement/index.d.ts +52 -0
- package/dist/skills-placement/index.js +44 -0
- package/dist/skills-placement/index.js.map +1 -0
- package/dist/web-react/index.js +2 -2
- package/package.json +12 -2
- package/dist/chunk-E7QYOOON.js.map +0 -1
- package/dist/chunk-KOG473C4.js +0 -132
- package/dist/chunk-KOG473C4.js.map +0 -1
- /package/dist/{chunk-UHXQ3KNX.js.map → chunk-54GSK2KK.js.map} +0 -0
- /package/dist/{chunk-5RJNEEO2.js.map → chunk-HRJJH3RX.js.map} +0 -0
- /package/dist/{chunk-PHIXZC6Y.js.map → chunk-O6H2WD3I.js.map} +0 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ The substrate packages — `@tangle-network/agent-runtime`, `agent-eval`, `agent
|
|
|
12
12
|
|
|
13
13
|
- **Structured tool side channel** — `submit_proposal` (approval-gated), `schedule_followup`, `render_ui`, `add_citation`, exposed as validated tool calls over three surfaces (HTTP route, per-turn MCP server, agent-runtime executor). No fenced-text parsing.
|
|
14
14
|
- **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged.
|
|
15
|
+
- **Assembled chat vertical** — `createChatTurnRoutes` wires auth → thread/message store → streaming turn with buffered replay → uploads → sidecar question answering into one route factory, over `authorize` / `produce` / `store` / `interactions` seams. No hand-rolled orchestration. See [`examples/chat-app.md`](./examples/chat-app.md).
|
|
15
16
|
- **Sandbox-optional** — the same tools, billing, eval, and loop work without a container. A `fetch`-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. See [`examples/browser-copilot.md`](./examples/browser-copilot.md).
|
|
16
17
|
- **Resumable turns (sandbox-free path)** — for a browser/edge copilot streaming the Router directly, buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. **Sandbox products don't need this** — the sandbox SDK already buffers + replays sessions (`streamPrompt` + `lastEventId`). See [`examples/resumable-turns.md`](./examples/resumable-turns.md).
|
|
17
18
|
- **Composes the engine, never forks it** — `/eval` re-exports `@tangle-network/agent-eval`'s verifier; `/integrations` wraps the hub; `/tangle` and `/billing` take the tcloud client as a structural contract. Engines are **peer dependencies** — you pin the version, nothing is bundled.
|
|
@@ -96,6 +97,8 @@ const streamTurn = createOpenAICompatStreamTurn({ ...cfg, tools })
|
|
|
96
97
|
|
|
97
98
|
The full three-transport walkthrough (Tangle Router, tcloud, Vercel AI SDK) is in [`examples/browser-copilot.md`](./examples/browser-copilot.md).
|
|
98
99
|
|
|
100
|
+
Building the full **server chat vertical** instead — auth, thread/message tables, a streaming turn with buffered replay, uploads, and sidecar question answering — is the job of `createChatTurnRoutes` (`/chat-routes`) and the modules around it. The end-to-end assembly, including the durable plan/question workflow and the client composer, is in [`examples/chat-app.md`](./examples/chat-app.md).
|
|
101
|
+
|
|
99
102
|
## How it's organised
|
|
100
103
|
|
|
101
104
|
One rule decides where anything lives:
|
|
@@ -128,6 +131,20 @@ Each is an independent entry point — import only what you use.
|
|
|
128
131
|
| [`/stream`](src/stream) | SSE normalization and turn identity: `normalizeToolEvent`, `resolveChatTurn`, `encodeEvent`, message-part merging. |
|
|
129
132
|
| [`/redact`](src/redact) | `redactForIngestion` — PII redaction before content leaves the boundary. |
|
|
130
133
|
|
|
134
|
+
**The chat vertical** — the assembled server chat stack. Wire it with [`examples/chat-app.md`](./examples/chat-app.md).
|
|
135
|
+
|
|
136
|
+
| Subpath | What it gives you |
|
|
137
|
+
|---|---|
|
|
138
|
+
| [`/chat-routes`](src/chat-routes) | `createChatTurnRoutes` — the assembled turn vertical: NDJSON `turn` + buffered `replay` + `running` reconnect-discovery + composed `interactions` answer endpoints, over `authorize` / `produce` / `store` seams. Plus `createSandboxChatProducer`, `createUploadRoute`, `createSandboxFileIndexRoute`, `withDurableChatProjection`, and the import-free `./wire` contract (`chatTurnRequestInit`, `ChatTurnRequestPayload`). Composes agent-runtime's `handleChatTurn`; subpath-only (not re-exported from the root). |
|
|
139
|
+
| [`/chat-store`](src/chat-store) | `createChatTables` + `createChatStore` — drizzle thread/message persistence behind the `ChatStore` port, and the canonical `ChatMessagePart` parts vocabulary (`toChatMessageParts`, part guards). The drizzle store/schema is subpath-only. |
|
|
140
|
+
| [`/interactions`](src/interactions) | Human-in-the-loop ask channel: `createInteractionAnswerRoute` (list/answer endpoint factory — validation, 410 mapping, duplicate-answer safety), the server sidecar client, and the shared `ChatInteraction` wire/persisted-part contract + codecs. Composes `@tangle-network/agent-interface` types. |
|
|
141
|
+
| [`/durable-chat`](src/durable-chat) | Durable plan/question workflow around the authoritative channels: `createDurablePlanRoutes`, `createDurableChatScope`, `createDurableChatEventProjection`, `createDurableInteractionRoutePersistence`, the `DurablePlanStore` port, and `InMemoryDurableChatStateStore` (tests/demos only — production supplies the store). |
|
|
142
|
+
| [`/plans`](src/plans) | Browser-safe durable-plan chat projection: `parsePlanSubmittedEvent`, `planToPersistedPart` / `persistedPartToPlan`, `canTransitionPlanStatus`, and the `ChatPlan` union. Byte-matches the SDK's `SandboxSession.plan()`. |
|
|
143
|
+
| [`/object-store`](src/object-store) | Content-addressed durable attachment store: the `ObjectStore` port + `createR2ObjectStore`, `signObjectUrl` / `verifyObjectUrl`, `createProxiedArtifactRoute`, `objectKey`. |
|
|
144
|
+
| [`/app-auth`](src/app-auth) | `createAppAuth` — better-auth config factory + request guards (`requireApiUser`) over the standard users/sessions/accounts/verifications tables; composes `/platform`'s SSO cookie minter. Optional `better-auth` peer; subpath-only. |
|
|
145
|
+
| [`/sandbox`](src/sandbox) | Workspace sandbox provisioning + turn streaming: `ensureWorkspaceSandbox` / `peekWorkspaceSandbox`, `streamSandboxPrompt` / `runSandboxPrompt`, `createWorkspaceSandboxManager`, and terminal / runtime-proxy handlers. Peer `@tangle-network/sandbox`; subpath-only. |
|
|
146
|
+
| [`/platform`](src/platform) | Tangle platform glue: cross-site SSO (`createTangleSsoHandlers`), request guards (`createAuthGuard`, `guardResolution`), the hub proxy, and seat billing. |
|
|
147
|
+
|
|
131
148
|
The root entry (`@tangle-network/agent-app`) re-exports every module, but importing the subpath keeps your bundle to what you use.
|
|
132
149
|
|
|
133
150
|
### Missions: id shape and product columns
|
package/dist/assistant/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
ChatMessages,
|
|
4
4
|
ModelPicker,
|
|
5
5
|
ProviderLogo
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-O6H2WD3I.js";
|
|
7
7
|
import "../chunk-65P3HJY3.js";
|
|
8
8
|
import "../chunk-LCNY3DCM.js";
|
|
9
9
|
import "../chunk-2QI7XV2T.js";
|
|
@@ -11,7 +11,7 @@ import "../chunk-JGYOYY5D.js";
|
|
|
11
11
|
import "../chunk-5MG74GVQ.js";
|
|
12
12
|
import "../chunk-XAWFPMAR.js";
|
|
13
13
|
import "../chunk-SIXYZ2FB.js";
|
|
14
|
-
import "../chunk-
|
|
14
|
+
import "../chunk-MCJSS6SM.js";
|
|
15
15
|
|
|
16
16
|
// src/assistant/sse.ts
|
|
17
17
|
var SSEChunkParser = class {
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
|
|
7
7
|
reconcileStaleTurnLock
|
|
8
8
|
} from "../chunk-BQV42AFK.js";
|
|
9
|
+
import {
|
|
10
|
+
parseJsonObjectBody
|
|
11
|
+
} from "../chunk-HFC4BTWJ.js";
|
|
9
12
|
import {
|
|
10
13
|
coalesceDeltas,
|
|
11
14
|
createBufferedTurnTap,
|
|
@@ -16,9 +19,6 @@ import {
|
|
|
16
19
|
import {
|
|
17
20
|
createInteractionAnswerRoute
|
|
18
21
|
} from "../chunk-6KVH5SP5.js";
|
|
19
|
-
import {
|
|
20
|
-
parseJsonObjectBody
|
|
21
|
-
} from "../chunk-HFC4BTWJ.js";
|
|
22
22
|
import {
|
|
23
23
|
ChatTurnInputError,
|
|
24
24
|
DISPATCH_MAX_MEDIA_PARTS,
|
|
@@ -69,8 +69,8 @@ import {
|
|
|
69
69
|
flattenHistory,
|
|
70
70
|
readSandboxBinaryBytes,
|
|
71
71
|
statSandboxFileSize
|
|
72
|
-
} from "../chunk-
|
|
73
|
-
import "../chunk-
|
|
72
|
+
} from "../chunk-HRJJH3RX.js";
|
|
73
|
+
import "../chunk-MCJSS6SM.js";
|
|
74
74
|
import "../chunk-PJC4NXPA.js";
|
|
75
75
|
import "../chunk-7EVZUIHW.js";
|
|
76
76
|
import "../chunk-S5SRJJQG.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createWorkspaceKeyManager
|
|
3
|
-
} from "./chunk-G3HCU7TA.js";
|
|
4
1
|
import {
|
|
5
2
|
createFieldCrypto
|
|
6
3
|
} from "./chunk-TA5Q4I2K.js";
|
|
4
|
+
import {
|
|
5
|
+
createWorkspaceKeyManager
|
|
6
|
+
} from "./chunk-G3HCU7TA.js";
|
|
7
7
|
|
|
8
8
|
// src/preset-cloudflare/index.ts
|
|
9
9
|
var PRESET_TABLES = {
|
|
@@ -294,4 +294,4 @@ export {
|
|
|
294
294
|
createPresetWorkspaceKeyStore,
|
|
295
295
|
createPresetWorkspaceKeyManager
|
|
296
296
|
};
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
297
|
+
//# sourceMappingURL=chunk-54GSK2KK.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertHarnessModelCompatible
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MCJSS6SM.js";
|
|
4
4
|
import {
|
|
5
5
|
buildAppToolMcpServer
|
|
6
6
|
} from "./chunk-7EVZUIHW.js";
|
|
@@ -1628,4 +1628,4 @@ export {
|
|
|
1628
1628
|
isTerminalPromptEvent,
|
|
1629
1629
|
detectInteractiveQuestion
|
|
1630
1630
|
};
|
|
1631
|
-
//# sourceMappingURL=chunk-
|
|
1631
|
+
//# sourceMappingURL=chunk-HRJJH3RX.js.map
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
var KNOWN_HARNESSES = [
|
|
10
10
|
"opencode",
|
|
11
11
|
"claude-code",
|
|
12
|
+
"nanoclaw",
|
|
12
13
|
"kimi-code",
|
|
13
14
|
"codex",
|
|
14
15
|
"amp",
|
|
@@ -70,4 +71,4 @@ export {
|
|
|
70
71
|
snapHarnessToModel,
|
|
71
72
|
assertHarnessModelCompatible
|
|
72
73
|
};
|
|
73
|
-
//# sourceMappingURL=chunk-
|
|
74
|
+
//# sourceMappingURL=chunk-MCJSS6SM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 'nanoclaw',\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;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":[]}
|
|
@@ -0,0 +1,337 @@
|
|
|
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
|
+
function parseFrontmatterScalar(value) {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
if (trimmed.startsWith('"')) return JSON.parse(trimmed);
|
|
127
|
+
return trimmed;
|
|
128
|
+
}
|
|
129
|
+
function parseSkillFrontmatter(raw) {
|
|
130
|
+
const lines = raw.split("\n");
|
|
131
|
+
if ((lines[0] ?? "").trim() !== "---") {
|
|
132
|
+
return { frontmatter: {}, body: raw, raw };
|
|
133
|
+
}
|
|
134
|
+
let closeIndex = -1;
|
|
135
|
+
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
136
|
+
if ((lines[i2] ?? "").trim() === "---") {
|
|
137
|
+
closeIndex = i2;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (closeIndex === -1) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
'parseSkillFrontmatter: opening "---" has no closing "---" \u2014 truncated frontmatter block'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const blockLines = lines.slice(1, closeIndex);
|
|
147
|
+
const body = lines.slice(closeIndex + 1).join("\n").replace(/^\n+/, "");
|
|
148
|
+
const frontmatter = {};
|
|
149
|
+
const scalarKeys = /* @__PURE__ */ new Set(["id", "name", "description", "source", "category", "tier"]);
|
|
150
|
+
const topLineRe = /^(\S[^:]*):\s*(.*)$/;
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (i < blockLines.length) {
|
|
153
|
+
const line = blockLines[i] ?? "";
|
|
154
|
+
if (line.trim() === "") {
|
|
155
|
+
i++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const match = line.match(topLineRe);
|
|
159
|
+
if (!match) {
|
|
160
|
+
throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`);
|
|
161
|
+
}
|
|
162
|
+
const key = (match[1] ?? "").trim();
|
|
163
|
+
const rest = match[2] ?? "";
|
|
164
|
+
if (key === "author") {
|
|
165
|
+
if (rest.trim() !== "") {
|
|
166
|
+
throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`);
|
|
167
|
+
}
|
|
168
|
+
const author = { name: "" };
|
|
169
|
+
let sawName = false;
|
|
170
|
+
i++;
|
|
171
|
+
while (i < blockLines.length && /^\s+\S/.test(blockLines[i] ?? "")) {
|
|
172
|
+
const sub = (blockLines[i] ?? "").trim();
|
|
173
|
+
const subMatch = sub.match(/^(name|url):\s*(.*)$/);
|
|
174
|
+
if (!subMatch) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(blockLines[i])}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const subKey = subMatch[1];
|
|
180
|
+
const subValue = parseFrontmatterScalar(subMatch[2] ?? "");
|
|
181
|
+
if (subKey === "name") {
|
|
182
|
+
author.name = subValue;
|
|
183
|
+
sawName = true;
|
|
184
|
+
} else {
|
|
185
|
+
author.url = subValue;
|
|
186
|
+
}
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
if (sawName) frontmatter.author = author;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (key === "tags") {
|
|
193
|
+
const inline = rest.trim();
|
|
194
|
+
if (inline.startsWith("[") && inline.endsWith("]")) {
|
|
195
|
+
const inner = inline.slice(1, -1).trim();
|
|
196
|
+
frontmatter.tags = inner === "" ? [] : inner.split(",").map((t) => parseFrontmatterScalar(t));
|
|
197
|
+
i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (inline !== "") {
|
|
201
|
+
throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`);
|
|
202
|
+
}
|
|
203
|
+
const tags = [];
|
|
204
|
+
i++;
|
|
205
|
+
while (i < blockLines.length && /^\s*-\s*/.test(blockLines[i] ?? "")) {
|
|
206
|
+
tags.push(parseFrontmatterScalar((blockLines[i] ?? "").replace(/^\s*-\s*/, "")));
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
frontmatter.tags = tags;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (scalarKeys.has(key)) {
|
|
213
|
+
;
|
|
214
|
+
frontmatter[key] = parseFrontmatterScalar(rest);
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
return { frontmatter, body, raw };
|
|
219
|
+
}
|
|
220
|
+
function skillEntryFromMarkdown(raw, fallbackId) {
|
|
221
|
+
const { frontmatter } = parseSkillFrontmatter(raw);
|
|
222
|
+
const id = frontmatter.id ?? fallbackId;
|
|
223
|
+
if (!id) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'skillEntryFromMarkdown: no "id" in frontmatter and no fallbackId supplied \u2014 cannot build a SkillEntry'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const entry = {
|
|
229
|
+
id,
|
|
230
|
+
name: frontmatter.name ?? id,
|
|
231
|
+
description: frontmatter.description ?? "",
|
|
232
|
+
tier: frontmatter.tier ?? "free",
|
|
233
|
+
skillMd: raw
|
|
234
|
+
};
|
|
235
|
+
if (frontmatter.author) entry.author = frontmatter.author;
|
|
236
|
+
if (frontmatter.source) entry.source = frontmatter.source;
|
|
237
|
+
if (frontmatter.category) entry.category = frontmatter.category;
|
|
238
|
+
if (frontmatter.tags) entry.tags = frontmatter.tags;
|
|
239
|
+
return entry;
|
|
240
|
+
}
|
|
241
|
+
function parseCorpusSkills(corpus) {
|
|
242
|
+
return corpus.map((entry) => skillEntryFromMarkdown(entry.content, entry.id));
|
|
243
|
+
}
|
|
244
|
+
function skillRefs(skills, opts = {}) {
|
|
245
|
+
const filtered = opts.tier ? skills.filter((s) => s.tier === opts.tier) : skills;
|
|
246
|
+
return [...filtered].sort((a, b) => a.id.localeCompare(b.id)).map((s) => inlineResource(s.id, s.skillMd));
|
|
247
|
+
}
|
|
248
|
+
function renderInlineSkillBody(skill) {
|
|
249
|
+
const { body } = parseSkillFrontmatter(skill.skillMd);
|
|
250
|
+
return `### ${skill.name}
|
|
251
|
+
|
|
252
|
+
${body.trim()}`;
|
|
253
|
+
}
|
|
254
|
+
function renderInlineSkills(input) {
|
|
255
|
+
const heading = input.heading ?? "## Skills";
|
|
256
|
+
const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills;
|
|
257
|
+
if (filtered.length === 0) return "";
|
|
258
|
+
const renderBody = input.body ?? renderInlineSkillBody;
|
|
259
|
+
return `
|
|
260
|
+
|
|
261
|
+
${heading}
|
|
262
|
+
|
|
263
|
+
${filtered.map(renderBody).join("\n\n")}`;
|
|
264
|
+
}
|
|
265
|
+
function renderSkillIndex(input) {
|
|
266
|
+
const heading = input.heading ?? "## Skills";
|
|
267
|
+
const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills;
|
|
268
|
+
if (filtered.length === 0) return "";
|
|
269
|
+
const lines = filtered.map(
|
|
270
|
+
(s) => `- ${s.name}: ${s.description} (read ${input.skillDir}/${s.id}/SKILL.md)`
|
|
271
|
+
);
|
|
272
|
+
return `
|
|
273
|
+
|
|
274
|
+
${heading}
|
|
275
|
+
|
|
276
|
+
${lines.join("\n")}`;
|
|
277
|
+
}
|
|
278
|
+
function composeSkills(input) {
|
|
279
|
+
const { skills, mode, tier, heading } = input;
|
|
280
|
+
const deliveredIds = (tier ? skills.filter((s) => s.tier === tier) : skills).map((s) => s.id);
|
|
281
|
+
if (mode === "inline") {
|
|
282
|
+
return {
|
|
283
|
+
refs: [],
|
|
284
|
+
promptSection: renderInlineSkills({ skills, tier, heading }),
|
|
285
|
+
inlineIds: deliveredIds,
|
|
286
|
+
mountedIds: []
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!input.skillDir) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
'composeSkills: mode "mounted" requires a non-null skillDir \u2014 this harness cannot receive skill files at a cwd path; pass mode "inline" instead'
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
refs: skillRefs(skills, { tier }),
|
|
296
|
+
promptSection: renderSkillIndex({ skills, skillDir: input.skillDir, tier, heading }),
|
|
297
|
+
inlineIds: [],
|
|
298
|
+
mountedIds: deliveredIds
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function assertSkillDeliveryDisjoint(inlineIds, mountedIds) {
|
|
302
|
+
const inline = new Set(inlineIds);
|
|
303
|
+
const overlap = [...new Set(mountedIds)].filter((id) => inline.has(id));
|
|
304
|
+
if (overlap.length > 0) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`assertSkillDeliveryDisjoint: skill id(s) delivered both inline and mounted: ${overlap.join(", ")}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function mergeComposedSkills(batches) {
|
|
311
|
+
const merged = {
|
|
312
|
+
refs: batches.flatMap((b) => b.refs),
|
|
313
|
+
promptSection: batches.map((b) => b.promptSection).join(""),
|
|
314
|
+
inlineIds: batches.flatMap((b) => b.inlineIds),
|
|
315
|
+
mountedIds: batches.flatMap((b) => b.mountedIds)
|
|
316
|
+
};
|
|
317
|
+
assertSkillDeliveryDisjoint(merged.inlineIds, merged.mountedIds);
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export {
|
|
322
|
+
skillMountPath,
|
|
323
|
+
loadMarkdownCorpus,
|
|
324
|
+
corpusSkills,
|
|
325
|
+
registrySkills,
|
|
326
|
+
composeShellResources,
|
|
327
|
+
parseSkillFrontmatter,
|
|
328
|
+
skillEntryFromMarkdown,
|
|
329
|
+
parseCorpusSkills,
|
|
330
|
+
skillRefs,
|
|
331
|
+
renderInlineSkills,
|
|
332
|
+
renderSkillIndex,
|
|
333
|
+
composeSkills,
|
|
334
|
+
assertSkillDeliveryDisjoint,
|
|
335
|
+
mergeComposedSkills
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=chunk-NBSBRZ6F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 * A THIRD surface lives alongside those two: adoptable `SkillEntry`s sourced\n * from `SKILL.md` frontmatter rather than hand-authored fields.\n * `parseSkillFrontmatter` is the ONE frontmatter parser (hand-rolled, no YAML\n * dependency — fail loud on a malformed block rather than silently mis-reading\n * a field); `skillEntryFromMarkdown`/`parseCorpusSkills` turn raw markdown into\n * `SkillEntry`s. From there a skill reaches the agent by one of two DELIVERY\n * MODES: `inline` renders the skill body straight into the system prompt (every\n * harness can read it, at prompt-byte cost), or `mounted` projects it onto the\n * typed `resources.skills` channel (`AgentProfileResourceRef[]`) plus an index\n * section that just names the file, and lets the platform materializer place it\n * at the harness-native skill dir. `composeSkills` builds either shape;\n * `mergeComposedSkills` combines batches and throws (via\n * `assertSkillDeliveryDisjoint`) on a skill accidentally delivered both\n * ways. Picking WHICH harnesses can take `mounted` delivery is platform-bound\n * (see `@tangle-network/agent-app/skills-placement`) and deliberately kept out\n * of this substrate-free module.\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[]`/`AgentProfileResourceRef[]`,\n * the exact shapes `resources.files`/`resources.skills` consume. Node builtins\n * are resolved lazily via `process.getBuiltinModule` so a static `node:*`\n * import never reaches the 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 backend reads natively. The\n * registry mounts here; the corpus mounts at its relative path.\n *\n * @deprecated Hardcodes the claude-code path (`~/.claude/skills/<id>/SKILL.md`).\n * It is NOT a path other harnesses read — OpenCode discovers `.opencode/skills`,\n * not `~/.claude/skills` (the doc comment here previously claimed otherwise);\n * codex, kimi-code, and the rest each have their own dir or none at all. Use\n * {@link skillRefs} to put a skill on the typed `resources.skills` channel and\n * let the platform materializer place it correctly, or resolve the\n * harness-native dir directly via `@tangle-network/agent-app/skills-placement`.\n * Kept only for the pre-existing `registrySkills`/`userSkillMounts` callers;\n * do not add new call sites. */\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\n// ─── Adoptable skills: one frontmatter parser, two delivery modes ─────────────\n\n/** Fields a `SKILL.md` frontmatter block may declare. All optional — absent\n * frontmatter (or an absent field within it) is legal; callers fill defaults\n * (see {@link skillEntryFromMarkdown}). */\nexport interface SkillFrontmatter {\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 tier?: string\n}\n\n/** The result of {@link parseSkillFrontmatter}: the parsed fields, the body\n * with the frontmatter block stripped, and the original untouched text. */\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\n/** Quoted values (`description: \"...\"`, as emitted by materialize's\n * `normalizeSkillMd`) are JSON strings — decode with `JSON.parse` so escapes\n * round-trip; anything else is a bare scalar, trimmed. */\nfunction parseFrontmatterScalar(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('\"')) return JSON.parse(trimmed) as string\n return trimmed\n}\n\n/** THE one `SKILL.md` frontmatter parser — hand-rolled, no YAML dependency.\n *\n * Absent frontmatter (text does not open with a `---` delimiter line) is\n * legal: returns `{frontmatter: {}, body: raw, raw}`. An OPENED block with no\n * closing `---` is truncated input and throws. Inside the block: scalar\n * `key: value` lines (value optionally double-quoted, decoded via\n * `JSON.parse`); a nested `author:` block whose indented `name:`/`url:` lines\n * are the only children it accepts; `tags:` as an inline `[a, b]` list or as\n * an indented `- item` block. Unknown scalar keys are ignored (forward-compat)\n * — but a line that matches NONE of these shapes (no colon, an orphaned\n * indented line, a bad dash) throws naming the offending line. Silently\n * mis-parsed metadata is the bug class this parser exists to kill; an\n * unrecognized shape is never guessed at.\n */\nexport function parseSkillFrontmatter(raw: string): ParsedSkill {\n const lines = raw.split('\\n')\n if ((lines[0] ?? '').trim() !== '---') {\n return { frontmatter: {}, body: raw, raw }\n }\n\n let closeIndex = -1\n for (let i = 1; i < lines.length; i++) {\n if ((lines[i] ?? '').trim() === '---') {\n closeIndex = i\n break\n }\n }\n if (closeIndex === -1) {\n throw new Error(\n 'parseSkillFrontmatter: opening \"---\" has no closing \"---\" — truncated frontmatter block',\n )\n }\n\n const blockLines = lines.slice(1, closeIndex)\n const body = lines.slice(closeIndex + 1).join('\\n').replace(/^\\n+/, '')\n const frontmatter: SkillFrontmatter = {}\n const scalarKeys = new Set(['id', 'name', 'description', 'source', 'category', 'tier'])\n const topLineRe = /^(\\S[^:]*):\\s*(.*)$/\n\n let i = 0\n while (i < blockLines.length) {\n const line = blockLines[i] ?? ''\n if (line.trim() === '') {\n i++\n continue\n }\n const match = line.match(topLineRe)\n if (!match) {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const key = (match[1] ?? '').trim()\n const rest = match[2] ?? ''\n\n if (key === 'author') {\n if (rest.trim() !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const author: { name: string; url?: string } = { name: '' }\n let sawName = false\n i++\n while (i < blockLines.length && /^\\s+\\S/.test(blockLines[i] ?? '')) {\n const sub = (blockLines[i] ?? '').trim()\n const subMatch = sub.match(/^(name|url):\\s*(.*)$/)\n if (!subMatch) {\n throw new Error(\n `parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(blockLines[i])}`,\n )\n }\n const subKey = subMatch[1] as 'name' | 'url'\n const subValue = parseFrontmatterScalar(subMatch[2] ?? '')\n if (subKey === 'name') {\n author.name = subValue\n sawName = true\n } else {\n author.url = subValue\n }\n i++\n }\n if (sawName) frontmatter.author = author\n continue\n }\n\n if (key === 'tags') {\n const inline = rest.trim()\n if (inline.startsWith('[') && inline.endsWith(']')) {\n const inner = inline.slice(1, -1).trim()\n frontmatter.tags = inner === '' ? [] : inner.split(',').map((t) => parseFrontmatterScalar(t))\n i++\n continue\n }\n if (inline !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const tags: string[] = []\n i++\n while (i < blockLines.length && /^\\s*-\\s*/.test(blockLines[i] ?? '')) {\n tags.push(parseFrontmatterScalar((blockLines[i] ?? '').replace(/^\\s*-\\s*/, '')))\n i++\n }\n frontmatter.tags = tags\n continue\n }\n\n if (scalarKeys.has(key)) {\n ;(frontmatter as Record<string, string>)[key] = parseFrontmatterScalar(rest)\n }\n // Unknown scalar key: ignored, forward-compat.\n i++\n }\n\n return { frontmatter, body, raw }\n}\n\n/** Build a {@link SkillEntry} from a raw `SKILL.md` body. `id` comes from\n * frontmatter, falling back to `fallbackId` (typically the corpus entry's\n * slug/filename); neither present throws. `name` defaults to `id`,\n * `description` to `''`, `tier` to `'free'`. `skillMd` is always the\n * untouched `raw` input — the full file, frontmatter included, is what a\n * `mounted` delivery writes to disk and what `renderInlineSkills` strips per\n * render. */\nexport function skillEntryFromMarkdown(raw: string, fallbackId?: string): SkillEntry {\n const { frontmatter } = parseSkillFrontmatter(raw)\n const id = frontmatter.id ?? fallbackId\n if (!id) {\n throw new Error(\n 'skillEntryFromMarkdown: no \"id\" in frontmatter and no fallbackId supplied — cannot build a SkillEntry',\n )\n }\n const entry: SkillEntry = {\n id,\n name: frontmatter.name ?? id,\n description: frontmatter.description ?? '',\n tier: frontmatter.tier ?? 'free',\n skillMd: raw,\n }\n if (frontmatter.author) entry.author = frontmatter.author\n if (frontmatter.source) entry.source = frontmatter.source\n if (frontmatter.category) entry.category = frontmatter.category\n if (frontmatter.tags) entry.tags = frontmatter.tags\n return entry\n}\n\n/** Map a loaded corpus (see {@link loadMarkdownCorpus}) onto `SkillEntry`s,\n * using each entry's `id` as the fallback when its `SKILL.md` carries no\n * frontmatter `id` of its own. */\nexport function parseCorpusSkills(corpus: CorpusEntry[]): SkillEntry[] {\n return corpus.map((entry) => skillEntryFromMarkdown(entry.content, entry.id))\n}\n\n/** Project skills onto the typed `resources.skills` channel\n * (`AgentProfileResourceRef[]`), tier-filtered (same `s.tier === tier`\n * semantics as {@link registrySkills}) when `opts.tier` is given, sorted by\n * id for determinism. `ref.name` MUST be the skill id: the platform\n * materializer writes each ref to `${skillDir}/${name}/SKILL.md`. */\nexport function skillRefs(\n skills: SkillEntry[],\n opts: { tier?: string } = {},\n): AgentProfileResourceRef[] {\n const filtered = opts.tier ? skills.filter((s) => s.tier === opts.tier) : skills\n return [...filtered]\n .sort((a, b) => a.id.localeCompare(b.id))\n .map((s) => inlineResource(s.id, s.skillMd))\n}\n\n/** Strip the frontmatter block from a skill's `skillMd` and render it as a\n * prompt subsection. The default `body` renderer {@link renderInlineSkills}\n * passes to itself. */\nfunction renderInlineSkillBody(skill: SkillEntry): string {\n const { body } = parseSkillFrontmatter(skill.skillMd)\n return `### ${skill.name}\\n\\n${body.trim()}`\n}\n\n/** Inputs to {@link renderInlineSkills}. */\nexport interface RenderInlineSkillsInput {\n skills: SkillEntry[]\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n /** Per-skill body renderer. Default strips frontmatter and emits\n * `### <name>\\n\\n<body>`. */\n body?: (skill: SkillEntry) => string\n}\n\n/**\n * Render every (tier-filtered) skill's full body inline into the prompt — the\n * `inline` delivery mode. Returns `''` when no skill survives the filter, else\n * a section starting `\\n\\n<heading>\\n\\n` with each skill's body joined by\n * `\\n\\n` — the same \"already carries its own leading `\\n\\n`\" shape\n * `assembleSystemPrompt` expects from every section it concatenates.\n */\nexport function renderInlineSkills(input: RenderInlineSkillsInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const renderBody = input.body ?? renderInlineSkillBody\n return `\\n\\n${heading}\\n\\n${filtered.map(renderBody).join('\\n\\n')}`\n}\n\n/** Inputs to {@link renderSkillIndex}. */\nexport interface RenderSkillIndexInput {\n skills: SkillEntry[]\n /** cwd-relative directory the skills are mounted under (e.g.\n * `.opencode/skills`) — named in each index line so the agent knows where\n * to read the full `SKILL.md`. */\n skillDir: string\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n}\n\n/**\n * Render a one-line-per-skill INDEX (name, description, and the path to read\n * the full body) — the `mounted` delivery mode's prompt section, paired with\n * {@link skillRefs} putting the actual files on `resources.skills`. Same\n * empty/section shape as {@link renderInlineSkills}.\n */\nexport function renderSkillIndex(input: RenderSkillIndexInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const lines = filtered.map(\n (s) => `- ${s.name}: ${s.description} (read ${input.skillDir}/${s.id}/SKILL.md)`,\n )\n return `\\n\\n${heading}\\n\\n${lines.join('\\n')}`\n}\n\n/** How a skill reaches the agent: `inline` renders its full body into the\n * system prompt; `mounted` puts it on the typed `resources.skills` channel\n * and renders only an index line. */\nexport type SkillDeliveryMode = 'inline' | 'mounted'\n\n/** The output of {@link composeSkills}: the refs to attach to\n * `resources.skills` (empty for `inline`) and the prompt section to fold into\n * the system prompt (already carries its own leading `\\n\\n`, or `''`).\n * `inlineIds`/`mountedIds` record which (tier-filtered) skills were delivered\n * in each mode so {@link mergeComposedSkills} can enforce the modes stay\n * disjoint when batches are combined. */\nexport interface ComposedSkills {\n refs: AgentProfileResourceRef[]\n promptSection: string\n inlineIds: string[]\n mountedIds: string[]\n}\n\n/** Inputs to {@link composeSkills}. */\nexport interface ComposeSkillsInput {\n skills: SkillEntry[]\n mode: SkillDeliveryMode\n tier?: string\n heading?: string\n /** REQUIRED (non-null) for `mode: 'mounted'` — the cwd-relative skill dir\n * the harness reads. Resolve it with `@tangle-network/agent-app/skills-placement`\n * rather than hardcoding it; omitted or `null` throws. */\n skillDir?: string | null\n}\n\n/**\n * Build the {@link ComposedSkills} for one delivery mode. `inline` never\n * touches `resources.skills` — `refs` is always `[]`. `mounted` requires a\n * non-null `skillDir` (the harness must have a native skill-discovery\n * directory); a null/absent one throws rather than silently falling back, so\n * the caller resolves the fallback deliberately (see\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,\n * which does exactly that).\n */\nexport function composeSkills(input: ComposeSkillsInput): ComposedSkills {\n const { skills, mode, tier, heading } = input\n const deliveredIds = (tier ? skills.filter((s) => s.tier === tier) : skills).map((s) => s.id)\n if (mode === 'inline') {\n return {\n refs: [],\n promptSection: renderInlineSkills({ skills, tier, heading }),\n inlineIds: deliveredIds,\n mountedIds: [],\n }\n }\n if (!input.skillDir) {\n throw new Error(\n 'composeSkills: mode \"mounted\" requires a non-null skillDir — this harness cannot receive ' +\n 'skill files at a cwd path; pass mode \"inline\" instead',\n )\n }\n return {\n refs: skillRefs(skills, { tier }),\n promptSection: renderSkillIndex({ skills, skillDir: input.skillDir, tier, heading }),\n inlineIds: [],\n mountedIds: deliveredIds,\n }\n}\n\n/** Throw when the same skill id is delivered both `inline` and `mounted` —\n * the agent would see it twice (once in the prompt body, once as a mounted\n * file it's told to go read), doubling prompt bytes and inviting drift\n * between the two copies. Lists every offending id in the message. */\nexport function assertSkillDeliveryDisjoint(\n inlineIds: Iterable<string>,\n mountedIds: Iterable<string>,\n): void {\n const inline = new Set(inlineIds)\n const overlap = [...new Set(mountedIds)].filter((id) => inline.has(id))\n if (overlap.length > 0) {\n throw new Error(\n `assertSkillDeliveryDisjoint: skill id(s) delivered both inline and mounted: ${overlap.join(', ')}`,\n )\n }\n}\n\n/**\n * Combine multiple {@link ComposedSkills} batches (e.g. a built-in corpus and\n * an installed catalog) into one, concatenating refs and prompt sections in\n * batch order. This is the seam where inline and mounted deliveries can first\n * collide, so it applies {@link assertSkillDeliveryDisjoint} — a skill id\n * delivered inline by one batch and mounted by another throws instead of\n * reaching the agent twice. Merge through this rather than spreading batch\n * fields by hand.\n */\nexport function mergeComposedSkills(batches: ComposedSkills[]): ComposedSkills {\n const merged: ComposedSkills = {\n refs: batches.flatMap((b) => b.refs),\n promptSection: batches.map((b) => b.promptSection).join(''),\n inlineIds: batches.flatMap((b) => b.inlineIds),\n mountedIds: batches.flatMap((b) => b.mountedIds),\n }\n assertSkillDeliveryDisjoint(merged.inlineIds, merged.mountedIds)\n return merged\n}\n"],"mappings":"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","names":["i"]}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
snapHarnessToModel,
|
|
20
20
|
snapModelToHarness
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-MCJSS6SM.js";
|
|
22
22
|
|
|
23
23
|
// src/web-react/index.tsx
|
|
24
24
|
import { useEffect as useEffect8, useMemo as useMemo7, useRef as useRef8, useState as useState12, memo } from "react";
|
|
@@ -3394,4 +3394,4 @@ export {
|
|
|
3394
3394
|
useThinkingSeconds,
|
|
3395
3395
|
ChatMessages
|
|
3396
3396
|
};
|
|
3397
|
-
//# sourceMappingURL=chunk-
|
|
3397
|
+
//# sourceMappingURL=chunk-O6H2WD3I.js.map
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { modelProvider } from '@tangle-network/agent-interface';
|
|
|
23
23
|
|
|
24
24
|
/** The known coding-agent backends. Mirrors `@tangle-network/sandbox`'s
|
|
25
25
|
* `BackendType`; kept structural so this module needs no sandbox dependency. */
|
|
26
|
-
declare const KNOWN_HARNESSES: readonly ["opencode", "claude-code", "kimi-code", "codex", "amp", "factory-droids", "pi", "hermes", "forge", "openclaw", "acp", "cursor", "cli-base"];
|
|
26
|
+
declare const KNOWN_HARNESSES: readonly ["opencode", "claude-code", "nanoclaw", "kimi-code", "codex", "amp", "factory-droids", "pi", "hermes", "forge", "openclaw", "acp", "cursor", "cli-base"];
|
|
27
27
|
type Harness = (typeof KNOWN_HARNESSES)[number];
|
|
28
28
|
declare const DEFAULT_HARNESS: Harness;
|
|
29
29
|
declare function isHarness(value: unknown): value is Harness;
|