skyloom 1.24.0 → 1.26.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.
- package/.env.example +90 -0
- package/.github/workflows/ci.yml +3 -0
- package/CONVERSION_PLAN.md +5 -0
- package/LICENSE +21 -0
- package/README.md +4 -4
- package/dist/cli/main.js +2 -22
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +0 -6
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/diff.d.ts.map +1 -1
- package/dist/core/diff.js +0 -1
- package/dist/core/diff.js.map +1 -1
- package/dist/core/evolve.d.ts.map +1 -1
- package/dist/core/evolve.js +0 -9
- package/dist/core/evolve.js.map +1 -1
- package/dist/core/graph.d.ts +1 -1
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +1 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/llm.d.ts +3 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +4 -27
- package/dist/core/llm.js.map +1 -1
- package/dist/core/mcp.d.ts +25 -1
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +175 -8
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandbox.js +0 -2
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/schemas.d.ts +1 -1
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +1 -23
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +0 -1
- package/dist/core/skill.js.map +1 -1
- package/dist/gateway/qr.d.ts.map +1 -1
- package/dist/gateway/qr.js +0 -1
- package/dist/gateway/qr.js.map +1 -1
- package/dist/tools/computer.d.ts.map +1 -1
- package/dist/tools/computer.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +0 -2
- package/dist/web/server.js.map +1 -1
- package/eslint.config.js +62 -0
- package/package.json +1 -1
- package/src/cli/main.ts +3 -22
- package/src/cli/tui.ts +0 -2
- package/src/core/diff.ts +0 -1
- package/src/core/evolve.ts +0 -6
- package/src/core/graph.ts +1 -1
- package/src/core/llm.ts +4 -33
- package/src/core/mcp.ts +185 -8
- package/src/core/sandbox.ts +1 -4
- package/src/core/schemas.ts +1 -25
- package/src/core/skill.ts +0 -1
- package/src/gateway/qr.ts +0 -1
- package/src/tools/computer.ts +2 -2
- package/src/web/server.ts +0 -3
- package/tests/factory.test.ts +56 -0
- package/tests/mcp_sse.test.ts +112 -0
- package/tests/pipelines.test.ts +118 -0
- package/tests/skill.test.ts +6 -5
- package/skill-test-ty2fOA/test.md +0 -10
package/src/tools/computer.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* processes and services, and install/uninstall software.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { execSync, execFileSync
|
|
8
|
+
import { execSync, execFileSync } from 'child_process';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import type { ToolRegistry
|
|
12
|
+
import type { ToolRegistry } from '../core/tool';
|
|
13
13
|
|
|
14
14
|
const MAX_OUT = 8000;
|
|
15
15
|
|
package/src/web/server.ts
CHANGED
|
@@ -8,11 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { createServer, IncomingMessage, ServerResponse } from "http";
|
|
10
10
|
import { createSystemContext } from "../core/factory";
|
|
11
|
-
import { getLogger } from "../core/logger";
|
|
12
11
|
import { renderInkWashUI, SKYLOOM_FAVICON_SVG } from "./ui";
|
|
13
12
|
|
|
14
|
-
const log = getLogger("web-server");
|
|
15
|
-
|
|
16
13
|
/* ──────────────────────────────────────────────
|
|
17
14
|
Server
|
|
18
15
|
────────────────────────────────────────────── */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { SystemContext, TaskExecutionResult } from "../src/core/factory";
|
|
3
|
+
import { MessageBus } from "../src/core/bus";
|
|
4
|
+
|
|
5
|
+
describe("factory · TaskExecutionResult", () => {
|
|
6
|
+
it("holds the provided fields", () => {
|
|
7
|
+
const r = new TaskExecutionResult({ id: "t1", agent: "fog", description: "do x", success: true, content: "done" });
|
|
8
|
+
expect(r).toMatchObject({ id: "t1", agent: "fog", success: true, content: "done" });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("factory · SystemContext", () => {
|
|
13
|
+
function ctx(overrides: Partial<ConstructorParameters<typeof SystemContext>[0]> = {}) {
|
|
14
|
+
return new SystemContext({
|
|
15
|
+
config: { agents: {} } as any,
|
|
16
|
+
bus: new MessageBus(),
|
|
17
|
+
llm: {} as any,
|
|
18
|
+
agentMap: new Map(),
|
|
19
|
+
toolRegistry: {} as any,
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it("stores constructor options with sensible defaults", () => {
|
|
25
|
+
const c = ctx({ workspacePath: "/ws" });
|
|
26
|
+
expect(c.workspacePath).toBe("/ws");
|
|
27
|
+
expect(c.mcp).toBeNull();
|
|
28
|
+
expect(c.mcpStatus).toEqual([]);
|
|
29
|
+
expect(c.agentMap.size).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("initAll fires the plugin init hook before agents come up", async () => {
|
|
33
|
+
const emit = vi.fn().mockResolvedValue(undefined);
|
|
34
|
+
const c = ctx({ plugins: { emit } as any });
|
|
35
|
+
await c.initAll();
|
|
36
|
+
expect(emit).toHaveBeenCalledWith("init", expect.objectContaining({ config: expect.anything() }));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("closeAll closes every agent and tolerates a missing mcp", async () => {
|
|
40
|
+
const close = vi.fn().mockResolvedValue(undefined);
|
|
41
|
+
const agents = new Map<string, any>([
|
|
42
|
+
["fog", { close }],
|
|
43
|
+
["rain", { close }],
|
|
44
|
+
]);
|
|
45
|
+
const c = ctx({ agentMap: agents as any });
|
|
46
|
+
await c.closeAll();
|
|
47
|
+
expect(close).toHaveBeenCalledTimes(2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("closeAll also closes mcp when present", async () => {
|
|
51
|
+
const closeAll = vi.fn().mockResolvedValue(undefined);
|
|
52
|
+
const c = ctx({ mcp: { closeAll } as any });
|
|
53
|
+
await c.closeAll();
|
|
54
|
+
expect(closeAll).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SSE transport — integration test against a local mock SSE server.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the full handshake: GET event-stream → `endpoint` event →
|
|
5
|
+
* initialize → tools/list → tools/call, with every response delivered back
|
|
6
|
+
* over the persistent SSE stream and correlated by JSON-RPC id.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
9
|
+
import * as http from "http";
|
|
10
|
+
import { AddressInfo } from "net";
|
|
11
|
+
import { MCPClient } from "../src/core/mcp";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A minimal MCP-over-SSE server: one event-stream connection, a POST endpoint
|
|
15
|
+
* that echoes JSON-RPC responses back over that stream.
|
|
16
|
+
*/
|
|
17
|
+
function startMockSSEServer(): Promise<{ url: string; close: () => void }> {
|
|
18
|
+
let sseRes: http.ServerResponse | null = null;
|
|
19
|
+
|
|
20
|
+
const server = http.createServer((req, res) => {
|
|
21
|
+
if (req.method === "GET") {
|
|
22
|
+
res.writeHead(200, {
|
|
23
|
+
"Content-Type": "text/event-stream",
|
|
24
|
+
"Cache-Control": "no-cache",
|
|
25
|
+
Connection: "keep-alive",
|
|
26
|
+
});
|
|
27
|
+
sseRes = res;
|
|
28
|
+
// Advertise the message endpoint (relative path, resolved by the client).
|
|
29
|
+
res.write("event: endpoint\ndata: /messages\n\n");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (req.method === "POST") {
|
|
34
|
+
let body = "";
|
|
35
|
+
req.on("data", (c) => (body += c));
|
|
36
|
+
req.on("end", () => {
|
|
37
|
+
res.writeHead(202).end();
|
|
38
|
+
const msg = JSON.parse(body);
|
|
39
|
+
if (msg.id === undefined || msg.id === null) return; // notification
|
|
40
|
+
const reply = (result: unknown) => {
|
|
41
|
+
sseRes?.write(
|
|
42
|
+
`event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", id: msg.id, result })}\n\n`
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
if (msg.method === "initialize") {
|
|
46
|
+
reply({ protocolVersion: "2025-03-26", capabilities: {} });
|
|
47
|
+
} else if (msg.method === "tools/list") {
|
|
48
|
+
reply({ tools: [{ name: "echo", description: "echo back" }] });
|
|
49
|
+
} else if (msg.method === "tools/call") {
|
|
50
|
+
reply({ content: [{ type: "text", text: `hi ${msg.params.arguments.who}` }] });
|
|
51
|
+
} else if (msg.method === "ping") {
|
|
52
|
+
reply({});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.writeHead(404).end();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
server.listen(0, "127.0.0.1", () => {
|
|
63
|
+
const port = (server.address() as AddressInfo).port;
|
|
64
|
+
resolve({
|
|
65
|
+
url: `http://127.0.0.1:${port}/sse`,
|
|
66
|
+
close: () => {
|
|
67
|
+
sseRes?.end();
|
|
68
|
+
server.close();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("MCP SSE transport", () => {
|
|
76
|
+
let mock: { url: string; close: () => void } | null = null;
|
|
77
|
+
let client: MCPClient | null = null;
|
|
78
|
+
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
await client?.close();
|
|
81
|
+
mock?.close();
|
|
82
|
+
client = null;
|
|
83
|
+
mock = null;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("completes the endpoint handshake and lists tools", async () => {
|
|
87
|
+
mock = await startMockSSEServer();
|
|
88
|
+
client = new MCPClient({ name: "mock", url: mock.url });
|
|
89
|
+
|
|
90
|
+
const tools = await client.initialize();
|
|
91
|
+
expect(tools.map((t) => t.name)).toEqual(["echo"]);
|
|
92
|
+
expect(client.getToolDefinitions()).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("calls a tool and extracts the text result over the stream", async () => {
|
|
96
|
+
mock = await startMockSSEServer();
|
|
97
|
+
client = new MCPClient({ name: "mock", url: mock.url });
|
|
98
|
+
|
|
99
|
+
await client.initialize();
|
|
100
|
+
const result = await client.callTool("echo", { who: "skyloom" });
|
|
101
|
+
expect(result).toBe("hi skyloom");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reports healthy after the handshake via an SSE ping round-trip", async () => {
|
|
105
|
+
mock = await startMockSSEServer();
|
|
106
|
+
client = new MCPClient({ name: "mock", url: mock.url });
|
|
107
|
+
|
|
108
|
+
await client.initialize();
|
|
109
|
+
const health = await client.healthCheck();
|
|
110
|
+
expect(health.healthy).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
matchPipeline,
|
|
4
|
+
matchAllPipelines,
|
|
5
|
+
buildTasksFromPipeline,
|
|
6
|
+
listPipelines,
|
|
7
|
+
getPipelineByName,
|
|
8
|
+
validateDAG,
|
|
9
|
+
topologicalSort,
|
|
10
|
+
type Task,
|
|
11
|
+
} from "../src/core/pipelines";
|
|
12
|
+
|
|
13
|
+
function task(id: string, dependsOn: string[] = []): Task {
|
|
14
|
+
return { id, description: id, assignedTo: "fog", parentId: dependsOn[0] ?? null, dependsOn };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("pipelines · matching", () => {
|
|
18
|
+
it("matches a code-review goal to the code_review pipeline", () => {
|
|
19
|
+
const p = matchPipeline("帮我审查代码");
|
|
20
|
+
expect(p?.name).toBe("code_review");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("matches an English review goal", () => {
|
|
24
|
+
expect(matchPipeline("please review my code")?.name).toBe("code_review");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns null for an unmatched / empty goal", () => {
|
|
28
|
+
expect(matchPipeline("今天天气怎么样")).toBeNull();
|
|
29
|
+
expect(matchPipeline("")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("matchAllPipelines returns an array (possibly empty)", () => {
|
|
33
|
+
expect(Array.isArray(matchAllPipelines("审查代码"))).toBe(true);
|
|
34
|
+
expect(matchAllPipelines("xyzzy nonsense")).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("pipelines · introspection", () => {
|
|
39
|
+
it("lists pipelines with names, triggers and steps", () => {
|
|
40
|
+
const list = listPipelines();
|
|
41
|
+
expect(list.length).toBeGreaterThan(0);
|
|
42
|
+
for (const p of list) {
|
|
43
|
+
expect(typeof p.name).toBe("string");
|
|
44
|
+
expect(Array.isArray(p.triggers)).toBe(true);
|
|
45
|
+
expect(Array.isArray(p.steps)).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("getPipelineByName round-trips a listed name; unknown → null", () => {
|
|
50
|
+
const name = listPipelines()[0].name as string;
|
|
51
|
+
expect(getPipelineByName(name)?.name).toBe(name);
|
|
52
|
+
expect(getPipelineByName("___nope___")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("pipelines · materialization", () => {
|
|
57
|
+
it("builds tasks from a pipeline, substituting {goal}", () => {
|
|
58
|
+
const p = getPipelineByName("code_review")!;
|
|
59
|
+
const tasks = buildTasksFromPipeline(p, "登录模块");
|
|
60
|
+
expect(tasks.length).toBe(p.steps.length);
|
|
61
|
+
expect(tasks[0].description).toContain("登录模块");
|
|
62
|
+
expect(tasks[0].metadata?.goal).toBe("登录模块");
|
|
63
|
+
expect(tasks[0].metadata?.pipeline).toBe("code_review");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("a materialized pipeline is a valid DAG", () => {
|
|
67
|
+
for (const meta of listPipelines()) {
|
|
68
|
+
const p = getPipelineByName(meta.name as string)!;
|
|
69
|
+
const tasks = buildTasksFromPipeline(p, "x");
|
|
70
|
+
const v = validateDAG(tasks);
|
|
71
|
+
expect(v.valid, `${meta.name}: ${v.errors.join("; ")}`).toBe(true);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("pipelines · validateDAG", () => {
|
|
77
|
+
it("accepts a linear chain", () => {
|
|
78
|
+
const v = validateDAG([task("1"), task("2", ["1"]), task("3", ["2"])]);
|
|
79
|
+
expect(v.valid).toBe(true);
|
|
80
|
+
expect(v.errors).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("flags a missing dependency", () => {
|
|
84
|
+
const v = validateDAG([task("2", ["1"])]); // 1 doesn't exist
|
|
85
|
+
expect(v.valid).toBe(false);
|
|
86
|
+
expect(v.errors.join(" ")).toMatch(/non-existent/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("detects a cycle", () => {
|
|
90
|
+
const v = validateDAG([task("a", ["b"]), task("b", ["a"])]);
|
|
91
|
+
expect(v.valid).toBe(false);
|
|
92
|
+
expect(v.errors.join(" ")).toMatch(/[Cc]ycle/);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("pipelines · topologicalSort", () => {
|
|
97
|
+
it("orders dependencies before dependents", () => {
|
|
98
|
+
const sorted = topologicalSort([task("3", ["2"]), task("1"), task("2", ["1"])]);
|
|
99
|
+
const order = sorted.map((t) => t.id);
|
|
100
|
+
expect(order.indexOf("1")).toBeLessThan(order.indexOf("2"));
|
|
101
|
+
expect(order.indexOf("2")).toBeLessThan(order.indexOf("3"));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles independent tasks (all in-degree 0)", () => {
|
|
105
|
+
const sorted = topologicalSort([task("a"), task("b"), task("c")]);
|
|
106
|
+
expect(sorted.map((t) => t.id).sort()).toEqual(["a", "b", "c"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("a diamond DAG keeps the root first and the join last", () => {
|
|
110
|
+
// a → b, a → c, b → d, c → d
|
|
111
|
+
const sorted = topologicalSort([
|
|
112
|
+
task("d", ["b", "c"]), task("b", ["a"]), task("c", ["a"]), task("a"),
|
|
113
|
+
]);
|
|
114
|
+
const order = sorted.map((t) => t.id);
|
|
115
|
+
expect(order[0]).toBe("a");
|
|
116
|
+
expect(order[order.length - 1]).toBe("d");
|
|
117
|
+
});
|
|
118
|
+
});
|
package/tests/skill.test.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { describe, it, expect } from 'vitest';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
7
8
|
|
|
8
9
|
describe('Skill', () => {
|
|
9
10
|
it('creates a skill with model override', async () => {
|
|
@@ -42,7 +43,7 @@ describe('Skill', () => {
|
|
|
42
43
|
describe('Skill.fromMarkdown', () => {
|
|
43
44
|
it('loads skill with YAML frontmatter', async () => {
|
|
44
45
|
const { Skill } = await import('../src/core/skill');
|
|
45
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
46
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-test-'));
|
|
46
47
|
const mdPath = path.join(tmpDir, 'test.md');
|
|
47
48
|
fs.writeFileSync(mdPath, `---
|
|
48
49
|
name: test_skill
|
|
@@ -69,7 +70,7 @@ This is a test skill.
|
|
|
69
70
|
|
|
70
71
|
it('supports string path', async () => {
|
|
71
72
|
const { Skill } = await import('../src/core/skill');
|
|
72
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
73
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-test-'));
|
|
73
74
|
const mdPath = path.join(tmpDir, 'x.md');
|
|
74
75
|
fs.writeFileSync(mdPath, '---\nname: x\ndescription: x\n---\n\nBody.', 'utf-8');
|
|
75
76
|
const s = Skill.fromMarkdown(mdPath);
|
|
@@ -83,7 +84,7 @@ This is a test skill.
|
|
|
83
84
|
|
|
84
85
|
it('derives triggers from quoted descriptions', async () => {
|
|
85
86
|
const { Skill } = await import('../src/core/skill');
|
|
86
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
87
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-test-'));
|
|
87
88
|
const mdPath = path.join(tmpDir, 'pptx.md');
|
|
88
89
|
fs.writeFileSync(mdPath, `---
|
|
89
90
|
name: pptx
|
|
@@ -108,7 +109,7 @@ Body.`, 'utf-8');
|
|
|
108
109
|
|
|
109
110
|
it('small body loaded in full', async () => {
|
|
110
111
|
const { Skill } = await import('../src/core/skill');
|
|
111
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
112
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-test-'));
|
|
112
113
|
const mdPath = path.join(tmpDir, 'small.md');
|
|
113
114
|
fs.writeFileSync(mdPath, '---\nname: small\ndescription: x\n---\n\n# Small Skill\n\nThis fits inline easily.', 'utf-8');
|
|
114
115
|
const s = Skill.fromMarkdown(mdPath);
|
|
@@ -122,7 +123,7 @@ Body.`, 'utf-8');
|
|
|
122
123
|
|
|
123
124
|
it('large body truncated to head', async () => {
|
|
124
125
|
const { Skill } = await import('../src/core/skill');
|
|
125
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
126
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-test-'));
|
|
126
127
|
const mdPath = path.join(tmpDir, 'big.md');
|
|
127
128
|
|
|
128
129
|
let body = '# Big Skill\n\n## Quick Reference\nFirst section content.\n\n';
|