@dexto/tools-plan 1.5.8 → 1.6.1
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/errors.cjs +126 -0
- package/dist/errors.js +99 -64
- package/dist/index.cjs +36 -0
- package/dist/index.d.cts +224 -0
- package/dist/index.d.ts +1 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -39
- package/dist/plan-service-getter.cjs +16 -0
- package/dist/plan-service-getter.d.ts +4 -0
- package/dist/plan-service-getter.d.ts.map +1 -0
- package/dist/plan-service-getter.js +0 -0
- package/dist/plan-service.cjs +247 -0
- package/dist/plan-service.d.ts +2 -2
- package/dist/plan-service.d.ts.map +1 -1
- package/dist/plan-service.js +201 -215
- package/dist/plan-service.test.cjs +227 -0
- package/dist/plan-service.test.js +200 -216
- package/dist/tool-factory-config.cjs +38 -0
- package/dist/tool-factory-config.d.ts +32 -0
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +13 -0
- package/dist/tool-factory.cjs +71 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +40 -0
- package/dist/tool-factory.test.cjs +96 -0
- package/dist/tool-factory.test.d.ts +7 -0
- package/dist/tool-factory.test.d.ts.map +1 -0
- package/dist/tool-factory.test.js +95 -0
- package/dist/tools/plan-create-tool.cjs +102 -0
- package/dist/tools/plan-create-tool.d.ts +15 -3
- package/dist/tools/plan-create-tool.d.ts.map +1 -1
- package/dist/tools/plan-create-tool.js +77 -71
- package/dist/tools/plan-create-tool.test.cjs +174 -0
- package/dist/tools/plan-create-tool.test.js +142 -109
- package/dist/tools/plan-read-tool.cjs +65 -0
- package/dist/tools/plan-read-tool.d.ts +6 -3
- package/dist/tools/plan-read-tool.d.ts.map +1 -1
- package/dist/tools/plan-read-tool.js +39 -38
- package/dist/tools/plan-read-tool.test.cjs +109 -0
- package/dist/tools/plan-read-tool.test.js +78 -75
- package/dist/tools/plan-review-tool.cjs +98 -0
- package/dist/tools/plan-review-tool.d.ts +14 -5
- package/dist/tools/plan-review-tool.d.ts.map +1 -1
- package/dist/tools/plan-review-tool.js +73 -83
- package/dist/tools/plan-update-tool.cjs +92 -0
- package/dist/tools/plan-update-tool.d.ts +12 -3
- package/dist/tools/plan-update-tool.d.ts.map +1 -1
- package/dist/tools/plan-update-tool.js +65 -69
- package/dist/tools/plan-update-tool.test.cjs +203 -0
- package/dist/tools/plan-update-tool.test.js +171 -142
- package/dist/types.cjs +44 -0
- package/dist/types.js +17 -24
- package/package.json +8 -8
- package/.dexto-plugin/plugin.json +0 -7
- package/dist/tool-provider.d.ts +0 -44
- package/dist/tool-provider.d.ts.map +0 -1
- package/dist/tool-provider.js +0 -81
- package/dist/tool-provider.test.d.ts +0 -7
- package/dist/tool-provider.test.d.ts.map +0 -1
- package/dist/tool-provider.test.js +0 -185
- package/skills/plan/SKILL.md +0 -102
|
@@ -1,220 +1,204 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import * as fs from 'node:fs/promises';
|
|
9
|
-
import * as os from 'node:os';
|
|
10
|
-
import { PlanService } from './plan-service.js';
|
|
11
|
-
import { PlanErrorCode } from './errors.js';
|
|
12
|
-
import { DextoRuntimeError } from '@dexto/core';
|
|
13
|
-
// Create mock logger
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { PlanService } from "./plan-service.js";
|
|
6
|
+
import { PlanErrorCode } from "./errors.js";
|
|
7
|
+
import { DextoRuntimeError } from "@dexto/core";
|
|
14
8
|
const createMockLogger = () => ({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
silly: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
trackException: vi.fn(),
|
|
15
|
+
createChild: vi.fn().mockReturnThis(),
|
|
16
|
+
setLevel: vi.fn(),
|
|
17
|
+
getLevel: vi.fn(() => "debug"),
|
|
18
|
+
getLogFilePath: vi.fn(() => null),
|
|
19
|
+
destroy: vi.fn(async () => void 0)
|
|
20
20
|
});
|
|
21
|
-
describe(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
describe('exists', () => {
|
|
43
|
-
it('should return false for non-existent plan', async () => {
|
|
44
|
-
const exists = await planService.exists('non-existent-session');
|
|
45
|
-
expect(exists).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
it('should return true for existing plan', async () => {
|
|
48
|
-
const sessionId = 'test-session';
|
|
49
|
-
await planService.create(sessionId, '# Test Plan');
|
|
50
|
-
const exists = await planService.exists(sessionId);
|
|
51
|
-
expect(exists).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
describe('create', () => {
|
|
55
|
-
it('should create a new plan with content and metadata', async () => {
|
|
56
|
-
const sessionId = 'test-session';
|
|
57
|
-
const content = '# Implementation Plan\n\n## Steps\n1. First step';
|
|
58
|
-
const title = 'Test Plan';
|
|
59
|
-
const plan = await planService.create(sessionId, content, { title });
|
|
60
|
-
expect(plan.content).toBe(content);
|
|
61
|
-
expect(plan.meta.sessionId).toBe(sessionId);
|
|
62
|
-
expect(plan.meta.status).toBe('draft');
|
|
63
|
-
expect(plan.meta.title).toBe(title);
|
|
64
|
-
expect(plan.meta.createdAt).toBeGreaterThan(0);
|
|
65
|
-
expect(plan.meta.updatedAt).toBeGreaterThan(0);
|
|
66
|
-
});
|
|
67
|
-
it('should throw error when plan already exists', async () => {
|
|
68
|
-
const sessionId = 'test-session';
|
|
69
|
-
await planService.create(sessionId, '# First Plan');
|
|
70
|
-
try {
|
|
71
|
-
await planService.create(sessionId, '# Second Plan');
|
|
72
|
-
expect.fail('Should have thrown an error');
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
76
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_ALREADY_EXISTS);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
it('should store plan files on disk', async () => {
|
|
80
|
-
const sessionId = 'test-session';
|
|
81
|
-
const content = '# Test Plan';
|
|
82
|
-
await planService.create(sessionId, content);
|
|
83
|
-
// Verify plan.md exists
|
|
84
|
-
const planPath = path.join(tempDir, sessionId, 'plan.md');
|
|
85
|
-
const storedContent = await fs.readFile(planPath, 'utf-8');
|
|
86
|
-
expect(storedContent).toBe(content);
|
|
87
|
-
// Verify plan-meta.json exists
|
|
88
|
-
const metaPath = path.join(tempDir, sessionId, 'plan-meta.json');
|
|
89
|
-
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
90
|
-
const meta = JSON.parse(metaContent);
|
|
91
|
-
expect(meta.sessionId).toBe(sessionId);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe('read', () => {
|
|
95
|
-
it('should return null for non-existent plan', async () => {
|
|
96
|
-
const plan = await planService.read('non-existent-session');
|
|
97
|
-
expect(plan).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
it('should read existing plan with content and metadata', async () => {
|
|
100
|
-
const sessionId = 'test-session';
|
|
101
|
-
const content = '# Test Plan';
|
|
102
|
-
const title = 'My Plan';
|
|
103
|
-
await planService.create(sessionId, content, { title });
|
|
104
|
-
const plan = await planService.read(sessionId);
|
|
105
|
-
expect(plan).not.toBeNull();
|
|
106
|
-
expect(plan.content).toBe(content);
|
|
107
|
-
expect(plan.meta.sessionId).toBe(sessionId);
|
|
108
|
-
expect(plan.meta.title).toBe(title);
|
|
109
|
-
});
|
|
110
|
-
it('should handle invalid metadata schema gracefully', async () => {
|
|
111
|
-
const sessionId = 'test-session';
|
|
112
|
-
await planService.create(sessionId, '# Test');
|
|
113
|
-
// Write valid JSON but invalid schema (missing required fields)
|
|
114
|
-
const metaPath = path.join(tempDir, sessionId, 'plan-meta.json');
|
|
115
|
-
await fs.writeFile(metaPath, JSON.stringify({ invalidField: 'value' }));
|
|
116
|
-
const plan = await planService.read(sessionId);
|
|
117
|
-
// Should return with default metadata
|
|
118
|
-
expect(plan).not.toBeNull();
|
|
119
|
-
expect(plan.meta.sessionId).toBe(sessionId);
|
|
120
|
-
expect(plan.meta.status).toBe('draft');
|
|
121
|
-
expect(mockLogger.warn).toHaveBeenCalled();
|
|
122
|
-
});
|
|
123
|
-
it('should return null for corrupted JSON metadata', async () => {
|
|
124
|
-
const sessionId = 'test-session';
|
|
125
|
-
await planService.create(sessionId, '# Test');
|
|
126
|
-
// Corrupt the metadata with invalid JSON
|
|
127
|
-
const metaPath = path.join(tempDir, sessionId, 'plan-meta.json');
|
|
128
|
-
await fs.writeFile(metaPath, '{ invalid json }');
|
|
129
|
-
const plan = await planService.read(sessionId);
|
|
130
|
-
// Should return null and log error
|
|
131
|
-
expect(plan).toBeNull();
|
|
132
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('update', () => {
|
|
136
|
-
it('should update plan content', async () => {
|
|
137
|
-
const sessionId = 'test-session';
|
|
138
|
-
await planService.create(sessionId, '# Original Content');
|
|
139
|
-
const result = await planService.update(sessionId, '# Updated Content');
|
|
140
|
-
expect(result.oldContent).toBe('# Original Content');
|
|
141
|
-
expect(result.newContent).toBe('# Updated Content');
|
|
142
|
-
expect(result.meta.updatedAt).toBeGreaterThan(0);
|
|
143
|
-
});
|
|
144
|
-
it('should preserve metadata when updating content', async () => {
|
|
145
|
-
const sessionId = 'test-session';
|
|
146
|
-
const plan = await planService.create(sessionId, '# Original', { title: 'My Title' });
|
|
147
|
-
const originalCreatedAt = plan.meta.createdAt;
|
|
148
|
-
await planService.update(sessionId, '# Updated');
|
|
149
|
-
const updatedPlan = await planService.read(sessionId);
|
|
150
|
-
expect(updatedPlan.meta.title).toBe('My Title');
|
|
151
|
-
expect(updatedPlan.meta.createdAt).toBe(originalCreatedAt);
|
|
152
|
-
});
|
|
153
|
-
it('should throw error when plan does not exist', async () => {
|
|
154
|
-
try {
|
|
155
|
-
await planService.update('non-existent', '# Content');
|
|
156
|
-
expect.fail('Should have thrown an error');
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
160
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
describe('updateMeta', () => {
|
|
165
|
-
it('should update plan status', async () => {
|
|
166
|
-
const sessionId = 'test-session';
|
|
167
|
-
await planService.create(sessionId, '# Plan');
|
|
168
|
-
const meta = await planService.updateMeta(sessionId, { status: 'approved' });
|
|
169
|
-
expect(meta.status).toBe('approved');
|
|
170
|
-
});
|
|
171
|
-
it('should update plan title', async () => {
|
|
172
|
-
const sessionId = 'test-session';
|
|
173
|
-
await planService.create(sessionId, '# Plan');
|
|
174
|
-
const meta = await planService.updateMeta(sessionId, { title: 'New Title' });
|
|
175
|
-
expect(meta.title).toBe('New Title');
|
|
176
|
-
});
|
|
177
|
-
it('should throw error when plan does not exist', async () => {
|
|
178
|
-
try {
|
|
179
|
-
await planService.updateMeta('non-existent', { status: 'approved' });
|
|
180
|
-
expect.fail('Should have thrown an error');
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
184
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
describe('delete', () => {
|
|
189
|
-
it('should delete existing plan', async () => {
|
|
190
|
-
const sessionId = 'test-session';
|
|
191
|
-
await planService.create(sessionId, '# Plan');
|
|
192
|
-
await planService.delete(sessionId);
|
|
193
|
-
const exists = await planService.exists(sessionId);
|
|
194
|
-
expect(exists).toBe(false);
|
|
195
|
-
});
|
|
196
|
-
it('should throw error when plan does not exist', async () => {
|
|
197
|
-
try {
|
|
198
|
-
await planService.delete('non-existent');
|
|
199
|
-
expect.fail('Should have thrown an error');
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
203
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
it('should remove plan directory from disk', async () => {
|
|
207
|
-
const sessionId = 'test-session';
|
|
208
|
-
await planService.create(sessionId, '# Plan');
|
|
209
|
-
const planDir = path.join(tempDir, sessionId);
|
|
210
|
-
await planService.delete(sessionId);
|
|
211
|
-
try {
|
|
212
|
-
await fs.access(planDir);
|
|
213
|
-
expect.fail('Directory should not exist');
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// Expected - directory should not exist
|
|
217
|
-
}
|
|
218
|
-
});
|
|
21
|
+
describe("PlanService", () => {
|
|
22
|
+
let mockLogger;
|
|
23
|
+
let tempDir;
|
|
24
|
+
let planService;
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
mockLogger = createMockLogger();
|
|
27
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-plan-test-"));
|
|
28
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
29
|
+
planService = new PlanService({ basePath: tempDir }, mockLogger);
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
try {
|
|
34
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
describe("exists", () => {
|
|
39
|
+
it("should return false for non-existent plan", async () => {
|
|
40
|
+
const exists = await planService.exists("non-existent-session");
|
|
41
|
+
expect(exists).toBe(false);
|
|
219
42
|
});
|
|
43
|
+
it("should return true for existing plan", async () => {
|
|
44
|
+
const sessionId = "test-session";
|
|
45
|
+
await planService.create(sessionId, "# Test Plan");
|
|
46
|
+
const exists = await planService.exists(sessionId);
|
|
47
|
+
expect(exists).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("create", () => {
|
|
51
|
+
it("should create a new plan with content and metadata", async () => {
|
|
52
|
+
const sessionId = "test-session";
|
|
53
|
+
const content = "# Implementation Plan\n\n## Steps\n1. First step";
|
|
54
|
+
const title = "Test Plan";
|
|
55
|
+
const plan = await planService.create(sessionId, content, { title });
|
|
56
|
+
expect(plan.content).toBe(content);
|
|
57
|
+
expect(plan.meta.sessionId).toBe(sessionId);
|
|
58
|
+
expect(plan.meta.status).toBe("draft");
|
|
59
|
+
expect(plan.meta.title).toBe(title);
|
|
60
|
+
expect(plan.meta.createdAt).toBeGreaterThan(0);
|
|
61
|
+
expect(plan.meta.updatedAt).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
it("should throw error when plan already exists", async () => {
|
|
64
|
+
const sessionId = "test-session";
|
|
65
|
+
await planService.create(sessionId, "# First Plan");
|
|
66
|
+
try {
|
|
67
|
+
await planService.create(sessionId, "# Second Plan");
|
|
68
|
+
expect.fail("Should have thrown an error");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
71
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_ALREADY_EXISTS);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
it("should store plan files on disk", async () => {
|
|
75
|
+
const sessionId = "test-session";
|
|
76
|
+
const content = "# Test Plan";
|
|
77
|
+
await planService.create(sessionId, content);
|
|
78
|
+
const planPath = path.join(tempDir, sessionId, "plan.md");
|
|
79
|
+
const storedContent = await fs.readFile(planPath, "utf-8");
|
|
80
|
+
expect(storedContent).toBe(content);
|
|
81
|
+
const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
|
|
82
|
+
const metaContent = await fs.readFile(metaPath, "utf-8");
|
|
83
|
+
const meta = JSON.parse(metaContent);
|
|
84
|
+
expect(meta.sessionId).toBe(sessionId);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("read", () => {
|
|
88
|
+
it("should return null for non-existent plan", async () => {
|
|
89
|
+
const plan = await planService.read("non-existent-session");
|
|
90
|
+
expect(plan).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
it("should read existing plan with content and metadata", async () => {
|
|
93
|
+
const sessionId = "test-session";
|
|
94
|
+
const content = "# Test Plan";
|
|
95
|
+
const title = "My Plan";
|
|
96
|
+
await planService.create(sessionId, content, { title });
|
|
97
|
+
const plan = await planService.read(sessionId);
|
|
98
|
+
expect(plan).not.toBeNull();
|
|
99
|
+
expect(plan.content).toBe(content);
|
|
100
|
+
expect(plan.meta.sessionId).toBe(sessionId);
|
|
101
|
+
expect(plan.meta.title).toBe(title);
|
|
102
|
+
});
|
|
103
|
+
it("should handle invalid metadata schema gracefully", async () => {
|
|
104
|
+
const sessionId = "test-session";
|
|
105
|
+
await planService.create(sessionId, "# Test");
|
|
106
|
+
const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
|
|
107
|
+
await fs.writeFile(metaPath, JSON.stringify({ invalidField: "value" }));
|
|
108
|
+
const plan = await planService.read(sessionId);
|
|
109
|
+
expect(plan).not.toBeNull();
|
|
110
|
+
expect(plan.meta.sessionId).toBe(sessionId);
|
|
111
|
+
expect(plan.meta.status).toBe("draft");
|
|
112
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
it("should return null for corrupted JSON metadata", async () => {
|
|
115
|
+
const sessionId = "test-session";
|
|
116
|
+
await planService.create(sessionId, "# Test");
|
|
117
|
+
const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
|
|
118
|
+
await fs.writeFile(metaPath, "{ invalid json }");
|
|
119
|
+
const plan = await planService.read(sessionId);
|
|
120
|
+
expect(plan).toBeNull();
|
|
121
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("update", () => {
|
|
125
|
+
it("should update plan content", async () => {
|
|
126
|
+
const sessionId = "test-session";
|
|
127
|
+
await planService.create(sessionId, "# Original Content");
|
|
128
|
+
const result = await planService.update(sessionId, "# Updated Content");
|
|
129
|
+
expect(result.oldContent).toBe("# Original Content");
|
|
130
|
+
expect(result.newContent).toBe("# Updated Content");
|
|
131
|
+
expect(result.meta.updatedAt).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
it("should preserve metadata when updating content", async () => {
|
|
134
|
+
const sessionId = "test-session";
|
|
135
|
+
const plan = await planService.create(sessionId, "# Original", { title: "My Title" });
|
|
136
|
+
const originalCreatedAt = plan.meta.createdAt;
|
|
137
|
+
await planService.update(sessionId, "# Updated");
|
|
138
|
+
const updatedPlan = await planService.read(sessionId);
|
|
139
|
+
expect(updatedPlan.meta.title).toBe("My Title");
|
|
140
|
+
expect(updatedPlan.meta.createdAt).toBe(originalCreatedAt);
|
|
141
|
+
});
|
|
142
|
+
it("should throw error when plan does not exist", async () => {
|
|
143
|
+
try {
|
|
144
|
+
await planService.update("non-existent", "# Content");
|
|
145
|
+
expect.fail("Should have thrown an error");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
148
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe("updateMeta", () => {
|
|
153
|
+
it("should update plan status", async () => {
|
|
154
|
+
const sessionId = "test-session";
|
|
155
|
+
await planService.create(sessionId, "# Plan");
|
|
156
|
+
const meta = await planService.updateMeta(sessionId, { status: "approved" });
|
|
157
|
+
expect(meta.status).toBe("approved");
|
|
158
|
+
});
|
|
159
|
+
it("should update plan title", async () => {
|
|
160
|
+
const sessionId = "test-session";
|
|
161
|
+
await planService.create(sessionId, "# Plan");
|
|
162
|
+
const meta = await planService.updateMeta(sessionId, { title: "New Title" });
|
|
163
|
+
expect(meta.title).toBe("New Title");
|
|
164
|
+
});
|
|
165
|
+
it("should throw error when plan does not exist", async () => {
|
|
166
|
+
try {
|
|
167
|
+
await planService.updateMeta("non-existent", { status: "approved" });
|
|
168
|
+
expect.fail("Should have thrown an error");
|
|
169
|
+
} catch (error) {
|
|
170
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
171
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe("delete", () => {
|
|
176
|
+
it("should delete existing plan", async () => {
|
|
177
|
+
const sessionId = "test-session";
|
|
178
|
+
await planService.create(sessionId, "# Plan");
|
|
179
|
+
await planService.delete(sessionId);
|
|
180
|
+
const exists = await planService.exists(sessionId);
|
|
181
|
+
expect(exists).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
it("should throw error when plan does not exist", async () => {
|
|
184
|
+
try {
|
|
185
|
+
await planService.delete("non-existent");
|
|
186
|
+
expect.fail("Should have thrown an error");
|
|
187
|
+
} catch (error) {
|
|
188
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
189
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
it("should remove plan directory from disk", async () => {
|
|
193
|
+
const sessionId = "test-session";
|
|
194
|
+
await planService.create(sessionId, "# Plan");
|
|
195
|
+
const planDir = path.join(tempDir, sessionId);
|
|
196
|
+
await planService.delete(sessionId);
|
|
197
|
+
try {
|
|
198
|
+
await fs.access(planDir);
|
|
199
|
+
expect.fail("Directory should not exist");
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
220
204
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var tool_factory_config_exports = {};
|
|
20
|
+
__export(tool_factory_config_exports, {
|
|
21
|
+
PLAN_TOOL_NAMES: () => PLAN_TOOL_NAMES,
|
|
22
|
+
PlanToolsConfigSchema: () => PlanToolsConfigSchema
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(tool_factory_config_exports);
|
|
25
|
+
var import_zod = require("zod");
|
|
26
|
+
const PLAN_TOOL_NAMES = ["plan_create", "plan_read", "plan_update", "plan_review"];
|
|
27
|
+
const PlanToolsConfigSchema = import_zod.z.object({
|
|
28
|
+
type: import_zod.z.literal("plan-tools"),
|
|
29
|
+
basePath: import_zod.z.string().default(".dexto/plans").describe("Base directory for plan storage (relative to working directory)"),
|
|
30
|
+
enabledTools: import_zod.z.array(import_zod.z.enum(PLAN_TOOL_NAMES)).optional().describe(
|
|
31
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${PLAN_TOOL_NAMES.join(", ")}`
|
|
32
|
+
)
|
|
33
|
+
}).strict();
|
|
34
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
35
|
+
0 && (module.exports = {
|
|
36
|
+
PLAN_TOOL_NAMES,
|
|
37
|
+
PlanToolsConfigSchema
|
|
38
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Tools Factory
|
|
3
|
+
*
|
|
4
|
+
* Provides implementation planning tools:
|
|
5
|
+
* - plan_create: Create a new plan for the session
|
|
6
|
+
* - plan_read: Read the current plan
|
|
7
|
+
* - plan_update: Update the existing plan
|
|
8
|
+
* - plan_review: Request user review of the plan (shows plan content with approval options)
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
/**
|
|
12
|
+
* Available plan tool names for enabledTools configuration
|
|
13
|
+
*/
|
|
14
|
+
export declare const PLAN_TOOL_NAMES: readonly ["plan_create", "plan_read", "plan_update", "plan_review"];
|
|
15
|
+
/**
|
|
16
|
+
* Configuration schema for Plan tools factory
|
|
17
|
+
*/
|
|
18
|
+
export declare const PlanToolsConfigSchema: z.ZodObject<{
|
|
19
|
+
type: z.ZodLiteral<"plan-tools">;
|
|
20
|
+
basePath: z.ZodDefault<z.ZodString>;
|
|
21
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["plan_create", "plan_read", "plan_update", "plan_review"]>, "many">>;
|
|
22
|
+
}, "strict", z.ZodTypeAny, {
|
|
23
|
+
type: "plan-tools";
|
|
24
|
+
basePath: string;
|
|
25
|
+
enabledTools?: ("plan_create" | "plan_read" | "plan_update" | "plan_review")[] | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
type: "plan-tools";
|
|
28
|
+
basePath?: string | undefined;
|
|
29
|
+
enabledTools?: ("plan_create" | "plan_read" | "plan_update" | "plan_review")[] | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
export type PlanToolsConfig = z.output<typeof PlanToolsConfigSchema>;
|
|
32
|
+
//# sourceMappingURL=tool-factory-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-factory-config.d.ts","sourceRoot":"","sources":["../src/tool-factory-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,eAAe,qEAAsE,CAAC;AAEnG;;GAEG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;EAcrB,CAAC;AAEd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const PLAN_TOOL_NAMES = ["plan_create", "plan_read", "plan_update", "plan_review"];
|
|
3
|
+
const PlanToolsConfigSchema = z.object({
|
|
4
|
+
type: z.literal("plan-tools"),
|
|
5
|
+
basePath: z.string().default(".dexto/plans").describe("Base directory for plan storage (relative to working directory)"),
|
|
6
|
+
enabledTools: z.array(z.enum(PLAN_TOOL_NAMES)).optional().describe(
|
|
7
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${PLAN_TOOL_NAMES.join(", ")}`
|
|
8
|
+
)
|
|
9
|
+
}).strict();
|
|
10
|
+
export {
|
|
11
|
+
PLAN_TOOL_NAMES,
|
|
12
|
+
PlanToolsConfigSchema
|
|
13
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var tool_factory_exports = {};
|
|
30
|
+
__export(tool_factory_exports, {
|
|
31
|
+
planToolsFactory: () => planToolsFactory
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(tool_factory_exports);
|
|
34
|
+
var path = __toESM(require("node:path"), 1);
|
|
35
|
+
var import_plan_service = require("./plan-service.js");
|
|
36
|
+
var import_plan_create_tool = require("./tools/plan-create-tool.js");
|
|
37
|
+
var import_plan_read_tool = require("./tools/plan-read-tool.js");
|
|
38
|
+
var import_plan_update_tool = require("./tools/plan-update-tool.js");
|
|
39
|
+
var import_plan_review_tool = require("./tools/plan-review-tool.js");
|
|
40
|
+
var import_tool_factory_config = require("./tool-factory-config.js");
|
|
41
|
+
const planToolsFactory = {
|
|
42
|
+
configSchema: import_tool_factory_config.PlanToolsConfigSchema,
|
|
43
|
+
metadata: {
|
|
44
|
+
displayName: "Plan Tools",
|
|
45
|
+
description: "Create and manage implementation plans linked to sessions",
|
|
46
|
+
category: "planning"
|
|
47
|
+
},
|
|
48
|
+
create: (config) => {
|
|
49
|
+
const basePath = path.isAbsolute(config.basePath) ? config.basePath : path.join(process.cwd(), config.basePath);
|
|
50
|
+
let planService;
|
|
51
|
+
const getPlanService = async (context) => {
|
|
52
|
+
if (planService) {
|
|
53
|
+
return planService;
|
|
54
|
+
}
|
|
55
|
+
planService = new import_plan_service.PlanService({ basePath }, context.logger);
|
|
56
|
+
return planService;
|
|
57
|
+
};
|
|
58
|
+
const toolCreators = {
|
|
59
|
+
plan_create: () => (0, import_plan_create_tool.createPlanCreateTool)(getPlanService),
|
|
60
|
+
plan_read: () => (0, import_plan_read_tool.createPlanReadTool)(getPlanService),
|
|
61
|
+
plan_update: () => (0, import_plan_update_tool.createPlanUpdateTool)(getPlanService),
|
|
62
|
+
plan_review: () => (0, import_plan_review_tool.createPlanReviewTool)(getPlanService)
|
|
63
|
+
};
|
|
64
|
+
const toolsToCreate = config.enabledTools ?? import_tool_factory_config.PLAN_TOOL_NAMES;
|
|
65
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
69
|
+
0 && (module.exports = {
|
|
70
|
+
planToolsFactory
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-factory.d.ts","sourceRoot":"","sources":["../src/tool-factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQvD,OAAO,EAGH,KAAK,eAAe,EACvB,MAAM,0BAA0B,CAAC;AAKlC,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,eAAe,CAiCzD,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { PlanService } from "./plan-service.js";
|
|
3
|
+
import { createPlanCreateTool } from "./tools/plan-create-tool.js";
|
|
4
|
+
import { createPlanReadTool } from "./tools/plan-read-tool.js";
|
|
5
|
+
import { createPlanUpdateTool } from "./tools/plan-update-tool.js";
|
|
6
|
+
import { createPlanReviewTool } from "./tools/plan-review-tool.js";
|
|
7
|
+
import {
|
|
8
|
+
PLAN_TOOL_NAMES,
|
|
9
|
+
PlanToolsConfigSchema
|
|
10
|
+
} from "./tool-factory-config.js";
|
|
11
|
+
const planToolsFactory = {
|
|
12
|
+
configSchema: PlanToolsConfigSchema,
|
|
13
|
+
metadata: {
|
|
14
|
+
displayName: "Plan Tools",
|
|
15
|
+
description: "Create and manage implementation plans linked to sessions",
|
|
16
|
+
category: "planning"
|
|
17
|
+
},
|
|
18
|
+
create: (config) => {
|
|
19
|
+
const basePath = path.isAbsolute(config.basePath) ? config.basePath : path.join(process.cwd(), config.basePath);
|
|
20
|
+
let planService;
|
|
21
|
+
const getPlanService = async (context) => {
|
|
22
|
+
if (planService) {
|
|
23
|
+
return planService;
|
|
24
|
+
}
|
|
25
|
+
planService = new PlanService({ basePath }, context.logger);
|
|
26
|
+
return planService;
|
|
27
|
+
};
|
|
28
|
+
const toolCreators = {
|
|
29
|
+
plan_create: () => createPlanCreateTool(getPlanService),
|
|
30
|
+
plan_read: () => createPlanReadTool(getPlanService),
|
|
31
|
+
plan_update: () => createPlanUpdateTool(getPlanService),
|
|
32
|
+
plan_review: () => createPlanReviewTool(getPlanService)
|
|
33
|
+
};
|
|
34
|
+
const toolsToCreate = config.enabledTools ?? PLAN_TOOL_NAMES;
|
|
35
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export {
|
|
39
|
+
planToolsFactory
|
|
40
|
+
};
|