dna-sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,9 @@
10
10
  * file:// <path> → FilesystemSource (read/write on disk)
11
11
  * fs:// <path> → alias of file://
12
12
  * <plain path> → treated as file://<path>
13
+ * pkg://<pkg>[/sub] → FilesystemSource (READ-ONLY) over a scope embedded
14
+ * as PACKAGE DATA of <pkg> (sub defaults to .dna);
15
+ * travels with the app (tarball / Docker)
13
16
  * postgresql:// … → PostgresSource (node-postgres)
14
17
  * postgres:// … → alias of postgresql://
15
18
  * sqlite:// <path> → NOT SUPPORTED in the TS runtime (Python-only; the
@@ -16,6 +16,22 @@ function schemeOf(url) {
16
16
  const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
17
17
  return (m ? m[1] : "file").toLowerCase();
18
18
  }
19
+ /**
20
+ * Split `pkg://<package>[/<subpath>]` into `{ pkg, subpath }`. Parity with the
21
+ * python `_parse_pkg_url`: `pkg://app` → `{pkg:"app", subpath:""}`;
22
+ * `pkg://app/.dna` → `{pkg:"app", subpath:".dna"}`. The package is the netloc
23
+ * (a dotted `pkg://my.app` stays `my.app`).
24
+ */
25
+ function parsePkgUrl(url) {
26
+ const m = /^pkg:\/\/([^/]*)(?:\/(.*))?$/.exec(url);
27
+ const pkg = m?.[1] ?? "";
28
+ if (!pkg) {
29
+ throw new UnsupportedSourceScheme(`pkg:// source URL is missing a package name — use ` +
30
+ `pkg://<package>[/<subpath>] (e.g. pkg://app or pkg://app/.dna). ` +
31
+ `Got: ${url}`);
32
+ }
33
+ return { pkg, subpath: m?.[2] ?? "" };
34
+ }
19
35
  /**
20
36
  * Build a source from a scheme URL (see module docstring).
21
37
  *
@@ -29,6 +45,16 @@ export async function sourceFromUrl(url, opts = {}) {
29
45
  const { FilesystemSource } = await import("./filesystem/source.js");
30
46
  return new FilesystemSource(urlToFsPath(url) || url);
31
47
  }
48
+ if (scheme === "pkg") {
49
+ // A scope embedded as PACKAGE DATA — resolve it from inside the installed
50
+ // package so it travels with the app (tarball / Docker), no path
51
+ // navigation and no manual copy. READ-ONLY: FilesystemSource has no write
52
+ // surface here (to write, use file:// or postgresql://).
53
+ const { FilesystemSource } = await import("./filesystem/source.js");
54
+ const { anchorScopesRoot, DEFAULT_SUBPATH } = await import("../package-scope.js");
55
+ const { pkg, subpath } = parsePkgUrl(url);
56
+ return new FilesystemSource(anchorScopesRoot(pkg, subpath || DEFAULT_SUBPATH));
57
+ }
32
58
  if (base === "postgresql" || base === "postgres") {
33
59
  const { PostgresSource } = await import("./postgres/source.js");
34
60
  const src = new PostgresSource({ connectionString: url, schema: opts.schema });
@@ -41,7 +67,8 @@ export async function sourceFromUrl(url, opts = {}) {
41
67
  `(filesystem) or postgresql:// here. Got: ${url}`);
42
68
  }
43
69
  throw new UnsupportedSourceScheme(`unsupported source URL scheme '${scheme}://' — the TS runtime ships ` +
44
- `file:// (filesystem) and postgresql:// adapters. Got: ${url}`);
70
+ `file:// (filesystem), pkg:// (read-only package-data scope) and ` +
71
+ `postgresql:// adapters. Got: ${url}`);
45
72
  }
46
73
  /**
47
74
  * The `file://` URL the SDK falls back to with no explicit config.
@@ -0,0 +1,240 @@
1
+ # Tool — a declarative, invocable capability an agent can call (record plane).
2
+ #
3
+ # "Tools as data" (f-dna-tools-as-data). The DNA already governs persona,
4
+ # instruction and guardrails declaratively; a Tool moves the AGENT-FACING
5
+ # surface of a tool into the same declarative plane — the ``description`` the
6
+ # model reads to decide whether to call it (``metadata.description``) and the
7
+ # ``parameters`` JSON Schema of its arguments (``spec.input_schema``). One
8
+ # source of truth, versioned, testable, and overridable per tenant, served
9
+ # identically to a Python backend (`@tool`) and a TypeScript frontend
10
+ # (CopilotKit `useCopilotAction`) via ``dna.load_tools`` / ``loadTools``.
11
+ #
12
+ # F3 migration (s-tool-kind-descriptor): this WAS a hand-written ``ToolKind``
13
+ # class (helix/__init__.py + helix.ts) with a ``TypedTool``/``ToolSpec``
14
+ # model. It is now a ``kinds/tool.kind.yaml`` descriptor — a record Kind
15
+ # expressed as data, per the repo's own ratchet (record Kinds are
16
+ # descriptors, never classes). The invocation surface (type/endpoint/mcp/
17
+ # python/shell + input/output schema + auth + read_only/requires_confirmation)
18
+ # is preserved verbatim; only the IMPLEMENTATION moved class → descriptor and
19
+ # the plane moved composition → record (a Tool is not a prompt target and
20
+ # never composes into an agent prompt, so it carries no composition signal —
21
+ # writing a Tool no longer invalidates the composition schema cache).
22
+ #
23
+ # Agents reference Tools by name via ``dep_filters.tools`` (unchanged — the
24
+ # alias ``helix-tool`` is stable). Stored as ``tools/<name>.yaml`` —
25
+ # marketplace-shareable as standalone bundles.
26
+ #
27
+ # tenant_scope intentionally NOT declared — permissive (base Tool + optional
28
+ # per-tenant overlay). A tenant may legitimately override a tool's
29
+ # description/parameters (the SaaS value hook); the base stays intact and the
30
+ # máxima "inheritable ⇒ never tenanted" holds.
31
+ #
32
+ # PARITY-CRITICAL package data: byte-identical mirror under both runtimes'
33
+ # helix/kinds/ (tests/test_descriptor_hash_parity.py enforces).
34
+ apiVersion: github.com/ruinosus/dna/core/v1
35
+ kind: KindDefinition
36
+ metadata:
37
+ name: tool
38
+ spec:
39
+ target_api_version: github.com/ruinosus/dna/v1
40
+ target_kind: Tool
41
+ alias: helix-tool
42
+ origin: github.com/ruinosus/dna/tool
43
+ plane: record
44
+ prompt_target: false
45
+ flatten_in_context: false
46
+ prompt_target_priority: 0
47
+ storage:
48
+ type: yaml
49
+ container: tools
50
+ schema:
51
+ type: object
52
+ properties:
53
+ type:
54
+ type: string
55
+ enum:
56
+ - http
57
+ - mcp
58
+ - python
59
+ - shell
60
+ - builtin
61
+ description: How the tool is executed. builtin | http | mcp | python
62
+ | shell.
63
+ endpoint:
64
+ type: string
65
+ description: URL called when type=http. Supports {placeholder}
66
+ templating.
67
+ method:
68
+ type: string
69
+ description: HTTP method when type=http (default POST).
70
+ mcp_server:
71
+ type: string
72
+ description: MCP server name when type=mcp.
73
+ mcp_tool:
74
+ type: string
75
+ description: Tool name on the MCP server when type=mcp.
76
+ python_module:
77
+ type: string
78
+ description: Dotted import path when type=python.
79
+ python_callable:
80
+ type: string
81
+ description: Attribute on the module (function or class) when
82
+ type=python.
83
+ shell_command:
84
+ type: string
85
+ description: Command template when type=shell. Never executed without
86
+ confirmation.
87
+ input_schema:
88
+ type: object
89
+ description: JSON Schema of the arguments the agent passes when
90
+ invoking the tool — the "parameters" the model fills in. Surfaced
91
+ as ``parameters`` by ``dna.load_tools`` / ``loadTools``.
92
+ output_schema:
93
+ type: object
94
+ description: JSON Schema describing the shape of the tool's response.
95
+ auth_type:
96
+ type: string
97
+ enum:
98
+ - none
99
+ - api_key
100
+ - bearer
101
+ - oauth2
102
+ description: Credential strategy for the invocation.
103
+ auth_env_var:
104
+ type: string
105
+ description: Environment variable holding the credential (e.g.
106
+ GITHUB_TOKEN).
107
+ read_only:
108
+ type: boolean
109
+ description: False = the tool may mutate state (DB writes, file
110
+ changes, external side effects).
111
+ requires_confirmation:
112
+ type: boolean
113
+ description: Force user approval before each invocation.
114
+ tags:
115
+ type: array
116
+ items:
117
+ type: string
118
+ description: Free-form labels for filtering and search.
119
+ examples:
120
+ type: array
121
+ items:
122
+ type: object
123
+ description: Usage examples ([{input, output}]).
124
+ ui:
125
+ mode: build
126
+ label:
127
+ en: Tools
128
+ pt-BR: Ferramentas
129
+ description:
130
+ en: '@tool functions available to agents.'
131
+ pt-BR: Ferramentas (@tool) disponíveis aos agentes.
132
+ routes:
133
+ list: docs/Tool
134
+ detail: docs/Tool/:name
135
+ permissions:
136
+ list: any
137
+ detail: any
138
+ in_sidebar: true
139
+ display_order: 53
140
+ ui_schema:
141
+ type:
142
+ widget: select
143
+ label: Invocation type
144
+ help: 'How the tool is executed: http | mcp | python | shell | builtin.'
145
+ order: 10
146
+ endpoint:
147
+ widget: text
148
+ label: HTTP endpoint
149
+ help: URL called when type=http. Supports {placeholder} templating.
150
+ order: 20
151
+ method:
152
+ widget: select
153
+ label: HTTP method
154
+ order: 25
155
+ mcp_server:
156
+ widget: text
157
+ label: MCP server
158
+ help: Server name when type=mcp.
159
+ order: 30
160
+ mcp_tool:
161
+ widget: text
162
+ label: MCP tool name
163
+ order: 35
164
+ python_module:
165
+ widget: text
166
+ label: Python module
167
+ help: Dotted import path when type=python.
168
+ order: 40
169
+ python_callable:
170
+ widget: text
171
+ label: Python callable
172
+ help: Attribute on the module (function or class).
173
+ order: 45
174
+ shell_command:
175
+ widget: textarea
176
+ label: Shell command
177
+ help: Command template when type=shell. Never executed without
178
+ confirmation.
179
+ order: 50
180
+ input_schema:
181
+ widget: code
182
+ language: yaml
183
+ label: Input schema (JSON Schema)
184
+ help: Validates the arguments the agent passes when invoking the tool.
185
+ height: 260
186
+ order: 60
187
+ output_schema:
188
+ widget: code
189
+ language: yaml
190
+ label: Output schema (JSON Schema)
191
+ help: Describes the shape of the tool's response.
192
+ height: 220
193
+ order: 70
194
+ auth_type:
195
+ widget: select
196
+ label: Auth type
197
+ help: none | api_key | bearer | oauth2.
198
+ order: 80
199
+ auth_env_var:
200
+ widget: text
201
+ label: Auth env var
202
+ help: Environment variable holding the credential.
203
+ order: 85
204
+ read_only:
205
+ widget: checkbox
206
+ label: Read-only
207
+ help: Uncheck if the tool mutates state (database writes, file changes,
208
+ external side effects).
209
+ order: 90
210
+ requires_confirmation:
211
+ widget: checkbox
212
+ label: Requires confirmation
213
+ help: Force user approval before each invocation.
214
+ order: 95
215
+ tags:
216
+ widget: tags
217
+ label: Tags
218
+ order: 100
219
+ examples:
220
+ widget: readonly
221
+ label: Examples
222
+ help: Usage examples. Nested list — edit via YAML for now.
223
+ order: 110
224
+ graph_style:
225
+ fill: '#14B8A6'
226
+ stroke: '#0D9488'
227
+ text_color: '#fff'
228
+ ascii_icon: 🔧
229
+ display_label: Tools
230
+ docs: A Tool is a declarative, invocable capability an agent can call — an
231
+ HTTP endpoint, an MCP server tool, a Python callable, a shell command, or
232
+ a builtin. It bridges DNA with OpenAI/Anthropic tool-calling conventions.
233
+ The agent-facing surface is its ``metadata.description`` (the text the
234
+ model reads to decide to call it) and its ``spec.input_schema`` (the
235
+ "parameters" JSON Schema of the arguments); ``dna.load_tools`` /
236
+ ``loadTools`` serve exactly that surface, identically to Python and
237
+ TypeScript consumers from this one source. It also declares an auth
238
+ strategy and read_only / requires_confirmation flags the host honors at
239
+ runtime. Agents reference Tools via ``dep_filters.tools``. Stored as
240
+ ``tools/<name>.yaml`` — marketplace-shareable as standalone bundles.
@@ -7,7 +7,8 @@ import yaml from "js-yaml";
7
7
  import { join as pathJoin } from "node:path";
8
8
  import { nodeFS, readTextSafe, collectDir } from "../kernel/fs.js";
9
9
  import { KindBase } from "../kernel/kind_base.js";
10
- import { AgentSchema, ActorSchema, UseCaseSchema, ToolSchema, AgentSpecSchema, ActorSpecSchema, UseCaseSpecSchema, ToolSpecSchema, GenomeSchema, GenomeSpecSchema, LayerPolicySchema, LayerPolicySpecSchema, zodSpecToJsonSchema } from "../kernel/models.js";
10
+ import { AgentSchema, ActorSchema, UseCaseSchema, AgentSpecSchema, ActorSpecSchema, UseCaseSpecSchema, GenomeSchema, GenomeSpecSchema, LayerPolicySchema, LayerPolicySpecSchema, zodSpecToJsonSchema } from "../kernel/models.js";
11
+ import { loadDescriptors } from "../kernel/descriptor-loader.js";
11
12
  import { SD } from "../kernel/protocols.js";
12
13
  import { readSpecString, readSpecStringArray } from "../kernel/spec-access.js";
13
14
  import { SettingKind, ThemeKind, UserProfileKind, CanvasKind } from "./helix_extras.js";
@@ -529,102 +530,6 @@ class UseCaseKind extends KindBase {
529
530
  }
530
531
  }
531
532
  // ---------------------------------------------------------------------------
532
- // ToolKind
533
- // ---------------------------------------------------------------------------
534
- class ToolKind extends KindBase {
535
- apiVersion = "github.com/ruinosus/dna/v1";
536
- kind = "Tool";
537
- alias = "helix-tool";
538
- isSchemaAffecting = true;
539
- origin = "github.com/ruinosus/dna/tool";
540
- isPromptTarget = false;
541
- promptTargetPriority = 0;
542
- flattenInContext = false;
543
- storage = SD.yaml("tools");
544
- graphStyle = { fill: "#14B8A6", stroke: "#0D9488", textColor: "#fff" };
545
- asciiIcon = "🔧";
546
- displayLabel = "Tools";
547
- _sourceUrl = MOD_URL;
548
- docs = "A Tool is a declarative, invocable capability an agent can call: an " +
549
- "HTTP endpoint, an MCP server tool, a Python callable, a shell command, " +
550
- "or a builtin. Bridges helix with OpenAI/Anthropic tool-calling " +
551
- "conventions. Each Tool declares an input/output JSON Schema, an auth " +
552
- "strategy, and read_only / requires_confirmation flags that the harness " +
553
- "honors at runtime. Agents reference Tools via dep_filters.tools. " +
554
- "Stored as tools/<name>.yaml — marketplace-shareable as standalone bundles.";
555
- uiSchema = {
556
- type: { widget: "select", label: "Invocation type", help: "How the tool is executed: http | mcp | python | shell | builtin.", order: 10 },
557
- endpoint: { widget: "text", label: "HTTP endpoint", help: "URL called when type=http. Supports {placeholder} templating.", order: 20 },
558
- method: { widget: "select", label: "HTTP method", order: 25 },
559
- mcp_server: { widget: "text", label: "MCP server", help: "Server name when type=mcp.", order: 30 },
560
- mcp_tool: { widget: "text", label: "MCP tool name", order: 35 },
561
- python_module: { widget: "text", label: "Python module", help: "Dotted import path when type=python.", order: 40 },
562
- python_callable: { widget: "text", label: "Python callable", help: "Attribute on the module (function or class).", order: 45 },
563
- shell_command: { widget: "textarea", label: "Shell command", help: "Command template when type=shell. Never executed without confirmation.", order: 50 },
564
- input_schema: {
565
- widget: "code",
566
- language: "yaml",
567
- label: "Input schema (JSON Schema)",
568
- help: "Validates the arguments the agent passes when invoking the tool.",
569
- height: 260,
570
- order: 60,
571
- },
572
- output_schema: {
573
- widget: "code",
574
- language: "yaml",
575
- label: "Output schema (JSON Schema)",
576
- help: "Describes the shape of the tool's response.",
577
- height: 220,
578
- order: 70,
579
- },
580
- auth_type: { widget: "select", label: "Auth type", help: "none | api_key | bearer | oauth2.", order: 80 },
581
- auth_env_var: { widget: "text", label: "Auth env var", help: "Environment variable holding the credential.", order: 85 },
582
- read_only: { widget: "checkbox", label: "Read-only", help: "Uncheck if the tool mutates state.", order: 90 },
583
- requires_confirmation: { widget: "checkbox", label: "Requires confirmation", help: "Force user approval before each invocation.", order: 95 },
584
- tags: { widget: "tags", label: "Tags", order: 100 },
585
- examples: { widget: "readonly", label: "Examples", help: "Usage examples. Edit via YAML for now.", order: 110 },
586
- };
587
- schema() { return zodSpecToJsonSchema(ToolSpecSchema); }
588
- parse(raw) {
589
- return ToolSchema.parse(raw);
590
- }
591
- summary() { return null; }
592
- preview(doc) {
593
- const spec = (doc.spec ?? {});
594
- const fields = [];
595
- if (typeof spec.type === "string")
596
- fields.push({ label: "type", value: spec.type });
597
- if (typeof spec.endpoint === "string")
598
- fields.push({ label: "endpoint", value: spec.endpoint });
599
- if (typeof spec.method === "string")
600
- fields.push({ label: "method", value: spec.method });
601
- if (typeof spec.mcp_server === "string")
602
- fields.push({ label: "mcp_server", value: spec.mcp_server });
603
- if (typeof spec.mcp_tool === "string")
604
- fields.push({ label: "mcp_tool", value: spec.mcp_tool });
605
- if (typeof spec.python_module === "string")
606
- fields.push({ label: "python_module", value: spec.python_module });
607
- if (typeof spec.python_callable === "string")
608
- fields.push({ label: "python_callable", value: spec.python_callable });
609
- if (typeof spec.shell_command === "string")
610
- fields.push({ label: "shell_command", value: spec.shell_command });
611
- if (spec.input_schema)
612
- fields.push({ label: "input_schema", value: JSON.stringify(spec.input_schema, null, 2) });
613
- if (spec.output_schema)
614
- fields.push({ label: "output_schema", value: JSON.stringify(spec.output_schema, null, 2) });
615
- if (typeof spec.auth_type === "string")
616
- fields.push({ label: "auth", value: spec.auth_type });
617
- if (spec.read_only != null)
618
- fields.push({ label: "read_only", value: String(spec.read_only) });
619
- if (spec.requires_confirmation != null)
620
- fields.push({ label: "requires_confirmation", value: String(spec.requires_confirmation) });
621
- if (fields.length === 0) {
622
- return [{ kind: "empty", title: `Tool ${doc.name}` }];
623
- }
624
- return [{ kind: "fields", title: `Tool ${doc.name}`, fields }];
625
- }
626
- }
627
- // ---------------------------------------------------------------------------
628
533
  // AgentReader / AgentWriter
629
534
  // ---------------------------------------------------------------------------
630
535
  const KNOWN_DIRS = new Set(["scripts", "references", "assets"]);
@@ -985,9 +890,15 @@ export class HelixExtension {
985
890
  kernel.kind(new GenomeKind());
986
891
  kernel.kind(new LayerPolicyKind());
987
892
  kernel.kind(new AgentKind());
988
- kernel.kind(new ToolKind());
989
893
  kernel.kind(new ActorKind());
990
894
  kernel.kind(new UseCaseKind());
895
+ // Tool (helix-tool) ships as a descriptor — helix/kinds/tool.kind.yaml
896
+ // (f-dna-tools-as-data / s-tool-kind-descriptor). It WAS a hand-written
897
+ // ToolKind class; migrated to a record-plane descriptor per the repo's
898
+ // own ratchet (record Kinds are data, not classes).
899
+ for (const raw of loadDescriptors(import.meta.url, "helix/kinds")) {
900
+ kernel.kindFromDescriptor(raw);
901
+ }
991
902
  // 2026-05-26 — absorbed from claude-code-templates catalog (MIT).
992
903
  // Setting rounds out the Claude-Code-customization primitives that
993
904
  // live alongside Skill / UA / Soul / Tool.
package/dist/index.d.ts CHANGED
@@ -22,8 +22,12 @@ export { ReportBuilder } from "./kernel/reports.js";
22
22
  export { serializeRawToFiles } from "./kernel/serialize-to-files.js";
23
23
  export * from "./viz/index.js";
24
24
  export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest, fromConfig } from "./bootstrap.js";
25
- export { AgentNotFound, UnknownLayout } from "./kernel/errors.js";
25
+ export { AgentNotFound, ToolNotFound, UnknownLayout } from "./kernel/errors.js";
26
26
  export { PromptLibrary, loadPrompts } from "./prompts.js";
27
+ export { ToolLibrary, loadTools } from "./tools.js";
28
+ export type { ToolSurface } from "./tools.js";
29
+ export type { LoadPromptsOptions } from "./prompts.js";
30
+ export { anchorScopesRoot, PackageScopeNotFound, DEFAULT_SUBPATH } from "./package-scope.js";
27
31
  export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
28
32
  export type { DnaConfig, SearchMode, EmbeddingMode } from "./config.js";
29
33
  export { sourceFromUrl, resolveDefaultFsUrl, UnsupportedSourceScheme } from "./adapters/source-url.js";
package/dist/index.js CHANGED
@@ -27,8 +27,10 @@ export * from "./viz/index.js";
27
27
  export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest, fromConfig } from "./bootstrap.js";
28
28
  // DX consumer surface (s-dx-*): fail-loud prompt building + the collapse-the-
29
29
  // shim helper + declarative port wiring.
30
- export { AgentNotFound, UnknownLayout } from "./kernel/errors.js";
30
+ export { AgentNotFound, ToolNotFound, UnknownLayout } from "./kernel/errors.js";
31
31
  export { PromptLibrary, loadPrompts } from "./prompts.js";
32
+ export { ToolLibrary, loadTools } from "./tools.js";
33
+ export { anchorScopesRoot, PackageScopeNotFound, DEFAULT_SUBPATH } from "./package-scope.js";
32
34
  export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
33
35
  export { sourceFromUrl, resolveDefaultFsUrl, UnsupportedSourceScheme } from "./adapters/source-url.js";
34
36
  export { HookRegistry, KNOWN_HOOK_NAMES } from "./kernel/hooks.js";
@@ -31,6 +31,24 @@ export declare class AgentNotFound extends Error {
31
31
  readonly agent: string | null;
32
32
  constructor(agent: string | null);
33
33
  }
34
+ /**
35
+ * `loadTools(scope).get(name)` was asked for a Tool that no `Tool` document in
36
+ * the scope declares (missing, renamed, or in another scope).
37
+ *
38
+ * Fail-loud contract (s-load-tools-helper), the twin of `AgentNotFound`: the
39
+ * agent-facing tool surface is data, so a miss must throw a typed error —
40
+ * never return an empty surface that would silently reach a model as a tool
41
+ * with no description.
42
+ *
43
+ * 1:1 parity with python `dna.ToolNotFound` (a `LookupError` subclass).
44
+ * Exported publicly from the package root.
45
+ */
46
+ export declare class ToolNotFound extends Error {
47
+ readonly toolName: string | null;
48
+ readonly scope: string | null;
49
+ readonly available: string[];
50
+ constructor(name: string | null, scope?: string | null, available?: string[]);
51
+ }
34
52
  /**
35
53
  * `buildPrompt` hit an Agent whose `layout:` names a preset the Kind does not
36
54
  * offer (s-dx-named-layouts).
@@ -36,6 +36,33 @@ export class AgentNotFound extends Error {
36
36
  Object.setPrototypeOf(this, new.target.prototype);
37
37
  }
38
38
  }
39
+ /**
40
+ * `loadTools(scope).get(name)` was asked for a Tool that no `Tool` document in
41
+ * the scope declares (missing, renamed, or in another scope).
42
+ *
43
+ * Fail-loud contract (s-load-tools-helper), the twin of `AgentNotFound`: the
44
+ * agent-facing tool surface is data, so a miss must throw a typed error —
45
+ * never return an empty surface that would silently reach a model as a tool
46
+ * with no description.
47
+ *
48
+ * 1:1 parity with python `dna.ToolNotFound` (a `LookupError` subclass).
49
+ * Exported publicly from the package root.
50
+ */
51
+ export class ToolNotFound extends Error {
52
+ toolName;
53
+ scope;
54
+ available;
55
+ constructor(name, scope = null, available = []) {
56
+ const where = scope ? ` in scope '${scope}'` : "";
57
+ const hint = available.length ? ` — available: ${available.join(", ")}` : "";
58
+ super(`Tool '${name}' not found${where}${hint}`);
59
+ this.name = "ToolNotFound";
60
+ this.toolName = name;
61
+ this.scope = scope;
62
+ this.available = available;
63
+ Object.setPrototypeOf(this, new.target.prototype);
64
+ }
65
+ }
39
66
  /**
40
67
  * `buildPrompt` hit an Agent whose `layout:` names a preset the Kind does not
41
68
  * offer (s-dx-named-layouts).
@@ -115,7 +115,9 @@ export function generateAlias(owner, kind) {
115
115
  export const EXPLICIT_ALIAS_ALLOWLIST = new Set([
116
116
  // helix
117
117
  "helix-genome", "helix-agent", "helix-actor",
118
- "helix-usecase", "helix-tool", "policy-layer-policy",
118
+ // helix-tool migrated to a descriptor (s-tool-kind-descriptor): its alias
119
+ // lives in helix/kinds/tool.kind.yaml (parity-critical) — shrink-only.
120
+ "helix-usecase", "policy-layer-policy",
119
121
  "helix-canvas",
120
122
  "helix-setting", "helix-theme", "helix-user-profile",
121
123
  // sdlc (classes; descriptors are outside the ratchet)
@@ -1166,197 +1166,6 @@ export declare const UseCaseSchema: z.ZodObject<{
1166
1166
  } | undefined;
1167
1167
  }>;
1168
1168
  export type TypedUseCase = z.output<typeof UseCaseSchema>;
1169
- export declare const ToolTypeEnum: z.ZodEnum<["http", "mcp", "python", "shell", "builtin"]>;
1170
- export declare const ToolAuthTypeEnum: z.ZodEnum<["none", "api_key", "bearer", "oauth2"]>;
1171
- export declare const ToolSpecSchema: z.ZodObject<{
1172
- type: z.ZodDefault<z.ZodEnum<["http", "mcp", "python", "shell", "builtin"]>>;
1173
- endpoint: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1174
- method: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1175
- mcp_server: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1176
- mcp_tool: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1177
- python_module: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1178
- python_callable: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1179
- shell_command: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1180
- input_schema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1181
- output_schema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1182
- auth_type: z.ZodDefault<z.ZodEnum<["none", "api_key", "bearer", "oauth2"]>>;
1183
- auth_env_var: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1184
- read_only: z.ZodDefault<z.ZodBoolean>;
1185
- requires_confirmation: z.ZodDefault<z.ZodBoolean>;
1186
- tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
1187
- examples: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
1188
- }, "strip", z.ZodTypeAny, {
1189
- type: "http" | "mcp" | "python" | "shell" | "builtin";
1190
- tags: string[];
1191
- input_schema: Record<string, unknown>;
1192
- endpoint: string;
1193
- method: string;
1194
- mcp_server: string;
1195
- mcp_tool: string;
1196
- python_module: string;
1197
- python_callable: string;
1198
- shell_command: string;
1199
- output_schema: Record<string, unknown>;
1200
- auth_type: "none" | "api_key" | "bearer" | "oauth2";
1201
- auth_env_var: string;
1202
- read_only: boolean;
1203
- requires_confirmation: boolean;
1204
- examples: Record<string, unknown>[];
1205
- }, {
1206
- type?: "http" | "mcp" | "python" | "shell" | "builtin" | undefined;
1207
- tags?: string[] | undefined;
1208
- input_schema?: Record<string, unknown> | undefined;
1209
- endpoint?: string | undefined;
1210
- method?: string | undefined;
1211
- mcp_server?: string | undefined;
1212
- mcp_tool?: string | undefined;
1213
- python_module?: string | undefined;
1214
- python_callable?: string | undefined;
1215
- shell_command?: string | undefined;
1216
- output_schema?: Record<string, unknown> | undefined;
1217
- auth_type?: "none" | "api_key" | "bearer" | "oauth2" | undefined;
1218
- auth_env_var?: string | undefined;
1219
- read_only?: boolean | undefined;
1220
- requires_confirmation?: boolean | undefined;
1221
- examples?: Record<string, unknown>[] | undefined;
1222
- }>;
1223
- export declare const ToolSchema: z.ZodObject<{
1224
- apiVersion: z.ZodLiteral<"github.com/ruinosus/dna/v1">;
1225
- kind: z.ZodLiteral<"Tool">;
1226
- metadata: z.ZodObject<{
1227
- name: z.ZodString;
1228
- description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1229
- version: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1230
- icon: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1231
- group: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1232
- labels: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1233
- }, "strip", z.ZodTypeAny, {
1234
- name: string;
1235
- description: string;
1236
- version: string;
1237
- icon: string;
1238
- group: string;
1239
- labels: Record<string, string>;
1240
- }, {
1241
- name: string;
1242
- description?: string | undefined;
1243
- version?: string | undefined;
1244
- icon?: string | undefined;
1245
- group?: string | undefined;
1246
- labels?: Record<string, string> | undefined;
1247
- }>;
1248
- spec: z.ZodDefault<z.ZodObject<{
1249
- type: z.ZodDefault<z.ZodEnum<["http", "mcp", "python", "shell", "builtin"]>>;
1250
- endpoint: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1251
- method: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1252
- mcp_server: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1253
- mcp_tool: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1254
- python_module: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1255
- python_callable: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1256
- shell_command: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1257
- input_schema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1258
- output_schema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1259
- auth_type: z.ZodDefault<z.ZodEnum<["none", "api_key", "bearer", "oauth2"]>>;
1260
- auth_env_var: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1261
- read_only: z.ZodDefault<z.ZodBoolean>;
1262
- requires_confirmation: z.ZodDefault<z.ZodBoolean>;
1263
- tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
1264
- examples: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
1265
- }, "strip", z.ZodTypeAny, {
1266
- type: "http" | "mcp" | "python" | "shell" | "builtin";
1267
- tags: string[];
1268
- input_schema: Record<string, unknown>;
1269
- endpoint: string;
1270
- method: string;
1271
- mcp_server: string;
1272
- mcp_tool: string;
1273
- python_module: string;
1274
- python_callable: string;
1275
- shell_command: string;
1276
- output_schema: Record<string, unknown>;
1277
- auth_type: "none" | "api_key" | "bearer" | "oauth2";
1278
- auth_env_var: string;
1279
- read_only: boolean;
1280
- requires_confirmation: boolean;
1281
- examples: Record<string, unknown>[];
1282
- }, {
1283
- type?: "http" | "mcp" | "python" | "shell" | "builtin" | undefined;
1284
- tags?: string[] | undefined;
1285
- input_schema?: Record<string, unknown> | undefined;
1286
- endpoint?: string | undefined;
1287
- method?: string | undefined;
1288
- mcp_server?: string | undefined;
1289
- mcp_tool?: string | undefined;
1290
- python_module?: string | undefined;
1291
- python_callable?: string | undefined;
1292
- shell_command?: string | undefined;
1293
- output_schema?: Record<string, unknown> | undefined;
1294
- auth_type?: "none" | "api_key" | "bearer" | "oauth2" | undefined;
1295
- auth_env_var?: string | undefined;
1296
- read_only?: boolean | undefined;
1297
- requires_confirmation?: boolean | undefined;
1298
- examples?: Record<string, unknown>[] | undefined;
1299
- }>>;
1300
- }, "strip", z.ZodTypeAny, {
1301
- metadata: {
1302
- name: string;
1303
- description: string;
1304
- version: string;
1305
- icon: string;
1306
- group: string;
1307
- labels: Record<string, string>;
1308
- };
1309
- spec: {
1310
- type: "http" | "mcp" | "python" | "shell" | "builtin";
1311
- tags: string[];
1312
- input_schema: Record<string, unknown>;
1313
- endpoint: string;
1314
- method: string;
1315
- mcp_server: string;
1316
- mcp_tool: string;
1317
- python_module: string;
1318
- python_callable: string;
1319
- shell_command: string;
1320
- output_schema: Record<string, unknown>;
1321
- auth_type: "none" | "api_key" | "bearer" | "oauth2";
1322
- auth_env_var: string;
1323
- read_only: boolean;
1324
- requires_confirmation: boolean;
1325
- examples: Record<string, unknown>[];
1326
- };
1327
- apiVersion: "github.com/ruinosus/dna/v1";
1328
- kind: "Tool";
1329
- }, {
1330
- metadata: {
1331
- name: string;
1332
- description?: string | undefined;
1333
- version?: string | undefined;
1334
- icon?: string | undefined;
1335
- group?: string | undefined;
1336
- labels?: Record<string, string> | undefined;
1337
- };
1338
- apiVersion: "github.com/ruinosus/dna/v1";
1339
- kind: "Tool";
1340
- spec?: {
1341
- type?: "http" | "mcp" | "python" | "shell" | "builtin" | undefined;
1342
- tags?: string[] | undefined;
1343
- input_schema?: Record<string, unknown> | undefined;
1344
- endpoint?: string | undefined;
1345
- method?: string | undefined;
1346
- mcp_server?: string | undefined;
1347
- mcp_tool?: string | undefined;
1348
- python_module?: string | undefined;
1349
- python_callable?: string | undefined;
1350
- shell_command?: string | undefined;
1351
- output_schema?: Record<string, unknown> | undefined;
1352
- auth_type?: "none" | "api_key" | "bearer" | "oauth2" | undefined;
1353
- auth_env_var?: string | undefined;
1354
- read_only?: boolean | undefined;
1355
- requires_confirmation?: boolean | undefined;
1356
- examples?: Record<string, unknown>[] | undefined;
1357
- } | undefined;
1358
- }>;
1359
- export type TypedTool = z.output<typeof ToolSchema>;
1360
1169
  export declare const SkillSpecSchema: z.ZodObject<{
1361
1170
  instruction: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1362
1171
  scripts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -335,38 +335,6 @@ export const UseCaseSchema = z.object({
335
335
  spec: UseCaseSpecSchema.default({}),
336
336
  });
337
337
  // ---------------------------------------------------------------------------
338
- // Tool (github.com/ruinosus/dna/v1)
339
- //
340
- // Declarative, invocable capability an agent can call. Bridges helix with
341
- // OpenAI/Anthropic tool-calling conventions.
342
- // ---------------------------------------------------------------------------
343
- export const ToolTypeEnum = z.enum(["http", "mcp", "python", "shell", "builtin"]);
344
- export const ToolAuthTypeEnum = z.enum(["none", "api_key", "bearer", "oauth2"]);
345
- export const ToolSpecSchema = z.object({
346
- type: ToolTypeEnum.default("builtin"),
347
- endpoint: z.string().optional().default(""),
348
- method: z.string().optional().default("POST"),
349
- mcp_server: z.string().optional().default(""),
350
- mcp_tool: z.string().optional().default(""),
351
- python_module: z.string().optional().default(""),
352
- python_callable: z.string().optional().default(""),
353
- shell_command: z.string().optional().default(""),
354
- input_schema: z.record(z.unknown()).default({}),
355
- output_schema: z.record(z.unknown()).default({}),
356
- auth_type: ToolAuthTypeEnum.default("none"),
357
- auth_env_var: z.string().optional().default(""),
358
- read_only: z.boolean().default(true),
359
- requires_confirmation: z.boolean().default(false),
360
- tags: z.array(z.string()).default([]),
361
- examples: z.array(z.record(z.unknown())).default([]),
362
- });
363
- export const ToolSchema = z.object({
364
- apiVersion: z.literal("github.com/ruinosus/dna/v1"),
365
- kind: z.literal("Tool"),
366
- metadata: MetadataSchema,
367
- spec: ToolSpecSchema.default({}),
368
- });
369
- // ---------------------------------------------------------------------------
370
338
  // Skill (agentskills.io/v1)
371
339
  // ---------------------------------------------------------------------------
372
340
  export const SkillSpecSchema = z.object({
@@ -0,0 +1,18 @@
1
+ /** The conventional scopes-root sub-directory inside a package (matches the
2
+ * repo's own `.dna/<scope>/` layout). */
3
+ export declare const DEFAULT_SUBPATH = ".dna";
4
+ /** Raised when an `anchor` package / subpath can't be resolved to a real
5
+ * on-disk scopes-root directory. */
6
+ export declare class PackageScopeNotFound extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /**
10
+ * Resolve the `.dna` scopes-root embedded in package `anchor`.
11
+ *
12
+ * `anchor` is a resolvable package specifier (e.g. `"app"`); `subpath` is the
13
+ * scopes-root dir inside it (default `.dna`). Returns the concrete filesystem
14
+ * path of `<anchor-package>/<subpath>` — the `baseDir` a FilesystemSource
15
+ * consumes. Fails loud (`PackageScopeNotFound`) when the package cannot be
16
+ * resolved or the subpath does not exist in the installed package.
17
+ */
18
+ export declare function anchorScopesRoot(anchor: string, subpath?: string): string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Resolve a DNA scope embedded as PACKAGE DATA (`s-scope-as-package-data`).
3
+ *
4
+ * 1:1 parity with python `dna/package_scope.py`. A consumer that deploys an
5
+ * app used to make the scope travel by hand — a brittle
6
+ * `path.resolve(__dirname, "../../.dna")` plus a manual `COPY .dna` in the
7
+ * Dockerfile. The image is the *app*, not the repo; forget the COPY and the
8
+ * app boots with no scope. This module owns the deploy-safe alternative:
9
+ * resolve the scope from INSIDE the installed package.
10
+ *
11
+ * The TS mechanism mirrors how the SDK finds its OWN bundled `*.kind.yaml`
12
+ * (see `kernel/descriptor-loader.ts`): resolve relative to a module. Here the
13
+ * anchor is a package NAME, so we resolve its `package.json` via
14
+ * `createRequire(import.meta.url)` — the package root is that file's dir —
15
+ * then join the scopes-root subpath (`.dna` by default). `npm`/`bun`/`pnpm`
16
+ * install the package UNPACKED into `node_modules`, and the package's `files`
17
+ * field carries the scope into the published tarball and into a Docker image,
18
+ * so resolution works from a source checkout, an installed dependency, and a
19
+ * container whose CWD is not the repo — zero path navigation, zero manual copy.
20
+ *
21
+ * Read-only by nature: package data is composition input, never a write
22
+ * target. To WRITE a scope, use a filesystem or postgres source.
23
+ */
24
+ import { createRequire } from "node:module";
25
+ import { existsSync } from "node:fs";
26
+ import { dirname, isAbsolute, join } from "node:path";
27
+ /** The conventional scopes-root sub-directory inside a package (matches the
28
+ * repo's own `.dna/<scope>/` layout). */
29
+ export const DEFAULT_SUBPATH = ".dna";
30
+ /** Raised when an `anchor` package / subpath can't be resolved to a real
31
+ * on-disk scopes-root directory. */
32
+ export class PackageScopeNotFound extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "PackageScopeNotFound";
36
+ Object.setPrototypeOf(this, new.target.prototype);
37
+ }
38
+ }
39
+ const require = createRequire(import.meta.url);
40
+ /**
41
+ * Resolve the `.dna` scopes-root embedded in package `anchor`.
42
+ *
43
+ * `anchor` is a resolvable package specifier (e.g. `"app"`); `subpath` is the
44
+ * scopes-root dir inside it (default `.dna`). Returns the concrete filesystem
45
+ * path of `<anchor-package>/<subpath>` — the `baseDir` a FilesystemSource
46
+ * consumes. Fails loud (`PackageScopeNotFound`) when the package cannot be
47
+ * resolved or the subpath does not exist in the installed package.
48
+ */
49
+ export function anchorScopesRoot(anchor, subpath = DEFAULT_SUBPATH) {
50
+ const pkgRoot = resolvePackageRoot(anchor);
51
+ const base = subpath ? join(pkgRoot, subpath) : pkgRoot;
52
+ if (!existsSync(base)) {
53
+ throw new PackageScopeNotFound(`anchor '${anchor}' is installed, but its scopes-root '${subpath}' was ` +
54
+ `not found at ${base}. Declare the scope files as package data so ` +
55
+ `they ship in the tarball/image: add the scopes dir to the package's ` +
56
+ `"files" array in package.json (e.g. "files": ["dist", "${subpath}"]). ` +
57
+ `See the guide "Shipping a scope with your app".`);
58
+ }
59
+ return base;
60
+ }
61
+ /** Locate a package's root dir from its name, via its `package.json`. */
62
+ function resolvePackageRoot(anchor) {
63
+ // Primary: resolve the package's package.json — its dir IS the package root.
64
+ // (A package can gate this behind "exports"; the example adds the standard
65
+ // `"./package.json": "./package.json"` so it always resolves.)
66
+ try {
67
+ return dirname(require.resolve(join(anchor, "package.json")));
68
+ }
69
+ catch {
70
+ // Fallback: an absolute/relative path anchor (a directory, not a package
71
+ // name) — treat it as the package root directly.
72
+ if (isAbsolute(anchor) && existsSync(anchor))
73
+ return anchor;
74
+ try {
75
+ // Last resort: resolve the package entry point and use its dir. Only
76
+ // reliable for a flat single-file package, but better than nothing.
77
+ return dirname(require.resolve(anchor));
78
+ }
79
+ catch (err) {
80
+ throw new PackageScopeNotFound(`anchor '${anchor}' is not resolvable — it must be an INSTALLED ` +
81
+ `package that embeds the scope as package data (npm/bun install it, ` +
82
+ `and list the scopes dir in the package's "files" so it ships). ` +
83
+ `If it uses "exports", add "./package.json": "./package.json". ` +
84
+ `Original resolve error: ${err.message}`);
85
+ }
86
+ }
87
+ }
package/dist/prompts.d.ts CHANGED
@@ -17,11 +17,34 @@ export declare class PromptLibrary {
17
17
  /** Names of every prompt-target document in the scope, sorted. */
18
18
  names(): string[];
19
19
  }
20
+ /** Options for {@link loadPrompts}. */
21
+ export interface LoadPromptsOptions {
22
+ /**
23
+ * The directory that holds `<scope>/` (the `.dna` scopes root), following
24
+ * the {@link quickInstance} convention.
25
+ */
26
+ baseDir?: string;
27
+ /**
28
+ * A package specifier whose package data embeds the scope. When given, the
29
+ * scope is resolved from INSIDE the installed package (via its
30
+ * `package.json`), so it TRAVELS with the app — an `npm`/`bun install`
31
+ * carries the package data into the published tarball and into a Docker
32
+ * image, and resolution works identically from a source checkout, an
33
+ * installed dependency, or a container whose CWD is not the repo (no
34
+ * `path.resolve(__dirname, "../..")` navigation, no manual `COPY .dna`).
35
+ * A scope embedded via `anchor` is READ-ONLY. See the guide "Shipping a
36
+ * scope with your app".
37
+ */
38
+ anchor?: string;
39
+ }
20
40
  /**
21
41
  * Compose the prompts of `scope` behind a {@link PromptLibrary}.
22
42
  *
23
- * `baseDir` follows the {@link quickInstance} convention the directory that
24
- * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
25
- * var, then `.dna` in the cwd.
43
+ * Precedence for the scopes-root (first one set wins):
44
+ *
45
+ * `opts.baseDir` > `$DNA_BASE_DIR` > `opts.anchor` (package data) > `.dna`
46
+ *
47
+ * The legacy positional-string form (`loadPrompts(scope, "/path/.dna")`) is
48
+ * still accepted for back-compat and is treated as `baseDir`.
26
49
  */
27
- export declare function loadPrompts(scope: string, baseDir?: string): Promise<PromptLibrary>;
50
+ export declare function loadPrompts(scope: string, opts?: string | LoadPromptsOptions): Promise<PromptLibrary>;
package/dist/prompts.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * is `await`ed in TS but not in Python).
25
25
  */
26
26
  import { quickInstance } from "./bootstrap.js";
27
+ import { anchorScopesRoot } from "./package-scope.js";
27
28
  /** Lazy, cached view `agent name -> composed prompt` over one scope. */
28
29
  export class PromptLibrary {
29
30
  mi;
@@ -65,12 +66,26 @@ export class PromptLibrary {
65
66
  /**
66
67
  * Compose the prompts of `scope` behind a {@link PromptLibrary}.
67
68
  *
68
- * `baseDir` follows the {@link quickInstance} convention the directory that
69
- * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
70
- * var, then `.dna` in the cwd.
69
+ * Precedence for the scopes-root (first one set wins):
70
+ *
71
+ * `opts.baseDir` > `$DNA_BASE_DIR` > `opts.anchor` (package data) > `.dna`
72
+ *
73
+ * The legacy positional-string form (`loadPrompts(scope, "/path/.dna")`) is
74
+ * still accepted for back-compat and is treated as `baseDir`.
71
75
  */
72
- export async function loadPrompts(scope, baseDir) {
73
- const resolved = baseDir ?? process.env.DNA_BASE_DIR ?? ".dna";
74
- const mi = await quickInstance(scope, resolved);
76
+ export async function loadPrompts(scope, opts) {
77
+ const o = typeof opts === "string" ? { baseDir: opts } : opts ?? {};
78
+ const mi = await quickInstance(scope, resolveScopeBaseDir(o));
75
79
  return new PromptLibrary(mi);
76
80
  }
81
+ /** Pick the `.dna` scopes-root by the documented precedence. */
82
+ function resolveScopeBaseDir(o) {
83
+ if (o.baseDir !== undefined)
84
+ return o.baseDir;
85
+ const env = process.env.DNA_BASE_DIR;
86
+ if (env)
87
+ return env;
88
+ if (o.anchor !== undefined)
89
+ return anchorScopesRoot(o.anchor);
90
+ return ".dna";
91
+ }
@@ -0,0 +1,41 @@
1
+ import { ToolNotFound } from "./kernel/errors.js";
2
+ import type { ManifestInstance } from "./kernel/instance.js";
3
+ export { ToolNotFound };
4
+ /**
5
+ * The agent-facing surface of a Tool — exactly what a tool-calling model is
6
+ * shown to decide whether, and how, to call it.
7
+ */
8
+ export interface ToolSurface {
9
+ /** Natural-language description (`metadata.description`) — the text the
10
+ * model reads. */
11
+ readonly description: string;
12
+ /** JSON Schema of the arguments (`spec.input_schema`) — what the model
13
+ * fills in. Empty `{}` when the tool takes no args. */
14
+ readonly parameters: Record<string, unknown>;
15
+ }
16
+ /** Lazy, cached view `tool name -> ToolSurface` over one scope. */
17
+ export declare class ToolLibrary {
18
+ /** The underlying ManifestInstance (drop down for the full surface). */
19
+ readonly mi: ManifestInstance;
20
+ private readonly cache;
21
+ constructor(
22
+ /** The underlying ManifestInstance (drop down for the full surface). */
23
+ mi: ManifestInstance);
24
+ /**
25
+ * Project `name`'s agent-facing surface (cached). Throws {@link ToolNotFound}
26
+ * on a miss.
27
+ */
28
+ get(name: string): ToolSurface;
29
+ /** True when `name` is a Tool document in the scope (no projection). */
30
+ has(name: string): boolean;
31
+ /** Names of every Tool document in the scope, sorted. */
32
+ names(): string[];
33
+ }
34
+ /**
35
+ * Load the Tool surfaces of `scope` behind a {@link ToolLibrary}.
36
+ *
37
+ * `baseDir` follows the {@link quickInstance} convention — the directory that
38
+ * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
39
+ * var, then `.dna` in the cwd.
40
+ */
41
+ export declare function loadTools(scope: string, baseDir?: string): Promise<ToolLibrary>;
package/dist/tools.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `loadTools` — the agent-facing tool surface, as data (TS twin of python
3
+ * `dna.load_tools`).
4
+ *
5
+ * The DNA already governs persona, instruction and guardrails declaratively.
6
+ * A **Tool** (record-plane Kind, `helix/kinds/tool.kind.yaml`) moves the last
7
+ * hard-coded piece into the same plane: the `description` the model reads to
8
+ * decide whether to call a tool, and the JSON Schema of its `parameters`.
9
+ * `loadTools` is the consumer one-liner — the twin of `loadPrompts`:
10
+ *
11
+ * ```ts
12
+ * import { loadTools } from "@ruinosus/dna";
13
+ *
14
+ * const tools = await loadTools("open-swe");
15
+ * const surface = tools.get("github-search"); // or throws ToolNotFound
16
+ * surface.description; // the text the model reads
17
+ * surface.parameters; // the args JSON Schema
18
+ * ```
19
+ *
20
+ * Because a Tool is ONE declarative document, the SAME surface is served to a
21
+ * Python backend (a `@tool` function's `description=`) and this TypeScript
22
+ * frontend (CopilotKit `useCopilotAction`) from one source of truth — the
23
+ * first place the Py↔TS descriptor parity pays off in a real consumer (see
24
+ * `examples/tools_as_data`).
25
+ *
26
+ * TS/Py asymmetry: `loadTools` is async (booting the kernel is async, exactly
27
+ * like `loadPrompts`), but `get(name)` is SYNC — reading a record document
28
+ * needs no async work (unlike `loadPrompts.get`, which awaits composition).
29
+ * A missing tool throws {@link ToolNotFound} (never an empty surface).
30
+ * Overlay-aware: a tenant overlay that overrides a tool's
31
+ * `metadata.description` / `spec.input_schema` is reflected here.
32
+ */
33
+ import { quickInstance } from "./bootstrap.js";
34
+ import { ToolNotFound } from "./kernel/errors.js";
35
+ export { ToolNotFound };
36
+ /** Lazy, cached view `tool name -> ToolSurface` over one scope. */
37
+ export class ToolLibrary {
38
+ mi;
39
+ cache = new Map();
40
+ constructor(
41
+ /** The underlying ManifestInstance (drop down for the full surface). */
42
+ mi) {
43
+ this.mi = mi;
44
+ }
45
+ /**
46
+ * Project `name`'s agent-facing surface (cached). Throws {@link ToolNotFound}
47
+ * on a miss.
48
+ */
49
+ get(name) {
50
+ const cached = this.cache.get(name);
51
+ if (cached !== undefined)
52
+ return cached;
53
+ const doc = this.mi._one("Tool", name);
54
+ if (doc === null) {
55
+ throw new ToolNotFound(name, this.mi.scope ?? null, this.names());
56
+ }
57
+ const meta = doc.metadata;
58
+ const spec = doc.spec;
59
+ const description = typeof meta.description === "string" ? meta.description : "";
60
+ const rawParams = spec.input_schema;
61
+ const parameters = rawParams && typeof rawParams === "object" && !Array.isArray(rawParams)
62
+ ? { ...rawParams }
63
+ : {};
64
+ const surface = { description, parameters };
65
+ this.cache.set(name, surface);
66
+ return surface;
67
+ }
68
+ /** True when `name` is a Tool document in the scope (no projection). */
69
+ has(name) {
70
+ return this.names().includes(name);
71
+ }
72
+ /** Names of every Tool document in the scope, sorted. */
73
+ names() {
74
+ return this.mi._all("Tool").map((d) => d.name).sort();
75
+ }
76
+ }
77
+ /**
78
+ * Load the Tool surfaces of `scope` behind a {@link ToolLibrary}.
79
+ *
80
+ * `baseDir` follows the {@link quickInstance} convention — the directory that
81
+ * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
82
+ * var, then `.dna` in the cwd.
83
+ */
84
+ export async function loadTools(scope, baseDir) {
85
+ const resolved = baseDir ?? process.env.DNA_BASE_DIR ?? ".dna";
86
+ const mi = await quickInstance(scope, resolved);
87
+ return new ToolLibrary(mi);
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dna-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "DNA — Domain Notation of Anything (TypeScript SDK)",
5
5
  "type": "module",
6
6
  "license": "MIT",