@botbotgo/agent-harness 0.0.56 → 0.0.57
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 +63 -1
- package/dist/api.d.ts +7 -0
- package/dist/api.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/resource/resource-impl.js +41 -9
- package/dist/runtime/agent-runtime-adapter.d.ts +8 -0
- package/dist/runtime/agent-runtime-adapter.js +116 -8
- package/dist/runtime/harness.d.ts +7 -0
- package/dist/runtime/harness.js +7 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/inventory.d.ts +10 -5
- package/dist/runtime/inventory.js +50 -12
- package/dist/runtime/skill-requirements.d.ts +27 -0
- package/dist/runtime/skill-requirements.js +112 -0
- package/dist/runtime/support/runtime-env.d.ts +2 -0
- package/dist/runtime/support/runtime-env.js +57 -0
- package/dist/runtime/support/skill-metadata.d.ts +14 -1
- package/dist/runtime/support/skill-metadata.js +59 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./docs/assets/readme-icon.png" width="120" alt="botbotgo runtime icon" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="./docs/assets/readme-banner.png" alt="botbotgo runtime banner" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
1
9
|
# @botbotgo/agent-harness
|
|
2
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<strong>A workspace-shaped runtime for agent systems that need to keep running after the demo.</strong>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<strong>botbotgo</strong> is the brand. <strong>agent-harness</strong> is the product.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://botbotgo.github.io/agent-harness/">Product website</a>
|
|
21
|
+
(static page in <code>docs/</code>, publish with GitHub Pages)
|
|
22
|
+
</p>
|
|
23
|
+
|
|
3
24
|
## Product Overview
|
|
4
25
|
|
|
5
26
|
`@botbotgo/agent-harness` is a workspace-shaped application runtime for real agent products.
|
|
6
27
|
|
|
7
28
|
It is not a new agent framework. It is the runtime layer around LangChain v1 and DeepAgents that turns one workspace into one operable application runtime.
|
|
8
29
|
|
|
30
|
+
The point is simple:
|
|
31
|
+
|
|
32
|
+
- Codex, Claude Code, and Cursor are products for people using agents
|
|
33
|
+
- LangChain v1 and DeepAgents are frameworks for defining agent execution semantics
|
|
34
|
+
- `agent-harness` is the runtime product layer for shipping, operating, recovering, and governing multi-agent applications
|
|
35
|
+
|
|
9
36
|
The product boundary is strict:
|
|
10
37
|
|
|
11
38
|
- LangChain v1 and DeepAgents own agent execution semantics
|
|
12
|
-
-
|
|
39
|
+
- application-level orchestration and lifecycle management stays in the harness
|
|
13
40
|
|
|
14
41
|
That means:
|
|
15
42
|
|
|
@@ -39,6 +66,27 @@ Boundary documents live in:
|
|
|
39
66
|
- `docs/product-boundary.md`
|
|
40
67
|
- `docs/feature-checklist.md`
|
|
41
68
|
|
|
69
|
+
## Why This Exists
|
|
70
|
+
|
|
71
|
+
Most agent demos stop at execution.
|
|
72
|
+
|
|
73
|
+
Real products need a runtime that can answer harder questions:
|
|
74
|
+
|
|
75
|
+
- Where do runs live?
|
|
76
|
+
- How are approvals resolved?
|
|
77
|
+
- What survives process restart?
|
|
78
|
+
- How do you inspect threads and events without exposing raw backend state?
|
|
79
|
+
- How do you swap backend implementations without rewriting the product model?
|
|
80
|
+
|
|
81
|
+
`agent-harness` is the layer that answers those questions without turning your application API into a mirror of LangChain v1 or DeepAgents.
|
|
82
|
+
|
|
83
|
+
## What Makes It Different
|
|
84
|
+
|
|
85
|
+
- It treats `runs`, `threads`, `approvals`, `events`, and recovery as product concepts
|
|
86
|
+
- It keeps checkpoint resume system-managed instead of promoting checkpoint internals into the primary API
|
|
87
|
+
- It lets YAML own assembly complexity while code keeps a tiny surface
|
|
88
|
+
- It goes deep on runtime concerns that upstream libraries do not fully productize
|
|
89
|
+
|
|
42
90
|
## Quick Start
|
|
43
91
|
|
|
44
92
|
Install:
|
|
@@ -89,6 +137,13 @@ try {
|
|
|
89
137
|
}
|
|
90
138
|
```
|
|
91
139
|
|
|
140
|
+
If you want the shortest possible mental model:
|
|
141
|
+
|
|
142
|
+
- one workspace becomes one runtime
|
|
143
|
+
- YAML defines assembly and policy
|
|
144
|
+
- `run(runtime, { ... })` executes requests against that runtime
|
|
145
|
+
- the runtime owns lifecycle, inspection, and recovery
|
|
146
|
+
|
|
92
147
|
## Feature List
|
|
93
148
|
|
|
94
149
|
- Workspace runtime for multi-agent applications
|
|
@@ -103,6 +158,13 @@ try {
|
|
|
103
158
|
- MCP bridge support for agent-declared MCP servers
|
|
104
159
|
- MCP server support for exposing harness tools outward
|
|
105
160
|
|
|
161
|
+
### Runtime Strengths
|
|
162
|
+
|
|
163
|
+
- Stable product-facing API even when backend details evolve
|
|
164
|
+
- Strong separation between public runtime contract and backend adapter contract
|
|
165
|
+
- Clear YAML ownership for routing, topology, policy, and infrastructure objects
|
|
166
|
+
- Better fit for long-running, approval-heavy, multi-agent products than a thin agent loop wrapper
|
|
167
|
+
|
|
106
168
|
## How To Use
|
|
107
169
|
|
|
108
170
|
### Create A Runtime
|
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ApprovalRecord, RunOptions, ResumeOptions, RuntimeAdapterOptions, ThreadSummary, ThreadRecord, WorkspaceLoadOptions } from "./contracts/types.js";
|
|
2
2
|
import { AgentHarnessRuntime } from "./runtime/harness.js";
|
|
3
|
+
import type { InventoryAgentRecord, InventorySkillRecord } from "./runtime/inventory.js";
|
|
4
|
+
import type { RequirementAssessmentOptions } from "./runtime/skill-requirements.js";
|
|
3
5
|
import type { ToolMcpServerOptions } from "./mcp.js";
|
|
4
6
|
export { AgentHarnessRuntime } from "./runtime/harness.js";
|
|
5
7
|
type CreateAgentHarnessOptions = {
|
|
@@ -15,6 +17,11 @@ export declare function getThread(runtime: AgentHarnessRuntime, threadId: string
|
|
|
15
17
|
export declare function deleteThread(runtime: AgentHarnessRuntime, threadId: string): Promise<boolean>;
|
|
16
18
|
export declare function listApprovals(runtime: AgentHarnessRuntime, filter?: Parameters<AgentHarnessRuntime["listApprovals"]>[0]): Promise<ApprovalRecord[]>;
|
|
17
19
|
export declare function getApproval(runtime: AgentHarnessRuntime, approvalId: string): Promise<ApprovalRecord | null>;
|
|
20
|
+
export declare function listAgentSkills(runtime: AgentHarnessRuntime, agentId: string, options?: RequirementAssessmentOptions): InventorySkillRecord[];
|
|
21
|
+
export declare function describeInventory(runtime: AgentHarnessRuntime, options?: RequirementAssessmentOptions): {
|
|
22
|
+
workspaceRoot: string;
|
|
23
|
+
agents: InventoryAgentRecord[];
|
|
24
|
+
};
|
|
18
25
|
export declare function resolveApproval(runtime: AgentHarnessRuntime, options: ResumeOptions): Promise<import("./contracts/types.js").RunResult>;
|
|
19
26
|
export declare function stop(runtime: AgentHarnessRuntime): Promise<void>;
|
|
20
27
|
export declare function createToolMcpServer(runtime: AgentHarnessRuntime, options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
|
package/dist/api.js
CHANGED
|
@@ -28,6 +28,12 @@ export async function listApprovals(runtime, filter) {
|
|
|
28
28
|
export async function getApproval(runtime, approvalId) {
|
|
29
29
|
return runtime.getApproval(approvalId);
|
|
30
30
|
}
|
|
31
|
+
export function listAgentSkills(runtime, agentId, options) {
|
|
32
|
+
return runtime.listAgentSkills(agentId, options);
|
|
33
|
+
}
|
|
34
|
+
export function describeInventory(runtime, options) {
|
|
35
|
+
return runtime.describeWorkspaceInventory(options);
|
|
36
|
+
}
|
|
31
37
|
export async function resolveApproval(runtime, options) {
|
|
32
38
|
return runtime.resume(options);
|
|
33
39
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, getApproval, getThread, listApprovals, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getThread, listAgentSkills, listApprovals, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
2
|
export type { ToolMcpServerOptions } from "./mcp.js";
|
|
3
3
|
export { tool } from "./tools.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, getApproval, getThread, listApprovals, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getThread, listAgentSkills, listApprovals, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
2
|
export { tool } from "./tools.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.56";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.56";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
@@ -12,6 +12,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
12
12
|
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
13
13
|
import { AGENT_HARNESS_VERSION } from "../package-version.js";
|
|
14
14
|
import { isSupportedToolModulePath, loadToolModuleDefinition } from "../tool-modules.js";
|
|
15
|
+
import { createRuntimeEnv } from "../runtime/support/runtime-env.js";
|
|
15
16
|
import { resolveIsolatedResourceModulePath } from "./isolation.js";
|
|
16
17
|
import { ensureExternalResourceSource, ensureExternalSource, isExternalSourceLocator, parseExternalSourceLocator } from "./sources.js";
|
|
17
18
|
const resourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -38,6 +39,33 @@ export function resolveLocalResourceProviderEntry(currentResourceDir, resolveIns
|
|
|
38
39
|
return candidates.at(-1) ?? path.resolve(currentResourceDir, "../../../resource-package/dist/src/index.js");
|
|
39
40
|
}
|
|
40
41
|
const resourceProviderEntry = resolveLocalResourceProviderEntry(resourceDir);
|
|
42
|
+
function listVirtualRootEntries(rootDir) {
|
|
43
|
+
try {
|
|
44
|
+
return new Set(readdirSync(rootDir, { withFileTypes: true }).map((entry) => entry.name));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Set();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeVirtualExecuteCommand(command, rootDir) {
|
|
51
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
52
|
+
return command;
|
|
53
|
+
}
|
|
54
|
+
const rootEntries = listVirtualRootEntries(rootDir);
|
|
55
|
+
if (rootEntries.size === 0) {
|
|
56
|
+
return command;
|
|
57
|
+
}
|
|
58
|
+
return command.replace(/(^|[\s=:(\[{,;|&])(?<quote>["']?)(?<virtualPath>\/(?:[^\s"'`;|&()<>]+))(?:\k<quote>)/g, (match, prefix, quote = "", virtualPath) => {
|
|
59
|
+
const normalizedVirtualPath = virtualPath.replace(/\/+/g, "/");
|
|
60
|
+
const segments = normalizedVirtualPath.split("/").filter((segment) => segment.length > 0);
|
|
61
|
+
const firstSegment = segments[0];
|
|
62
|
+
if (!firstSegment || !rootEntries.has(firstSegment)) {
|
|
63
|
+
return match;
|
|
64
|
+
}
|
|
65
|
+
const translatedPath = path.resolve(rootDir, ...segments);
|
|
66
|
+
return `${prefix}${quote}${translatedPath}${quote}`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
41
69
|
async function loadLocalResource(entry) {
|
|
42
70
|
if (!existsSync(entry)) {
|
|
43
71
|
return null;
|
|
@@ -70,7 +98,11 @@ class CompatibleCompositeBackend {
|
|
|
70
98
|
const sandboxLike = defaultBackend;
|
|
71
99
|
if (typeof sandboxLike.id === "string" && typeof sandboxLike.execute === "function") {
|
|
72
100
|
this.id = sandboxLike.id;
|
|
73
|
-
|
|
101
|
+
const virtualCwd = typeof sandboxLike.cwd === "string" && sandboxLike.virtualMode === true ? sandboxLike.cwd : null;
|
|
102
|
+
this.execute =
|
|
103
|
+
virtualCwd
|
|
104
|
+
? (command) => this.composite.execute(normalizeVirtualExecuteCommand(command, virtualCwd))
|
|
105
|
+
: (command) => this.composite.execute(command);
|
|
74
106
|
}
|
|
75
107
|
}
|
|
76
108
|
lsInfo(filePath) {
|
|
@@ -116,6 +148,10 @@ function createInlineBackendResolver(workspace) {
|
|
|
116
148
|
return workspace.workspaceRoot;
|
|
117
149
|
};
|
|
118
150
|
const createBackend = (kind, config, runtimeLike) => {
|
|
151
|
+
const configuredEnv = typeof config?.env === "object" && config.env
|
|
152
|
+
? Object.fromEntries(Object.entries(config.env).filter((entry) => typeof entry[1] === "string"))
|
|
153
|
+
: undefined;
|
|
154
|
+
const inheritedEnv = config?.inheritEnv === false ? {} : process.env;
|
|
119
155
|
switch (kind) {
|
|
120
156
|
case "LocalShellBackend": {
|
|
121
157
|
const rootDir = resolveBackendRootDir(config?.rootDir);
|
|
@@ -125,9 +161,7 @@ function createInlineBackendResolver(workspace) {
|
|
|
125
161
|
virtualMode: config?.virtualMode === true,
|
|
126
162
|
timeout: typeof config?.timeout === "number" ? config.timeout : undefined,
|
|
127
163
|
maxOutputBytes: typeof config?.maxOutputBytes === "number" ? config.maxOutputBytes : undefined,
|
|
128
|
-
env:
|
|
129
|
-
? Object.fromEntries(Object.entries(config.env).filter((entry) => typeof entry[1] === "string"))
|
|
130
|
-
: undefined,
|
|
164
|
+
env: createRuntimeEnv(configuredEnv, inheritedEnv),
|
|
131
165
|
inheritEnv: config?.inheritEnv !== false,
|
|
132
166
|
});
|
|
133
167
|
}
|
|
@@ -139,9 +173,7 @@ function createInlineBackendResolver(workspace) {
|
|
|
139
173
|
virtualMode: config?.virtualMode === false ? false : true,
|
|
140
174
|
timeout: typeof config?.timeout === "number" ? config.timeout : undefined,
|
|
141
175
|
maxOutputBytes: typeof config?.maxOutputBytes === "number" ? config.maxOutputBytes : undefined,
|
|
142
|
-
env:
|
|
143
|
-
? Object.fromEntries(Object.entries(config.env).filter((entry) => typeof entry[1] === "string"))
|
|
144
|
-
: undefined,
|
|
176
|
+
env: createRuntimeEnv(configuredEnv, inheritedEnv),
|
|
145
177
|
inheritEnv: config?.inheritEnv !== false,
|
|
146
178
|
});
|
|
147
179
|
}
|
|
@@ -318,7 +350,7 @@ export async function getOrCreateMcpClient(config) {
|
|
|
318
350
|
: new StdioClientTransport({
|
|
319
351
|
command: config.command ?? "",
|
|
320
352
|
args: config.args,
|
|
321
|
-
env: config.env,
|
|
353
|
+
env: createRuntimeEnv(config.env),
|
|
322
354
|
cwd: config.cwd,
|
|
323
355
|
});
|
|
324
356
|
await client.connect(transport);
|
|
@@ -12,6 +12,13 @@ declare class RuntimeOperationTimeoutError extends Error {
|
|
|
12
12
|
readonly stage: "stream" | "invoke";
|
|
13
13
|
constructor(operation: string, timeoutMs: number, stage?: "stream" | "invoke");
|
|
14
14
|
}
|
|
15
|
+
export declare function relativizeDeepAgentSkillSourcePaths(workspaceRoot: string | undefined, skillPaths: string[] | undefined): string[] | undefined;
|
|
16
|
+
export declare function materializeDeepAgentSkillSourcePaths(options: {
|
|
17
|
+
workspaceRoot?: string;
|
|
18
|
+
runRoot?: string;
|
|
19
|
+
ownerId: string;
|
|
20
|
+
skillPaths?: string[];
|
|
21
|
+
}): Promise<string[] | undefined>;
|
|
15
22
|
export declare class AgentRuntimeAdapter {
|
|
16
23
|
private readonly options;
|
|
17
24
|
constructor(options?: RuntimeAdapterOptions);
|
|
@@ -26,6 +33,7 @@ export declare class AgentRuntimeAdapter {
|
|
|
26
33
|
private resolveModel;
|
|
27
34
|
private buildToolNameMapping;
|
|
28
35
|
private buildAgentMessages;
|
|
36
|
+
private buildSlashCommandSkillInstruction;
|
|
29
37
|
private buildInvocationRequest;
|
|
30
38
|
private buildStateSnapshot;
|
|
31
39
|
private buildRawModelMessages;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
2
4
|
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
3
5
|
import { tool as createLangChainTool } from "@langchain/core/tools";
|
|
4
6
|
import { createDeepAgent, createMemoryMiddleware, createSkillsMiddleware, createSubAgentMiddleware, FilesystemBackend, } from "deepagents";
|
|
@@ -16,6 +18,7 @@ import { wrapToolForExecution } from "./tool-hitl.js";
|
|
|
16
18
|
import { resolveDeclaredMiddleware } from "./declared-middleware.js";
|
|
17
19
|
import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
|
|
18
20
|
import { getBindingDeepAgentParams, getBindingInterruptCompatibilityRules, getBindingLangChainParams, getBindingMiddlewareConfigs, getBindingModelInit, getBindingPrimaryModel, getBindingPrimaryTools, getBindingSystemPrompt, isDeepAgentBinding, isLangChainBinding, } from "./support/compiled-binding.js";
|
|
21
|
+
import { readSkillMetadata } from "./support/skill-metadata.js";
|
|
19
22
|
function countConfiguredTools(binding) {
|
|
20
23
|
return getBindingPrimaryTools(binding).length;
|
|
21
24
|
}
|
|
@@ -52,6 +55,65 @@ function computeRemainingTimeoutMs(deadlineAt, fallbackTimeoutMs) {
|
|
|
52
55
|
function isPlaceholderApiKey(value) {
|
|
53
56
|
return typeof value === "string" && value.trim().toLowerCase() === "dummy";
|
|
54
57
|
}
|
|
58
|
+
export function relativizeDeepAgentSkillSourcePaths(workspaceRoot, skillPaths) {
|
|
59
|
+
if (!workspaceRoot || !skillPaths) {
|
|
60
|
+
return skillPaths;
|
|
61
|
+
}
|
|
62
|
+
return skillPaths.map((skillPath) => {
|
|
63
|
+
if (!path.isAbsolute(skillPath)) {
|
|
64
|
+
return skillPath;
|
|
65
|
+
}
|
|
66
|
+
const relative = path.relative(workspaceRoot, skillPath);
|
|
67
|
+
if (!relative || relative.startsWith("..")) {
|
|
68
|
+
return skillPath;
|
|
69
|
+
}
|
|
70
|
+
return relative.split(path.sep).join("/");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function isDeepAgentSkillDirectory(sourcePath) {
|
|
74
|
+
return existsSync(sourcePath) && statSync(sourcePath).isDirectory() && existsSync(path.join(sourcePath, "SKILL.md"));
|
|
75
|
+
}
|
|
76
|
+
function toWorkspaceRelativePath(workspaceRoot, targetPath) {
|
|
77
|
+
if (!workspaceRoot) {
|
|
78
|
+
return targetPath;
|
|
79
|
+
}
|
|
80
|
+
const relative = path.relative(workspaceRoot, targetPath);
|
|
81
|
+
if (!relative || relative.startsWith("..")) {
|
|
82
|
+
return targetPath;
|
|
83
|
+
}
|
|
84
|
+
return relative.split(path.sep).join("/");
|
|
85
|
+
}
|
|
86
|
+
export async function materializeDeepAgentSkillSourcePaths(options) {
|
|
87
|
+
const { workspaceRoot, runRoot, ownerId, skillPaths } = options;
|
|
88
|
+
if (!skillPaths) {
|
|
89
|
+
return skillPaths;
|
|
90
|
+
}
|
|
91
|
+
const materialized = relativizeDeepAgentSkillSourcePaths(workspaceRoot, skillPaths) ?? skillPaths;
|
|
92
|
+
if (!workspaceRoot || !runRoot) {
|
|
93
|
+
return materialized;
|
|
94
|
+
}
|
|
95
|
+
const sourceRoot = path.join(runRoot, "deepagent-skill-sources", ownerId);
|
|
96
|
+
let wroteSyntheticSource = false;
|
|
97
|
+
const resolvedSources = [];
|
|
98
|
+
for (const [index, sourcePath] of materialized.entries()) {
|
|
99
|
+
const absolutePath = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(workspaceRoot, sourcePath);
|
|
100
|
+
if (!isDeepAgentSkillDirectory(absolutePath)) {
|
|
101
|
+
resolvedSources.push(sourcePath);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!wroteSyntheticSource) {
|
|
105
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
106
|
+
await mkdir(sourceRoot, { recursive: true });
|
|
107
|
+
wroteSyntheticSource = true;
|
|
108
|
+
}
|
|
109
|
+
const skillDirectoryName = path.basename(absolutePath);
|
|
110
|
+
const syntheticSourcePath = path.join(sourceRoot, `${String(index + 1).padStart(3, "0")}-${skillDirectoryName}`);
|
|
111
|
+
await mkdir(syntheticSourcePath, { recursive: true });
|
|
112
|
+
await cp(absolutePath, path.join(syntheticSourcePath, skillDirectoryName), { recursive: true });
|
|
113
|
+
resolvedSources.push(toWorkspaceRelativePath(workspaceRoot, syntheticSourcePath));
|
|
114
|
+
}
|
|
115
|
+
return resolvedSources;
|
|
116
|
+
}
|
|
55
117
|
function buildAuthOmittingFetch(baseFetch = fetch) {
|
|
56
118
|
return async (input, init) => {
|
|
57
119
|
const sanitizedHeaders = new Headers(input instanceof Request ? input.headers : undefined);
|
|
@@ -399,11 +461,42 @@ export class AgentRuntimeAdapter {
|
|
|
399
461
|
{ role: "user", content: normalizeMessageContent(input) },
|
|
400
462
|
];
|
|
401
463
|
}
|
|
402
|
-
|
|
464
|
+
buildSlashCommandSkillInstruction(binding, input) {
|
|
465
|
+
const inputText = extractMessageText(input).trim();
|
|
466
|
+
const match = inputText.match(/^\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?$/i);
|
|
467
|
+
if (!match) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
const invokedName = match[1].toLowerCase();
|
|
471
|
+
const argumentText = match[2]?.trim() ?? "";
|
|
472
|
+
const skillPaths = binding.deepAgentParams?.skills ?? binding.langchainAgentParams?.skills ?? [];
|
|
473
|
+
const matchedSkillPath = skillPaths.find((skillPath) => readSkillMetadata(skillPath).name.toLowerCase() === invokedName);
|
|
474
|
+
if (!matchedSkillPath) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
const metadata = readSkillMetadata(matchedSkillPath);
|
|
478
|
+
const skillQualifier = metadata.userInvocable === true ? "user-invocable skill" : "skill";
|
|
479
|
+
const dryRunHint = /\s--dry-run(?:\s|$)/.test(` ${argumentText} `)
|
|
480
|
+
? "This invocation includes `--dry-run`. Perform the real fetch or inspection steps needed for dry-run output. Do not return hypothetical or mock results."
|
|
481
|
+
: undefined;
|
|
482
|
+
return [
|
|
483
|
+
`This user message is an explicit command-style invocation of the ${skillQualifier} \`${metadata.name}\`.`,
|
|
484
|
+
`Read the skill file for \`${metadata.name}\` before taking action, then follow its documented phases and constraints exactly.`,
|
|
485
|
+
`You must use the \`${metadata.name}\` skill for this request and follow its documented workflow.`,
|
|
486
|
+
`Treat everything after \`/${metadata.name}\` as the skill argument string: ${argumentText ? JSON.stringify(argumentText) : '""'}.`,
|
|
487
|
+
"Do not answer with a generic explanation of what the skill would do. Execute the skill workflow using the available tools unless the skill instructions explicitly require confirmation before acting.",
|
|
488
|
+
dryRunHint,
|
|
489
|
+
].filter((line) => typeof line === "string" && line.length > 0).join("\n");
|
|
490
|
+
}
|
|
491
|
+
buildInvocationRequest(binding, history, input, options = {}) {
|
|
492
|
+
const userInvocableInstruction = this.buildSlashCommandSkillInstruction(binding, input);
|
|
493
|
+
const messages = this.buildAgentMessages(history, input);
|
|
403
494
|
return {
|
|
404
495
|
...(options.state ?? {}),
|
|
405
496
|
...(options.files ? { files: options.files } : {}),
|
|
406
|
-
messages:
|
|
497
|
+
messages: userInvocableInstruction
|
|
498
|
+
? [{ role: "system", content: userInvocableInstruction }, ...messages]
|
|
499
|
+
: messages,
|
|
407
500
|
};
|
|
408
501
|
}
|
|
409
502
|
buildStateSnapshot(result) {
|
|
@@ -414,11 +507,15 @@ export class AgentRuntimeAdapter {
|
|
|
414
507
|
delete snapshot.files;
|
|
415
508
|
return Object.keys(snapshot).length > 0 ? snapshot : undefined;
|
|
416
509
|
}
|
|
417
|
-
buildRawModelMessages(systemPrompt, history, input) {
|
|
510
|
+
buildRawModelMessages(binding, systemPrompt, history, input) {
|
|
418
511
|
const messages = [];
|
|
419
512
|
if (systemPrompt) {
|
|
420
513
|
messages.push({ role: "system", content: systemPrompt });
|
|
421
514
|
}
|
|
515
|
+
const userInvocableInstruction = this.buildSlashCommandSkillInstruction(binding, input);
|
|
516
|
+
if (userInvocableInstruction) {
|
|
517
|
+
messages.push({ role: "system", content: userInvocableInstruction });
|
|
518
|
+
}
|
|
422
519
|
messages.push(...this.buildAgentMessages(history, input));
|
|
423
520
|
return messages;
|
|
424
521
|
}
|
|
@@ -572,6 +669,12 @@ export class AgentRuntimeAdapter {
|
|
|
572
669
|
...(subagent.passthrough ?? {}),
|
|
573
670
|
model: subagent.model ? (await this.resolveModel(subagent.model)) : undefined,
|
|
574
671
|
tools: subagent.tools ? this.resolveTools(subagent.tools) : undefined,
|
|
672
|
+
skills: await materializeDeepAgentSkillSourcePaths({
|
|
673
|
+
workspaceRoot: binding?.harnessRuntime.workspaceRoot,
|
|
674
|
+
runRoot: binding?.harnessRuntime.runRoot,
|
|
675
|
+
ownerId: `${binding?.agent.id ?? "agent"}-${subagent.name}`,
|
|
676
|
+
skillPaths: subagent.skills,
|
|
677
|
+
}),
|
|
575
678
|
interruptOn: this.compileInterruptOn(subagent.tools ?? [], subagent.interruptOn),
|
|
576
679
|
responseFormat: subagent.responseFormat,
|
|
577
680
|
contextSchema: subagent.contextSchema,
|
|
@@ -635,7 +738,12 @@ export class AgentRuntimeAdapter {
|
|
|
635
738
|
interruptOn: this.resolveInterruptOn(binding),
|
|
636
739
|
name: params.name,
|
|
637
740
|
memory: params.memory,
|
|
638
|
-
skills:
|
|
741
|
+
skills: await materializeDeepAgentSkillSourcePaths({
|
|
742
|
+
workspaceRoot: binding.harnessRuntime.workspaceRoot,
|
|
743
|
+
runRoot: binding.harnessRuntime.runRoot,
|
|
744
|
+
ownerId: binding.agent.id,
|
|
745
|
+
skillPaths: params.skills,
|
|
746
|
+
}),
|
|
639
747
|
generalPurposeAgent: params.generalPurposeAgent,
|
|
640
748
|
taskDescription: params.taskDescription,
|
|
641
749
|
};
|
|
@@ -670,7 +778,7 @@ export class AgentRuntimeAdapter {
|
|
|
670
778
|
}
|
|
671
779
|
async invoke(binding, input, threadId, runId, resumePayload, history = [], options = {}) {
|
|
672
780
|
const request = resumePayload === undefined
|
|
673
|
-
? this.buildInvocationRequest(history, input, options)
|
|
781
|
+
? this.buildInvocationRequest(binding, history, input, options)
|
|
674
782
|
: new Command({ resume: resumePayload });
|
|
675
783
|
let result;
|
|
676
784
|
try {
|
|
@@ -683,7 +791,7 @@ export class AgentRuntimeAdapter {
|
|
|
683
791
|
}
|
|
684
792
|
const retriedBinding = this.applyStrictToolJsonInstruction(binding);
|
|
685
793
|
const runnable = await this.create(retriedBinding);
|
|
686
|
-
result = (await this.withTimeout(() => runnable.invoke(this.buildInvocationRequest(history, input, options), { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
|
|
794
|
+
result = (await this.withTimeout(() => runnable.invoke(this.buildInvocationRequest(retriedBinding, history, input, options), { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
|
|
687
795
|
}
|
|
688
796
|
const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
|
|
689
797
|
const extractedOutput = extractVisibleOutput(result);
|
|
@@ -739,7 +847,7 @@ export class AgentRuntimeAdapter {
|
|
|
739
847
|
// agent loop and only adds an extra model round-trip before the runnable path.
|
|
740
848
|
if (canUseDirectModelStream && typeof model.stream === "function") {
|
|
741
849
|
let emitted = false;
|
|
742
|
-
const stream = await this.withTimeout(() => model.stream(this.buildRawModelMessages(getBindingSystemPrompt(binding), history, input)), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "model stream start", "stream");
|
|
850
|
+
const stream = await this.withTimeout(() => model.stream(this.buildRawModelMessages(binding, getBindingSystemPrompt(binding), history, input)), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "model stream start", "stream");
|
|
743
851
|
for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "model stream", streamDeadlineAt, invokeTimeoutMs)) {
|
|
744
852
|
const delta = readStreamDelta(chunk);
|
|
745
853
|
if (delta) {
|
|
@@ -758,7 +866,7 @@ export class AgentRuntimeAdapter {
|
|
|
758
866
|
}
|
|
759
867
|
}
|
|
760
868
|
const runnable = await this.create(binding);
|
|
761
|
-
const request = this.buildInvocationRequest(history, input, options);
|
|
869
|
+
const request = this.buildInvocationRequest(binding, history, input, options);
|
|
762
870
|
if (typeof runnable.streamEvents === "function") {
|
|
763
871
|
const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2", ...(options.context ? { context: options.context } : {}) }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
|
|
764
872
|
const allowVisibleStreamDeltas = isLangChainBinding(binding);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ApprovalRecord, HarnessEvent, HarnessStreamItem, MessageContent, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
2
2
|
import { type ToolMcpServerOptions } from "../mcp.js";
|
|
3
|
+
import { type InventoryAgentRecord, type InventorySkillRecord } from "./inventory.js";
|
|
4
|
+
import type { RequirementAssessmentOptions } from "./skill-requirements.js";
|
|
3
5
|
export declare class AgentHarnessRuntime {
|
|
4
6
|
private readonly workspace;
|
|
5
7
|
private readonly runtimeAdapterOptions;
|
|
@@ -53,6 +55,11 @@ export declare class AgentHarnessRuntime {
|
|
|
53
55
|
runId?: string;
|
|
54
56
|
}): Promise<ApprovalRecord[]>;
|
|
55
57
|
getApproval(approvalId: string): Promise<ApprovalRecord | null>;
|
|
58
|
+
listAgentSkills(agentId: string, options?: RequirementAssessmentOptions): InventorySkillRecord[];
|
|
59
|
+
describeWorkspaceInventory(options?: RequirementAssessmentOptions): {
|
|
60
|
+
workspaceRoot: string;
|
|
61
|
+
agents: InventoryAgentRecord[];
|
|
62
|
+
};
|
|
56
63
|
private deleteThreadCheckpoints;
|
|
57
64
|
deleteThread(threadId: string): Promise<boolean>;
|
|
58
65
|
createToolMcpServer(options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -16,6 +16,7 @@ import { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, readCh
|
|
|
16
16
|
import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
|
|
17
17
|
import { createToolMcpServerFromTools, serveToolsOverStdioFromHarness } from "../mcp.js";
|
|
18
18
|
import { getBindingAdapterKind, getBindingPrimaryTools, getBindingStoreConfig } from "./support/compiled-binding.js";
|
|
19
|
+
import { describeWorkspaceInventory, listAgentSkills as listWorkspaceAgentSkills, } from "./inventory.js";
|
|
19
20
|
export class AgentHarnessRuntime {
|
|
20
21
|
workspace;
|
|
21
22
|
runtimeAdapterOptions;
|
|
@@ -286,6 +287,12 @@ export class AgentHarnessRuntime {
|
|
|
286
287
|
const approval = await this.persistence.getApproval(approvalId);
|
|
287
288
|
return approval ? this.toPublicApprovalRecord(approval) : null;
|
|
288
289
|
}
|
|
290
|
+
listAgentSkills(agentId, options = {}) {
|
|
291
|
+
return listWorkspaceAgentSkills(this.workspace, agentId, options);
|
|
292
|
+
}
|
|
293
|
+
describeWorkspaceInventory(options = {}) {
|
|
294
|
+
return describeWorkspaceInventory(this.workspace, options);
|
|
295
|
+
}
|
|
289
296
|
async deleteThreadCheckpoints(threadId) {
|
|
290
297
|
const resolver = this.resolvedRuntimeAdapterOptions.checkpointerResolver;
|
|
291
298
|
if (!resolver) {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, mainta
|
|
|
6
6
|
export { ManagedSqliteSaver } from "./sqlite-maintained-checkpoint-saver.js";
|
|
7
7
|
export { AgentHarnessRuntime, AgentHarness } from "./harness.js";
|
|
8
8
|
export { describeWorkspaceInventory, findAgentBinding, listAgentSkills, listAgentTools, listAvailableAgents, listSpecialists, } from "./inventory.js";
|
|
9
|
+
export { assessOpenClawRequirements, assessSkillRequirements, } from "./skill-requirements.js";
|
|
9
10
|
export * from "./parsing/index.js";
|
|
10
11
|
export { PolicyEngine } from "./policy-engine.js";
|
|
11
12
|
export { createInMemoryStore, FileBackedStore } from "./store.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, mainta
|
|
|
6
6
|
export { ManagedSqliteSaver } from "./sqlite-maintained-checkpoint-saver.js";
|
|
7
7
|
export { AgentHarnessRuntime, AgentHarness } from "./harness.js";
|
|
8
8
|
export { describeWorkspaceInventory, findAgentBinding, listAgentSkills, listAgentTools, listAvailableAgents, listSpecialists, } from "./inventory.js";
|
|
9
|
+
export { assessOpenClawRequirements, assessSkillRequirements, } from "./skill-requirements.js";
|
|
9
10
|
export * from "./parsing/index.js";
|
|
10
11
|
export { PolicyEngine } from "./policy-engine.js";
|
|
11
12
|
export { createInMemoryStore, FileBackedStore } from "./store.js";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CompiledAgentBinding, WorkspaceBundle } from "../contracts/types.js";
|
|
2
|
+
import type { OpenClawSkillMetadata } from "./support/skill-metadata.js";
|
|
3
|
+
import { type RequirementAssessmentOptions, type SkillRequirementAssessment } from "./skill-requirements.js";
|
|
2
4
|
export type InventoryToolRecord = {
|
|
3
5
|
name: string;
|
|
4
6
|
description: string;
|
|
@@ -9,8 +11,11 @@ export type InventorySkillRecord = {
|
|
|
9
11
|
description?: string;
|
|
10
12
|
license?: string;
|
|
11
13
|
compatibility?: string;
|
|
12
|
-
metadata?: Record<string,
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
13
15
|
allowedTools?: string[];
|
|
16
|
+
userInvocable?: boolean;
|
|
17
|
+
openclaw?: OpenClawSkillMetadata;
|
|
18
|
+
requirements?: SkillRequirementAssessment;
|
|
14
19
|
};
|
|
15
20
|
export type InventoryAgentRecord = {
|
|
16
21
|
id: string;
|
|
@@ -21,10 +26,10 @@ export type InventoryAgentRecord = {
|
|
|
21
26
|
};
|
|
22
27
|
export declare function findAgentBinding(workspace: WorkspaceBundle, agentId: string): CompiledAgentBinding | undefined;
|
|
23
28
|
export declare function listAgentTools(workspace: WorkspaceBundle, agentId: string): InventoryToolRecord[];
|
|
24
|
-
export declare function listAgentSkills(workspace: WorkspaceBundle, agentId: string): InventorySkillRecord[];
|
|
25
|
-
export declare function listSpecialists(workspace: WorkspaceBundle): InventoryAgentRecord[];
|
|
26
|
-
export declare function listAvailableAgents(workspace: WorkspaceBundle): InventoryAgentRecord[];
|
|
27
|
-
export declare function describeWorkspaceInventory(workspace: WorkspaceBundle): {
|
|
29
|
+
export declare function listAgentSkills(workspace: WorkspaceBundle, agentId: string, options?: RequirementAssessmentOptions): InventorySkillRecord[];
|
|
30
|
+
export declare function listSpecialists(workspace: WorkspaceBundle, options?: RequirementAssessmentOptions): InventoryAgentRecord[];
|
|
31
|
+
export declare function listAvailableAgents(workspace: WorkspaceBundle, options?: RequirementAssessmentOptions): InventoryAgentRecord[];
|
|
32
|
+
export declare function describeWorkspaceInventory(workspace: WorkspaceBundle, options?: RequirementAssessmentOptions): {
|
|
28
33
|
workspaceRoot: string;
|
|
29
34
|
agents: InventoryAgentRecord[];
|
|
30
35
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readSkillMetadata } from "./support/skill-metadata.js";
|
|
2
2
|
import { getBindingPrimaryTools } from "./support/compiled-binding.js";
|
|
3
|
+
import { assessSkillRequirements, } from "./skill-requirements.js";
|
|
4
|
+
import { createRuntimeEnv } from "./support/runtime-env.js";
|
|
3
5
|
function listHostBindings(workspace) {
|
|
4
6
|
return Array.from(workspace.bindings.values()).filter((binding) => binding.harnessRuntime.hostFacing);
|
|
5
7
|
}
|
|
@@ -13,7 +15,36 @@ function dedupeTools(tools) {
|
|
|
13
15
|
}
|
|
14
16
|
return Array.from(deduped.values());
|
|
15
17
|
}
|
|
16
|
-
function
|
|
18
|
+
function readBackendRequirementOptions(binding) {
|
|
19
|
+
const backend = binding.deepAgentParams?.backend && typeof binding.deepAgentParams.backend === "object"
|
|
20
|
+
? binding.deepAgentParams.backend
|
|
21
|
+
: undefined;
|
|
22
|
+
if (!backend) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
const backendState = typeof backend.state === "object" && backend.state
|
|
26
|
+
? backend.state
|
|
27
|
+
: backend;
|
|
28
|
+
const env = typeof backendState.env === "object" && backendState.env
|
|
29
|
+
? Object.fromEntries(Object.entries(backendState.env).filter((entry) => typeof entry[1] === "string"))
|
|
30
|
+
: undefined;
|
|
31
|
+
const inheritedEnv = backendState.inheritEnv === false ? {} : process.env;
|
|
32
|
+
const runtimeEnv = createRuntimeEnv(env, inheritedEnv);
|
|
33
|
+
return {
|
|
34
|
+
env: runtimeEnv,
|
|
35
|
+
path: runtimeEnv.PATH,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function mergeRequirementOptions(binding, options = {}) {
|
|
39
|
+
const fromBackend = readBackendRequirementOptions(binding);
|
|
40
|
+
return {
|
|
41
|
+
...fromBackend,
|
|
42
|
+
...options,
|
|
43
|
+
env: options.env ? { ...(fromBackend.env ?? {}), ...options.env } : fromBackend.env,
|
|
44
|
+
path: options.path ?? fromBackend.path,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toSkillRecords(skillPaths, options = {}) {
|
|
17
48
|
return Array.from(new Set(skillPaths)).map((skillPath) => {
|
|
18
49
|
const metadata = readSkillMetadata(skillPath);
|
|
19
50
|
return {
|
|
@@ -24,6 +55,9 @@ function toSkillRecords(skillPaths) {
|
|
|
24
55
|
compatibility: metadata.compatibility,
|
|
25
56
|
metadata: metadata.metadata,
|
|
26
57
|
allowedTools: metadata.allowedTools,
|
|
58
|
+
userInvocable: metadata.userInvocable,
|
|
59
|
+
openclaw: metadata.openclaw,
|
|
60
|
+
requirements: assessSkillRequirements(metadata, options),
|
|
27
61
|
};
|
|
28
62
|
});
|
|
29
63
|
}
|
|
@@ -34,38 +68,42 @@ export function listAgentTools(workspace, agentId) {
|
|
|
34
68
|
}
|
|
35
69
|
return dedupeTools(getBindingPrimaryTools(binding));
|
|
36
70
|
}
|
|
37
|
-
export function listAgentSkills(workspace, agentId) {
|
|
71
|
+
export function listAgentSkills(workspace, agentId, options = {}) {
|
|
38
72
|
const binding = findAgentBinding(workspace, agentId);
|
|
39
73
|
if (!binding) {
|
|
40
74
|
return [];
|
|
41
75
|
}
|
|
42
|
-
|
|
76
|
+
const resolvedOptions = mergeRequirementOptions(binding, options);
|
|
77
|
+
return toSkillRecords(binding.deepAgentParams?.skills ?? binding.langchainAgentParams?.skills ?? [], resolvedOptions);
|
|
43
78
|
}
|
|
44
|
-
function describeSubagent(subagent) {
|
|
79
|
+
function describeSubagent(subagent, options = {}) {
|
|
45
80
|
return {
|
|
46
81
|
id: subagent.name,
|
|
47
82
|
description: subagent.description,
|
|
48
83
|
role: "specialist",
|
|
49
84
|
tools: dedupeTools(subagent.tools ?? []),
|
|
50
|
-
skills: toSkillRecords(subagent.skills ?? []),
|
|
85
|
+
skills: toSkillRecords(subagent.skills ?? [], options),
|
|
51
86
|
};
|
|
52
87
|
}
|
|
53
|
-
export function listSpecialists(workspace) {
|
|
54
|
-
return listHostBindings(workspace).flatMap((binding) =>
|
|
88
|
+
export function listSpecialists(workspace, options = {}) {
|
|
89
|
+
return listHostBindings(workspace).flatMap((binding) => {
|
|
90
|
+
const resolvedOptions = mergeRequirementOptions(binding, options);
|
|
91
|
+
return (binding.deepAgentParams?.subagents ?? []).map((subagent) => describeSubagent(subagent, resolvedOptions));
|
|
92
|
+
});
|
|
55
93
|
}
|
|
56
|
-
export function listAvailableAgents(workspace) {
|
|
94
|
+
export function listAvailableAgents(workspace, options = {}) {
|
|
57
95
|
const topLevel = listHostBindings(workspace).map((binding) => ({
|
|
58
96
|
id: binding.agent.id,
|
|
59
97
|
description: binding.agent.description,
|
|
60
98
|
role: "host",
|
|
61
99
|
tools: listAgentTools(workspace, binding.agent.id),
|
|
62
|
-
skills: listAgentSkills(workspace, binding.agent.id),
|
|
100
|
+
skills: listAgentSkills(workspace, binding.agent.id, options),
|
|
63
101
|
}));
|
|
64
|
-
return [...topLevel, ...listSpecialists(workspace)];
|
|
102
|
+
return [...topLevel, ...listSpecialists(workspace, options)];
|
|
65
103
|
}
|
|
66
|
-
export function describeWorkspaceInventory(workspace) {
|
|
104
|
+
export function describeWorkspaceInventory(workspace, options = {}) {
|
|
67
105
|
return {
|
|
68
106
|
workspaceRoot: workspace.workspaceRoot,
|
|
69
|
-
agents: listAvailableAgents(workspace),
|
|
107
|
+
agents: listAvailableAgents(workspace, options),
|
|
70
108
|
};
|
|
71
109
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OpenClawSkillMetadata, SkillMetadata } from "./support/skill-metadata.js";
|
|
2
|
+
export type RequirementStatus = "satisfied" | "missing" | "unknown";
|
|
3
|
+
export type RequirementCheckRecord = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: RequirementStatus;
|
|
6
|
+
};
|
|
7
|
+
export type OpenClawRequirementAssessment = {
|
|
8
|
+
ready: boolean;
|
|
9
|
+
missing: boolean;
|
|
10
|
+
unknown: boolean;
|
|
11
|
+
bins: RequirementCheckRecord[];
|
|
12
|
+
anyBins: RequirementCheckRecord[];
|
|
13
|
+
env: RequirementCheckRecord[];
|
|
14
|
+
config: RequirementCheckRecord[];
|
|
15
|
+
primaryEnv?: RequirementCheckRecord;
|
|
16
|
+
};
|
|
17
|
+
export type SkillRequirementAssessment = {
|
|
18
|
+
openclaw?: OpenClawRequirementAssessment;
|
|
19
|
+
};
|
|
20
|
+
export type RequirementAssessmentOptions = {
|
|
21
|
+
env?: Record<string, string | undefined>;
|
|
22
|
+
path?: string;
|
|
23
|
+
availableBins?: string[];
|
|
24
|
+
config?: Record<string, unknown> | string[];
|
|
25
|
+
};
|
|
26
|
+
export declare function assessOpenClawRequirements(metadata: OpenClawSkillMetadata | undefined, options?: RequirementAssessmentOptions): OpenClawRequirementAssessment | undefined;
|
|
27
|
+
export declare function assessSkillRequirements(metadata: SkillMetadata, options?: RequirementAssessmentOptions): SkillRequirementAssessment;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { augmentExecutablePath } from "./support/runtime-env.js";
|
|
4
|
+
function dedupe(items) {
|
|
5
|
+
return Array.from(new Set(items.filter(Boolean)));
|
|
6
|
+
}
|
|
7
|
+
function pathEntries(pathValue) {
|
|
8
|
+
return augmentExecutablePath(pathValue ?? process.env.PATH ?? "")
|
|
9
|
+
.split(path.delimiter)
|
|
10
|
+
.map((entry) => entry.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function isExecutableInPath(binName, searchPath) {
|
|
14
|
+
for (const entry of pathEntries(searchPath)) {
|
|
15
|
+
if (existsSync(path.join(entry, binName))) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function assessNamedRequirements(names, evaluator) {
|
|
22
|
+
return dedupe(names ?? []).map((name) => ({
|
|
23
|
+
name,
|
|
24
|
+
status: evaluator(name),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function hasConfigPath(config, dottedPath) {
|
|
28
|
+
const parts = dottedPath.split(".").filter(Boolean);
|
|
29
|
+
let cursor = config;
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor) || !(part in cursor)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
cursor = cursor[part];
|
|
35
|
+
}
|
|
36
|
+
return cursor !== undefined;
|
|
37
|
+
}
|
|
38
|
+
function assessConfigRequirements(names, config) {
|
|
39
|
+
if (!names || names.length === 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!config) {
|
|
43
|
+
return dedupe(names).map((name) => ({ name, status: "unknown" }));
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(config)) {
|
|
46
|
+
const set = new Set(config);
|
|
47
|
+
return dedupe(names).map((name) => ({
|
|
48
|
+
name,
|
|
49
|
+
status: set.has(name) ? "satisfied" : "missing",
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
return dedupe(names).map((name) => ({
|
|
53
|
+
name,
|
|
54
|
+
status: hasConfigPath(config, name) ? "satisfied" : "missing",
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function summarizeRequirementGroups(groups) {
|
|
58
|
+
const flat = groups.flat();
|
|
59
|
+
const missing = flat.some((item) => item.status === "missing");
|
|
60
|
+
const unknown = flat.some((item) => item.status === "unknown");
|
|
61
|
+
return {
|
|
62
|
+
ready: flat.length === 0 ? true : !missing && !unknown,
|
|
63
|
+
missing,
|
|
64
|
+
unknown,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function assessOpenClawRequirements(metadata, options = {}) {
|
|
68
|
+
if (!metadata?.requires) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const availableBins = options.availableBins ? new Set(options.availableBins) : null;
|
|
72
|
+
const bins = assessNamedRequirements(metadata.requires.bins, (name) => availableBins
|
|
73
|
+
? (availableBins.has(name) ? "satisfied" : "missing")
|
|
74
|
+
: (isExecutableInPath(name, options.path) ? "satisfied" : "missing"));
|
|
75
|
+
const anyBins = assessNamedRequirements(metadata.requires.anyBins, (name) => availableBins
|
|
76
|
+
? (availableBins.has(name) ? "satisfied" : "missing")
|
|
77
|
+
: (isExecutableInPath(name, options.path) ? "satisfied" : "missing"));
|
|
78
|
+
const env = assessNamedRequirements(metadata.requires.env, (name) => options.env && options.env[name] ? "satisfied" : "missing");
|
|
79
|
+
const config = assessConfigRequirements(metadata.requires.config, options.config);
|
|
80
|
+
const primaryEnv = metadata.requires.primaryEnv
|
|
81
|
+
? {
|
|
82
|
+
name: metadata.requires.primaryEnv,
|
|
83
|
+
status: (options.env && options.env[metadata.requires.primaryEnv] ? "satisfied" : "missing"),
|
|
84
|
+
}
|
|
85
|
+
: undefined;
|
|
86
|
+
const anyBinsSummary = anyBins.length === 0
|
|
87
|
+
? []
|
|
88
|
+
: [{
|
|
89
|
+
name: anyBins.map((item) => item.name).join(" | "),
|
|
90
|
+
status: (anyBins.some((item) => item.status === "satisfied") ? "satisfied" : "missing"),
|
|
91
|
+
}];
|
|
92
|
+
const summary = summarizeRequirementGroups([
|
|
93
|
+
bins,
|
|
94
|
+
anyBinsSummary,
|
|
95
|
+
env,
|
|
96
|
+
config,
|
|
97
|
+
...(primaryEnv ? [[primaryEnv]] : []),
|
|
98
|
+
]);
|
|
99
|
+
return {
|
|
100
|
+
...summary,
|
|
101
|
+
bins,
|
|
102
|
+
anyBins,
|
|
103
|
+
env,
|
|
104
|
+
config,
|
|
105
|
+
...(primaryEnv ? { primaryEnv } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function assessSkillRequirements(metadata, options = {}) {
|
|
109
|
+
return {
|
|
110
|
+
...(metadata.openclaw ? { openclaw: assessOpenClawRequirements(metadata.openclaw, options) } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function augmentExecutablePath(pathValue: string | undefined, env?: Record<string, string | undefined> | NodeJS.ProcessEnv): string;
|
|
2
|
+
export declare function createRuntimeEnv(env: Record<string, string> | undefined, baseEnv?: Record<string, string | undefined> | NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
const COMMON_EXECUTABLE_DIRS = [
|
|
5
|
+
"/opt/homebrew/bin",
|
|
6
|
+
"/opt/homebrew/sbin",
|
|
7
|
+
"/usr/local/bin",
|
|
8
|
+
"/usr/local/sbin",
|
|
9
|
+
"/usr/bin",
|
|
10
|
+
"/bin",
|
|
11
|
+
"/usr/sbin",
|
|
12
|
+
"/sbin",
|
|
13
|
+
];
|
|
14
|
+
function normalizeEnvRecord(env) {
|
|
15
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
16
|
+
}
|
|
17
|
+
function splitPathEntries(pathValue) {
|
|
18
|
+
return (pathValue ?? "")
|
|
19
|
+
.split(path.delimiter)
|
|
20
|
+
.map((entry) => entry.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
function discoverNvmExecutableDirs(home) {
|
|
24
|
+
const versionsRoot = path.join(home, ".nvm", "versions", "node");
|
|
25
|
+
if (!existsSync(versionsRoot)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return readdirSync(versionsRoot, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory())
|
|
30
|
+
.map((entry) => path.join(versionsRoot, entry.name, "bin"));
|
|
31
|
+
}
|
|
32
|
+
function defaultExecutableDirs(env) {
|
|
33
|
+
const home = env.HOME ?? env.USERPROFILE ?? homedir();
|
|
34
|
+
return [
|
|
35
|
+
...COMMON_EXECUTABLE_DIRS,
|
|
36
|
+
path.join(home, ".pyenv", "shims"),
|
|
37
|
+
path.join(home, ".asdf", "shims"),
|
|
38
|
+
...discoverNvmExecutableDirs(home),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export function augmentExecutablePath(pathValue, env = process.env) {
|
|
42
|
+
const deduped = new Set();
|
|
43
|
+
for (const entry of [...splitPathEntries(pathValue), ...defaultExecutableDirs(env)]) {
|
|
44
|
+
deduped.add(entry);
|
|
45
|
+
}
|
|
46
|
+
return Array.from(deduped).join(path.delimiter);
|
|
47
|
+
}
|
|
48
|
+
export function createRuntimeEnv(env, baseEnv = process.env) {
|
|
49
|
+
const base = normalizeEnvRecord(baseEnv);
|
|
50
|
+
const merged = {
|
|
51
|
+
...base,
|
|
52
|
+
...(env ?? {}),
|
|
53
|
+
};
|
|
54
|
+
const combinedPath = [base.PATH, env?.PATH].filter((value) => typeof value === "string").join(path.delimiter);
|
|
55
|
+
merged.PATH = augmentExecutablePath(combinedPath, { ...base, ...(env ?? {}) });
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
@@ -3,8 +3,21 @@ export type SkillMetadata = {
|
|
|
3
3
|
description?: string;
|
|
4
4
|
license?: string;
|
|
5
5
|
compatibility?: string;
|
|
6
|
-
metadata?: Record<string,
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
7
|
allowedTools?: string[];
|
|
8
|
+
userInvocable?: boolean;
|
|
9
|
+
openclaw?: OpenClawSkillMetadata;
|
|
10
|
+
};
|
|
11
|
+
export type OpenClawSkillRequirements = {
|
|
12
|
+
bins?: string[];
|
|
13
|
+
anyBins?: string[];
|
|
14
|
+
env?: string[];
|
|
15
|
+
config?: string[];
|
|
16
|
+
primaryEnv?: string;
|
|
17
|
+
};
|
|
18
|
+
export type OpenClawSkillMetadata = {
|
|
19
|
+
emoji?: string;
|
|
20
|
+
requires?: OpenClawSkillRequirements;
|
|
8
21
|
};
|
|
9
22
|
export declare function readSkillMetadata(skillPath: string): SkillMetadata;
|
|
10
23
|
export declare function validateSkillMetadata(skillPath: string): SkillMetadata;
|
|
@@ -11,33 +11,81 @@ function parseFrontmatterSource(document) {
|
|
|
11
11
|
function isRecord(value) {
|
|
12
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
13
|
}
|
|
14
|
+
function isJsonLikeValue(value) {
|
|
15
|
+
if (value === null ||
|
|
16
|
+
typeof value === "string" ||
|
|
17
|
+
typeof value === "number" ||
|
|
18
|
+
typeof value === "boolean") {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.every((item) => isJsonLikeValue(item));
|
|
23
|
+
}
|
|
24
|
+
if (isRecord(value)) {
|
|
25
|
+
return Object.values(value).every((item) => isJsonLikeValue(item));
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
14
29
|
function normalizeMetadata(value, strict) {
|
|
15
30
|
if (value === undefined) {
|
|
16
31
|
return undefined;
|
|
17
32
|
}
|
|
18
33
|
if (!isRecord(value)) {
|
|
19
34
|
if (strict) {
|
|
20
|
-
throw new Error("metadata must be a
|
|
35
|
+
throw new Error("metadata must be a JSON-like key-value map");
|
|
21
36
|
}
|
|
22
37
|
return undefined;
|
|
23
38
|
}
|
|
24
39
|
const normalized = Object.entries(value).reduce((acc, [key, item]) => {
|
|
25
|
-
if (
|
|
40
|
+
if (isJsonLikeValue(item)) {
|
|
26
41
|
acc[key] = item;
|
|
27
42
|
}
|
|
28
43
|
return acc;
|
|
29
44
|
}, {});
|
|
30
45
|
if (strict && Object.keys(normalized).length !== Object.keys(value).length) {
|
|
31
|
-
throw new Error("metadata values must
|
|
46
|
+
throw new Error("metadata values must be JSON-like");
|
|
32
47
|
}
|
|
33
48
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
34
49
|
}
|
|
50
|
+
function normalizeStringArray(value) {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
const tokens = value.split(/\s+/).filter(Boolean);
|
|
53
|
+
return tokens.length > 0 ? tokens : undefined;
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(value)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const tokens = value
|
|
59
|
+
.filter((item) => typeof item === "string")
|
|
60
|
+
.map((item) => item.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
return tokens.length > 0 ? tokens : undefined;
|
|
63
|
+
}
|
|
35
64
|
function toAllowedTools(value) {
|
|
36
|
-
|
|
65
|
+
const tools = normalizeStringArray(value);
|
|
66
|
+
return tools && tools.length > 0 ? tools : undefined;
|
|
67
|
+
}
|
|
68
|
+
function parseOpenClawMetadata(value) {
|
|
69
|
+
if (!isRecord(value)) {
|
|
37
70
|
return undefined;
|
|
38
71
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
72
|
+
const requires = isRecord(value.requires) ? value.requires : undefined;
|
|
73
|
+
const normalizedRequires = {
|
|
74
|
+
...(normalizeStringArray(requires?.bins) ? { bins: normalizeStringArray(requires?.bins) } : {}),
|
|
75
|
+
...(normalizeStringArray(requires?.anyBins) ? { anyBins: normalizeStringArray(requires?.anyBins) } : {}),
|
|
76
|
+
...(normalizeStringArray(requires?.env) ? { env: normalizeStringArray(requires?.env) } : {}),
|
|
77
|
+
...(normalizeStringArray(requires?.config) ? { config: normalizeStringArray(requires?.config) } : {}),
|
|
78
|
+
...(typeof value.primaryEnv === "string" && value.primaryEnv.trim()
|
|
79
|
+
? { primaryEnv: value.primaryEnv.trim() }
|
|
80
|
+
: typeof requires?.primaryEnv === "string" && requires.primaryEnv.trim()
|
|
81
|
+
? { primaryEnv: requires.primaryEnv.trim() }
|
|
82
|
+
: {}),
|
|
83
|
+
};
|
|
84
|
+
const normalized = {
|
|
85
|
+
...(typeof value.emoji === "string" && value.emoji.trim() ? { emoji: value.emoji } : {}),
|
|
86
|
+
...(Object.keys(normalizedRequires).length > 0 ? { requires: normalizedRequires } : {}),
|
|
87
|
+
};
|
|
88
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
41
89
|
}
|
|
42
90
|
function parseFrontmatter(document, strict = false) {
|
|
43
91
|
const source = parseFrontmatterSource(document);
|
|
@@ -55,6 +103,8 @@ function parseFrontmatter(document, strict = false) {
|
|
|
55
103
|
compatibility: typeof parsed.compatibility === "string" ? parsed.compatibility : undefined,
|
|
56
104
|
metadata: normalizeMetadata(parsed.metadata, strict),
|
|
57
105
|
allowedTools: toAllowedTools(parsed["allowed-tools"]),
|
|
106
|
+
userInvocable: typeof parsed["user-invocable"] === "boolean" ? parsed["user-invocable"] : undefined,
|
|
107
|
+
openclaw: parseOpenClawMetadata(isRecord(parsed.metadata) ? parsed.metadata.openclaw : undefined),
|
|
58
108
|
};
|
|
59
109
|
}
|
|
60
110
|
function isLegacyBuiltinSkillName(name) {
|
|
@@ -86,7 +136,7 @@ function validateMetadata(metadata, skillPath) {
|
|
|
86
136
|
return;
|
|
87
137
|
}
|
|
88
138
|
if (Object.keys(metadata).length === 0) {
|
|
89
|
-
throw new Error(`Skill ${skillPath} metadata must contain
|
|
139
|
+
throw new Error(`Skill ${skillPath} metadata must contain JSON-like key-value pairs when provided`);
|
|
90
140
|
}
|
|
91
141
|
}
|
|
92
142
|
function parseSkillMetadataFromDocument(document, skillPath, strict) {
|
|
@@ -104,6 +154,8 @@ function parseSkillMetadataFromDocument(document, skillPath, strict) {
|
|
|
104
154
|
compatibility: parsed.compatibility,
|
|
105
155
|
metadata: parsed.metadata,
|
|
106
156
|
allowedTools: parsed.allowedTools,
|
|
157
|
+
userInvocable: parsed.userInvocable,
|
|
158
|
+
openclaw: parsed.openclaw,
|
|
107
159
|
};
|
|
108
160
|
if (strict) {
|
|
109
161
|
validateSkillName(metadata.name, skillPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botbotgo/agent-harness",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.57",
|
|
4
4
|
"description": "Workspace runtime for multi-agent applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
|
|
52
52
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
53
|
-
"test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/stock-research-app-config.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/product-boundary-docs.test.ts test/runtime-adapter-regressions.test.ts test/runtime-capabilities.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts test/upstream-compat-regressions.test.ts test/yaml-format.test.ts",
|
|
53
|
+
"test": "vitest run test/hello-file.test.ts test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/stock-research-app-config.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/product-boundary-docs.test.ts test/docs-site.test.ts test/runtime-adapter-regressions.test.ts test/runtime-capabilities.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts test/upstream-compat-regressions.test.ts test/yaml-format.test.ts",
|
|
54
54
|
"test:real-providers": "vitest run test/real-provider-harness.test.ts",
|
|
55
55
|
"release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
|
|
56
56
|
"release:pack": "npm pack --dry-run",
|