@thinkwell/acp 0.5.4 → 0.5.6
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/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +184 -263
- package/dist/connection.js.map +1 -1
- package/dist/generated/features.d.ts +5 -0
- package/dist/generated/features.d.ts.map +1 -0
- package/dist/generated/features.js +4 -0
- package/dist/generated/features.js.map +1 -0
- package/dist/index.js +0 -7
- package/dist/json.js +0 -1
- package/dist/mcp-over-acp-handler.d.ts +3 -8
- package/dist/mcp-over-acp-handler.d.ts.map +1 -1
- package/dist/mcp-over-acp-handler.js +136 -180
- package/dist/mcp-over-acp-handler.js.map +1 -1
- package/dist/mcp-server.js +119 -143
- package/dist/session.js +113 -159
- package/dist/skill-server.js +54 -89
- package/dist/skill.js +41 -107
- package/dist/types.js +0 -7
- package/package.json +6 -5
package/dist/session.js
CHANGED
|
@@ -1,165 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Active session with an agent
|
|
3
|
-
*/
|
|
4
1
|
export class ActiveSession {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (this._closed) {
|
|
47
|
-
return { type: "stop", reason: "session_closed" };
|
|
48
|
-
}
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
this._updateResolvers.push(resolve);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Read all updates until completion, returning concatenated text
|
|
55
|
-
*/
|
|
56
|
-
async readToString() {
|
|
57
|
-
const parts = [];
|
|
58
|
-
while (true) {
|
|
59
|
-
const update = await this.readUpdate();
|
|
60
|
-
if (update.type === "text") {
|
|
61
|
-
parts.push(update.content);
|
|
62
|
-
}
|
|
63
|
-
else if (update.type === "stop") {
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
// tool_use updates are handled by the connection layer
|
|
67
|
-
}
|
|
68
|
-
return parts.join("");
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Mark the session as closed
|
|
72
|
-
*/
|
|
73
|
-
close() {
|
|
74
|
-
this._closed = true;
|
|
75
|
-
// Resolve any pending readers
|
|
76
|
-
for (const resolver of this._updateResolvers) {
|
|
77
|
-
resolver({ type: "stop", reason: "session_closed" });
|
|
78
|
-
}
|
|
79
|
-
this._updateResolvers = [];
|
|
2
|
+
sessionId;
|
|
3
|
+
_connection;
|
|
4
|
+
_mcpHandler;
|
|
5
|
+
_pendingUpdates = [];
|
|
6
|
+
_updateResolvers = [];
|
|
7
|
+
_closed = !1;
|
|
8
|
+
constructor(sessionId, connection, mcpHandler) {
|
|
9
|
+
this.sessionId = sessionId, this._connection = connection, this._mcpHandler = mcpHandler, this._mcpHandler.setSessionId(sessionId);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Send a prompt to the agent
|
|
13
|
+
*/
|
|
14
|
+
async sendPrompt(content) {
|
|
15
|
+
const response = await this._connection.sendPrompt(this.sessionId, content);
|
|
16
|
+
response.stopReason && this.pushUpdate({ type: "stop", reason: response.stopReason });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Push an update received from the agent
|
|
20
|
+
*/
|
|
21
|
+
pushUpdate(update) {
|
|
22
|
+
this._updateResolvers.length > 0 ? this._updateResolvers.shift()(update) : this._pendingUpdates.push(update);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read the next update from the agent
|
|
26
|
+
*/
|
|
27
|
+
async readUpdate() {
|
|
28
|
+
return this._pendingUpdates.length > 0 ? this._pendingUpdates.shift() : this._closed ? { type: "stop", reason: "session_closed" } : new Promise((resolve) => {
|
|
29
|
+
this._updateResolvers.push(resolve);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read all updates until completion, returning concatenated text
|
|
34
|
+
*/
|
|
35
|
+
async readToString() {
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (; ; ) {
|
|
38
|
+
const update = await this.readUpdate();
|
|
39
|
+
if (update.type === "text")
|
|
40
|
+
parts.push(update.content);
|
|
41
|
+
else if (update.type === "stop")
|
|
42
|
+
break;
|
|
80
43
|
}
|
|
44
|
+
return parts.join("");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Mark the session as closed
|
|
48
|
+
*/
|
|
49
|
+
close() {
|
|
50
|
+
this._closed = !0;
|
|
51
|
+
for (const resolver of this._updateResolvers)
|
|
52
|
+
resolver({ type: "stop", reason: "session_closed" });
|
|
53
|
+
this._updateResolvers = [];
|
|
54
|
+
}
|
|
81
55
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Builder for creating ACP sessions with MCP servers
|
|
84
|
-
*/
|
|
85
56
|
export class SessionBuilder {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Wait for MCP tools discovery if we have MCP servers attached
|
|
147
|
-
const shouldWait = this._mcpServers.length > 0 &&
|
|
148
|
-
(this._mcpReadyOptions.enabled !== false);
|
|
149
|
-
if (shouldWait) {
|
|
150
|
-
const timeout = this._mcpReadyOptions.timeout ?? 2000;
|
|
151
|
-
await this._mcpHandler.waitForToolsDiscovery(sessionId, timeout);
|
|
152
|
-
}
|
|
153
|
-
return await callback(session);
|
|
154
|
-
}
|
|
155
|
-
finally {
|
|
156
|
-
session.close();
|
|
157
|
-
this._connection.removeSessionHandler(sessionId);
|
|
158
|
-
// Unregister MCP servers
|
|
159
|
-
for (const server of this._mcpServers) {
|
|
160
|
-
this._mcpHandler.unregister(server);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
57
|
+
_connection;
|
|
58
|
+
_mcpHandler;
|
|
59
|
+
_mcpServers = [];
|
|
60
|
+
_cwd;
|
|
61
|
+
_systemPrompt;
|
|
62
|
+
_mcpReadyOptions = {};
|
|
63
|
+
constructor(connection, mcpHandler) {
|
|
64
|
+
this._connection = connection, this._mcpHandler = mcpHandler;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Attach an MCP server to this session
|
|
68
|
+
*/
|
|
69
|
+
withMcpServer(server) {
|
|
70
|
+
return this._mcpServers.push(server), this._mcpHandler.register(server), this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Set the working directory for the session
|
|
74
|
+
*/
|
|
75
|
+
cwd(path) {
|
|
76
|
+
return this._cwd = path, this;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Set the system prompt for the session
|
|
80
|
+
*/
|
|
81
|
+
systemPrompt(prompt) {
|
|
82
|
+
return this._systemPrompt = prompt, this;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Configure waiting for MCP tools discovery.
|
|
86
|
+
*
|
|
87
|
+
* By default, when MCP servers are attached, the session will wait for
|
|
88
|
+
* the agent to call tools/list before invoking the callback. This prevents
|
|
89
|
+
* a race condition where the prompt is sent before the agent knows about
|
|
90
|
+
* available tools.
|
|
91
|
+
*
|
|
92
|
+
* @param options - Options for MCP readiness waiting
|
|
93
|
+
*/
|
|
94
|
+
waitForMcpReady(options = {}) {
|
|
95
|
+
return this._mcpReadyOptions = options, this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Start the session and run a callback
|
|
99
|
+
*/
|
|
100
|
+
async run(callback) {
|
|
101
|
+
const sessionId = await this._connection.createSession({
|
|
102
|
+
cwd: this._cwd,
|
|
103
|
+
systemPrompt: this._systemPrompt,
|
|
104
|
+
mcpServers: this._mcpServers.map((s) => s.toSessionConfig())
|
|
105
|
+
}), session = new ActiveSession(sessionId, this._connection, this._mcpHandler);
|
|
106
|
+
this._connection.setSessionHandler(sessionId, session);
|
|
107
|
+
try {
|
|
108
|
+
if (this._mcpServers.length > 0 && this._mcpReadyOptions.enabled !== !1) {
|
|
109
|
+
const timeout = this._mcpReadyOptions.timeout ?? 2e3;
|
|
110
|
+
await this._mcpHandler.waitForToolsDiscovery(sessionId, timeout);
|
|
111
|
+
}
|
|
112
|
+
return await callback(session);
|
|
113
|
+
} finally {
|
|
114
|
+
session.close(), this._connection.removeSessionHandler(sessionId);
|
|
115
|
+
for (const server of this._mcpServers)
|
|
116
|
+
this._mcpHandler.unregister(server);
|
|
163
117
|
}
|
|
118
|
+
}
|
|
164
119
|
}
|
|
165
|
-
//# sourceMappingURL=session.js.map
|
package/dist/skill-server.js
CHANGED
|
@@ -1,96 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill MCP server builder.
|
|
3
|
-
*
|
|
4
|
-
* Creates an MCP server that exposes three tools for skill interaction:
|
|
5
|
-
* - activate_skill: returns a skill's instruction body
|
|
6
|
-
* - call_skill_tool: dispatches to a virtual skill's tool handler
|
|
7
|
-
* - read_skill_file: reads a file from a stored skill's basePath
|
|
8
|
-
*
|
|
9
|
-
* These tools are registered as standard MCP tools but are intended to be
|
|
10
|
-
* hidden from the prompt (via Plan's defineTool pattern).
|
|
11
|
-
*/
|
|
12
1
|
import { readFile } from "node:fs/promises";
|
|
13
2
|
import { resolve, relative, isAbsolute } from "node:path";
|
|
14
3
|
import { mcpServer } from "./mcp-server.js";
|
|
15
|
-
/** Type guard for StoredSkill (has basePath). */
|
|
16
4
|
function isStoredSkill(skill) {
|
|
17
|
-
|
|
5
|
+
return "basePath" in skill && typeof skill.basePath == "string";
|
|
18
6
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Build an MCP server that provides skill tools.
|
|
21
|
-
*
|
|
22
|
-
* @param skills - resolved skills (virtual or stored) to make available
|
|
23
|
-
* @returns an McpServer with activate_skill, call_skill_tool, and read_skill_file tools
|
|
24
|
-
*/
|
|
25
7
|
export function createSkillServer(skills) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!isStoredSkill(skill)) {
|
|
80
|
-
throw new Error(`Skill "${input.skill_name}" is not a stored skill (no basePath)`);
|
|
81
|
-
}
|
|
82
|
-
// Reject absolute paths outright
|
|
83
|
-
if (isAbsolute(input.path)) {
|
|
84
|
-
throw new Error("Path must be relative");
|
|
85
|
-
}
|
|
86
|
-
const resolved = resolve(skill.basePath, input.path);
|
|
87
|
-
// Path traversal check: resolved path must be within basePath
|
|
88
|
-
const rel = relative(skill.basePath, resolved);
|
|
89
|
-
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
90
|
-
throw new Error("Path traversal is not allowed");
|
|
91
|
-
}
|
|
92
|
-
return readFile(resolved, "utf-8");
|
|
93
|
-
})
|
|
94
|
-
.build();
|
|
8
|
+
const skillsByName = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const skill of skills)
|
|
10
|
+
skillsByName.set(skill.name, skill);
|
|
11
|
+
return mcpServer("skills").tool("activate_skill", "Activate a skill by name, returning its full instructions.", {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
skill_name: { type: "string", description: "The name of the skill to activate" }
|
|
15
|
+
},
|
|
16
|
+
required: ["skill_name"]
|
|
17
|
+
}, { type: "string" }, async (input) => {
|
|
18
|
+
const skill = skillsByName.get(input.skill_name);
|
|
19
|
+
if (!skill)
|
|
20
|
+
throw new Error(`Unknown skill: "${input.skill_name}"`);
|
|
21
|
+
return skill.body;
|
|
22
|
+
}).tool("call_skill_tool", "Call a tool provided by a skill.", {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
skill_name: { type: "string", description: "The name of the skill that provides the tool" },
|
|
26
|
+
tool_name: { type: "string", description: "The name of the tool to call" },
|
|
27
|
+
input: { description: "Input to pass to the tool handler" }
|
|
28
|
+
},
|
|
29
|
+
required: ["skill_name", "tool_name"]
|
|
30
|
+
}, {}, async (input) => {
|
|
31
|
+
const skill = skillsByName.get(input.skill_name);
|
|
32
|
+
if (!skill)
|
|
33
|
+
throw new Error(`Unknown skill: "${input.skill_name}"`);
|
|
34
|
+
const tools = skill.tools;
|
|
35
|
+
if (!tools || tools.length === 0)
|
|
36
|
+
throw new Error(`Skill "${input.skill_name}" has no tools`);
|
|
37
|
+
const tool = tools.find((t) => t.name === input.tool_name);
|
|
38
|
+
if (!tool)
|
|
39
|
+
throw new Error(`Unknown tool "${input.tool_name}" for skill "${input.skill_name}"`);
|
|
40
|
+
return tool.handler(input.input);
|
|
41
|
+
}).tool("read_skill_file", "Read a file from a stored skill's directory.", {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
skill_name: { type: "string", description: "The name of the skill" },
|
|
45
|
+
path: { type: "string", description: "Relative path to the file within the skill directory" }
|
|
46
|
+
},
|
|
47
|
+
required: ["skill_name", "path"]
|
|
48
|
+
}, { type: "string" }, async (input) => {
|
|
49
|
+
const skill = skillsByName.get(input.skill_name);
|
|
50
|
+
if (!skill)
|
|
51
|
+
throw new Error(`Unknown skill: "${input.skill_name}"`);
|
|
52
|
+
if (!isStoredSkill(skill))
|
|
53
|
+
throw new Error(`Skill "${input.skill_name}" is not a stored skill (no basePath)`);
|
|
54
|
+
if (isAbsolute(input.path))
|
|
55
|
+
throw new Error("Path must be relative");
|
|
56
|
+
const resolved = resolve(skill.basePath, input.path), rel = relative(skill.basePath, resolved);
|
|
57
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
58
|
+
throw new Error("Path traversal is not allowed");
|
|
59
|
+
return readFile(resolved, "utf-8");
|
|
60
|
+
}).build();
|
|
95
61
|
}
|
|
96
|
-
//# sourceMappingURL=skill-server.js.map
|
package/dist/skill.js
CHANGED
|
@@ -1,117 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
* Skill types and SKILL.md parser for the Agent Skills standard.
|
|
3
|
-
*
|
|
4
|
-
* This module defines the core skill types and provides a parser for
|
|
5
|
-
* SKILL.md files (YAML frontmatter + Markdown body).
|
|
6
|
-
*/
|
|
7
|
-
/** Validation pattern for skill names: lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens. */
|
|
8
|
-
const SKILL_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*$/;
|
|
9
|
-
/** Maximum length for skill names. */
|
|
10
|
-
const MAX_NAME_LENGTH = 64;
|
|
11
|
-
/** Maximum length for skill descriptions. */
|
|
12
|
-
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
13
|
-
/**
|
|
14
|
-
* Validate a skill name per the Agent Skills spec.
|
|
15
|
-
* - 1-64 characters
|
|
16
|
-
* - Lowercase alphanumeric + hyphens
|
|
17
|
-
* - No leading, trailing, or consecutive hyphens
|
|
18
|
-
*/
|
|
1
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*$/, MAX_NAME_LENGTH = 64, MAX_DESCRIPTION_LENGTH = 1024;
|
|
19
2
|
export function validateSkillName(name) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
27
|
-
throw new Error(`Invalid skill name "${name}": must be lowercase alphanumeric with hyphens, no leading/trailing/consecutive hyphens`);
|
|
28
|
-
}
|
|
3
|
+
if (typeof name != "string" || name.length === 0)
|
|
4
|
+
throw new Error("Skill name is required");
|
|
5
|
+
if (name.length > 64)
|
|
6
|
+
throw new Error(`Skill name must be at most 64 characters, got ${name.length}`);
|
|
7
|
+
if (!SKILL_NAME_PATTERN.test(name))
|
|
8
|
+
throw new Error(`Invalid skill name "${name}": must be lowercase alphanumeric with hyphens, no leading/trailing/consecutive hyphens`);
|
|
29
9
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Validate a skill description per the Agent Skills spec.
|
|
32
|
-
* - 1-1024 characters
|
|
33
|
-
* - Non-empty (after trimming)
|
|
34
|
-
*/
|
|
35
10
|
export function validateSkillDescription(description) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
throw new Error(`Skill description must be at most ${MAX_DESCRIPTION_LENGTH} characters, got ${description.length}`);
|
|
41
|
-
}
|
|
11
|
+
if (typeof description != "string" || description.trim().length === 0)
|
|
12
|
+
throw new Error("Skill description is required");
|
|
13
|
+
if (description.length > 1024)
|
|
14
|
+
throw new Error(`Skill description must be at most 1024 characters, got ${description.length}`);
|
|
42
15
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Parse a SKILL.md file content into a Skill.
|
|
45
|
-
*
|
|
46
|
-
* Extracts YAML frontmatter (delimited by `---`), validates required fields
|
|
47
|
-
* (name, description), and returns the parsed skill with the Markdown body.
|
|
48
|
-
*
|
|
49
|
-
* Optional frontmatter fields (license, compatibility, metadata) are preserved
|
|
50
|
-
* in the returned object but not acted upon.
|
|
51
|
-
*
|
|
52
|
-
* @throws Error if frontmatter is missing, malformed, or required fields are invalid
|
|
53
|
-
*/
|
|
54
16
|
export function parseSkillMd(content) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
description: fields.description,
|
|
63
|
-
body,
|
|
64
|
-
};
|
|
17
|
+
const { frontmatter, body } = extractFrontmatter(content), fields = parseYamlFrontmatter(frontmatter);
|
|
18
|
+
return validateSkillName(fields.name), validateSkillDescription(fields.description), {
|
|
19
|
+
...fields,
|
|
20
|
+
name: fields.name,
|
|
21
|
+
description: fields.description,
|
|
22
|
+
body
|
|
23
|
+
};
|
|
65
24
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Extract YAML frontmatter and body from a SKILL.md string.
|
|
68
|
-
* Frontmatter is delimited by opening and closing `---` lines.
|
|
69
|
-
*/
|
|
70
25
|
function extractFrontmatter(content) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const frontmatter = trimmed.slice(3, endIndex).trim();
|
|
81
|
-
// Body starts after the closing `---` and its newline
|
|
82
|
-
const afterClosing = endIndex + 4; // "\n---".length
|
|
83
|
-
// Strip the line ending after the closing --- and any single blank line
|
|
84
|
-
const body = trimmed.slice(afterClosing).replace(/^(\r?\n){1,2}/, "");
|
|
85
|
-
return { frontmatter, body };
|
|
26
|
+
const trimmed = content.trimStart();
|
|
27
|
+
if (!trimmed.startsWith("---"))
|
|
28
|
+
throw new Error("SKILL.md must begin with YAML frontmatter (---)");
|
|
29
|
+
const endIndex = trimmed.indexOf(`
|
|
30
|
+
---`, 3);
|
|
31
|
+
if (endIndex === -1)
|
|
32
|
+
throw new Error("SKILL.md frontmatter is missing closing ---");
|
|
33
|
+
const frontmatter = trimmed.slice(3, endIndex).trim(), afterClosing = endIndex + 4, body = trimmed.slice(afterClosing).replace(/^(\r?\n){1,2}/, "");
|
|
34
|
+
return { frontmatter, body };
|
|
86
35
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Minimal YAML parser for SKILL.md frontmatter.
|
|
89
|
-
*
|
|
90
|
-
* Supports only the flat key-value structure needed for skill metadata:
|
|
91
|
-
* simple string values (quoted or unquoted). This is intentionally limited --
|
|
92
|
-
* we don't need nested objects, arrays, or other YAML features.
|
|
93
|
-
*/
|
|
94
36
|
function parseYamlFrontmatter(yaml) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
110
|
-
value = value.slice(1, -1);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
result[key] = value;
|
|
114
|
-
}
|
|
115
|
-
return result;
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const line of yaml.split(`
|
|
39
|
+
`)) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
42
|
+
continue;
|
|
43
|
+
const colonIndex = trimmed.indexOf(":");
|
|
44
|
+
if (colonIndex === -1)
|
|
45
|
+
throw new Error(`Invalid frontmatter line: ${trimmed}`);
|
|
46
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
47
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
48
|
+
typeof value == "string" && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) && (value = value.slice(1, -1)), result[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
116
51
|
}
|
|
117
|
-
//# sourceMappingURL=skill.js.map
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkwell/acp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "TypeScript implementation of ACP (Agent Client Protocol) extensions for Thinkwell",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"author": "David Herman",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@agentclientprotocol/sdk": "^0.
|
|
28
|
+
"@agentclientprotocol/sdk": "^0.16.1",
|
|
29
29
|
"uuid": "^11.0.3",
|
|
30
|
-
"@thinkwell/conductor": "0.5.
|
|
31
|
-
"@thinkwell/protocol": "0.5.
|
|
30
|
+
"@thinkwell/conductor": "0.5.6",
|
|
31
|
+
"@thinkwell/protocol": "0.5.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^24.10.4",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"typescript": "^5.7.2"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsc",
|
|
40
|
+
"build": "tsc && tsx ../../scripts/strip-features.ts --mode=release",
|
|
41
|
+
"build:debug": "tsc",
|
|
41
42
|
"clean": "rm -rf dist",
|
|
42
43
|
"test": "node --test --import tsx src/**/*.test.ts"
|
|
43
44
|
}
|