@dexto/tools-plan 1.5.7
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/.dexto-plugin/plugin.json +7 -0
- package/LICENSE +44 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/plan-service.d.ts +77 -0
- package/dist/plan-service.d.ts.map +1 -0
- package/dist/plan-service.js +227 -0
- package/dist/plan-service.test.d.ts +7 -0
- package/dist/plan-service.test.d.ts.map +1 -0
- package/dist/plan-service.test.js +220 -0
- package/dist/tool-provider.d.ts +44 -0
- package/dist/tool-provider.d.ts.map +1 -0
- package/dist/tool-provider.js +81 -0
- package/dist/tool-provider.test.d.ts +7 -0
- package/dist/tool-provider.test.d.ts.map +1 -0
- package/dist/tool-provider.test.js +185 -0
- package/dist/tools/plan-create-tool.d.ts +13 -0
- package/dist/tools/plan-create-tool.d.ts.map +1 -0
- package/dist/tools/plan-create-tool.js +72 -0
- package/dist/tools/plan-create-tool.test.d.ts +7 -0
- package/dist/tools/plan-create-tool.test.d.ts.map +1 -0
- package/dist/tools/plan-create-tool.test.js +118 -0
- package/dist/tools/plan-read-tool.d.ts +13 -0
- package/dist/tools/plan-read-tool.d.ts.map +1 -0
- package/dist/tools/plan-read-tool.js +40 -0
- package/dist/tools/plan-read-tool.test.d.ts +7 -0
- package/dist/tools/plan-read-tool.test.d.ts.map +1 -0
- package/dist/tools/plan-read-tool.test.js +83 -0
- package/dist/tools/plan-review-tool.d.ts +22 -0
- package/dist/tools/plan-review-tool.d.ts.map +1 -0
- package/dist/tools/plan-review-tool.js +84 -0
- package/dist/tools/plan-update-tool.d.ts +13 -0
- package/dist/tools/plan-update-tool.d.ts.map +1 -0
- package/dist/tools/plan-update-tool.js +72 -0
- package/dist/tools/plan-update-tool.test.d.ts +7 -0
- package/dist/tools/plan-update-tool.test.d.ts.map +1 -0
- package/dist/tools/plan-update-tool.test.js +151 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/package.json +40 -0
- package/skills/plan/SKILL.md +102 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Service Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the PlanService CRUD operations and error handling.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import * as path from 'node:path';
|
|
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
|
|
14
|
+
const createMockLogger = () => ({
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
info: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
createChild: vi.fn().mockReturnThis(),
|
|
20
|
+
});
|
|
21
|
+
describe('PlanService', () => {
|
|
22
|
+
let mockLogger;
|
|
23
|
+
let tempDir;
|
|
24
|
+
let planService;
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
mockLogger = createMockLogger();
|
|
27
|
+
// Create temp directory for testing
|
|
28
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dexto-plan-test-'));
|
|
29
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
30
|
+
planService = new PlanService({ basePath: tempDir }, mockLogger);
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
// Cleanup temp directory
|
|
35
|
+
try {
|
|
36
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Ignore cleanup errors
|
|
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
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Tools Provider
|
|
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
|
+
import type { CustomToolProvider } from '@dexto/core';
|
|
12
|
+
/**
|
|
13
|
+
* Configuration schema for Plan tools provider
|
|
14
|
+
*/
|
|
15
|
+
declare const PlanToolsConfigSchema: z.ZodObject<{
|
|
16
|
+
type: z.ZodLiteral<"plan-tools">;
|
|
17
|
+
basePath: z.ZodDefault<z.ZodString>;
|
|
18
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["plan_create", "plan_read", "plan_update", "plan_review"]>, "many">>;
|
|
19
|
+
}, "strict", z.ZodTypeAny, {
|
|
20
|
+
type: "plan-tools";
|
|
21
|
+
basePath: string;
|
|
22
|
+
enabledTools?: ("plan_create" | "plan_read" | "plan_update" | "plan_review")[] | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
type: "plan-tools";
|
|
25
|
+
basePath?: string | undefined;
|
|
26
|
+
enabledTools?: ("plan_create" | "plan_read" | "plan_update" | "plan_review")[] | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
type PlanToolsConfig = z.output<typeof PlanToolsConfigSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* Plan tools provider
|
|
31
|
+
*
|
|
32
|
+
* Provides implementation planning tools:
|
|
33
|
+
* - plan_create: Create a new plan with markdown content
|
|
34
|
+
* - plan_read: Read the current plan
|
|
35
|
+
* - plan_update: Update existing plan (shows diff preview)
|
|
36
|
+
* - plan_review: Request user review of the plan (shows plan with approval options)
|
|
37
|
+
*
|
|
38
|
+
* Plans are stored in .dexto/plans/{sessionId}/ with:
|
|
39
|
+
* - plan.md: Markdown content with checkboxes (- [ ] and - [x])
|
|
40
|
+
* - plan-meta.json: Metadata (status, title, timestamps)
|
|
41
|
+
*/
|
|
42
|
+
export declare const planToolsProvider: CustomToolProvider<'plan-tools', PlanToolsConfig>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=tool-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-provider.d.ts","sourceRoot":"","sources":["../src/tool-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAqC,MAAM,aAAa,CAAC;AAazF;;GAEG;AACH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;EAcd,CAAC;AAEd,KAAK,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE9D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,EAAE,kBAAkB,CAAC,YAAY,EAAE,eAAe,CAwC/E,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Tools Provider
|
|
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 * as path from 'node:path';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { PlanService } from './plan-service.js';
|
|
13
|
+
import { createPlanCreateTool } from './tools/plan-create-tool.js';
|
|
14
|
+
import { createPlanReadTool } from './tools/plan-read-tool.js';
|
|
15
|
+
import { createPlanUpdateTool } from './tools/plan-update-tool.js';
|
|
16
|
+
import { createPlanReviewTool } from './tools/plan-review-tool.js';
|
|
17
|
+
/**
|
|
18
|
+
* Available plan tool names for enabledTools configuration
|
|
19
|
+
*/
|
|
20
|
+
const PLAN_TOOL_NAMES = ['plan_create', 'plan_read', 'plan_update', 'plan_review'];
|
|
21
|
+
/**
|
|
22
|
+
* Configuration schema for Plan tools provider
|
|
23
|
+
*/
|
|
24
|
+
const PlanToolsConfigSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
type: z.literal('plan-tools'),
|
|
27
|
+
basePath: z
|
|
28
|
+
.string()
|
|
29
|
+
.default('.dexto/plans')
|
|
30
|
+
.describe('Base directory for plan storage (relative to working directory)'),
|
|
31
|
+
enabledTools: z
|
|
32
|
+
.array(z.enum(PLAN_TOOL_NAMES))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(`Subset of tools to enable. If not specified, all tools are enabled. Available: ${PLAN_TOOL_NAMES.join(', ')}`),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
/**
|
|
38
|
+
* Plan tools provider
|
|
39
|
+
*
|
|
40
|
+
* Provides implementation planning tools:
|
|
41
|
+
* - plan_create: Create a new plan with markdown content
|
|
42
|
+
* - plan_read: Read the current plan
|
|
43
|
+
* - plan_update: Update existing plan (shows diff preview)
|
|
44
|
+
* - plan_review: Request user review of the plan (shows plan with approval options)
|
|
45
|
+
*
|
|
46
|
+
* Plans are stored in .dexto/plans/{sessionId}/ with:
|
|
47
|
+
* - plan.md: Markdown content with checkboxes (- [ ] and - [x])
|
|
48
|
+
* - plan-meta.json: Metadata (status, title, timestamps)
|
|
49
|
+
*/
|
|
50
|
+
export const planToolsProvider = {
|
|
51
|
+
type: 'plan-tools',
|
|
52
|
+
configSchema: PlanToolsConfigSchema,
|
|
53
|
+
create: (config, context) => {
|
|
54
|
+
const { logger } = context;
|
|
55
|
+
// Resolve base path (relative to cwd or absolute)
|
|
56
|
+
const basePath = path.isAbsolute(config.basePath)
|
|
57
|
+
? config.basePath
|
|
58
|
+
: path.join(process.cwd(), config.basePath);
|
|
59
|
+
logger.debug(`Creating PlanService with basePath: ${basePath}`);
|
|
60
|
+
const planService = new PlanService({ basePath }, logger);
|
|
61
|
+
// Build tool map for selective enabling
|
|
62
|
+
const toolCreators = {
|
|
63
|
+
plan_create: () => createPlanCreateTool(planService),
|
|
64
|
+
plan_read: () => createPlanReadTool(planService),
|
|
65
|
+
plan_update: () => createPlanUpdateTool(planService),
|
|
66
|
+
plan_review: () => createPlanReviewTool(planService),
|
|
67
|
+
};
|
|
68
|
+
// Determine which tools to create
|
|
69
|
+
const toolsToCreate = config.enabledTools ?? PLAN_TOOL_NAMES;
|
|
70
|
+
if (config.enabledTools) {
|
|
71
|
+
logger.debug(`Creating subset of plan tools: ${toolsToCreate.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
// Create and return only the enabled tools
|
|
74
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
75
|
+
},
|
|
76
|
+
metadata: {
|
|
77
|
+
displayName: 'Plan Tools',
|
|
78
|
+
description: 'Create and manage implementation plans linked to sessions',
|
|
79
|
+
category: 'planning',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-provider.test.d.ts","sourceRoot":"","sources":["../src/tool-provider.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Tools Provider Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the planToolsProvider configuration and tool creation.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { planToolsProvider } from './tool-provider.js';
|
|
11
|
+
// Create mock logger
|
|
12
|
+
const createMockLogger = () => ({
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
info: vi.fn(),
|
|
15
|
+
warn: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
createChild: vi.fn().mockReturnThis(),
|
|
18
|
+
});
|
|
19
|
+
// Create mock context with logger and minimal agent
|
|
20
|
+
const createMockContext = (logger) => ({
|
|
21
|
+
logger: logger,
|
|
22
|
+
agent: {}, // Minimal mock - provider only uses logger
|
|
23
|
+
});
|
|
24
|
+
describe('planToolsProvider', () => {
|
|
25
|
+
let mockLogger;
|
|
26
|
+
let tempDir;
|
|
27
|
+
let originalCwd;
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
mockLogger = createMockLogger();
|
|
30
|
+
// Create temp directory for testing
|
|
31
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dexto-provider-test-'));
|
|
32
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
33
|
+
// Store original cwd and mock process.cwd to return temp dir
|
|
34
|
+
originalCwd = process.cwd();
|
|
35
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
// Restore mocked process.cwd
|
|
40
|
+
vi.mocked(process.cwd).mockRestore();
|
|
41
|
+
try {
|
|
42
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore cleanup errors
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
describe('provider metadata', () => {
|
|
49
|
+
it('should have correct type', () => {
|
|
50
|
+
expect(planToolsProvider.type).toBe('plan-tools');
|
|
51
|
+
});
|
|
52
|
+
it('should have metadata', () => {
|
|
53
|
+
expect(planToolsProvider.metadata).toBeDefined();
|
|
54
|
+
expect(planToolsProvider.metadata?.displayName).toBe('Plan Tools');
|
|
55
|
+
expect(planToolsProvider.metadata?.category).toBe('planning');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('config schema', () => {
|
|
59
|
+
it('should validate minimal config', () => {
|
|
60
|
+
const result = planToolsProvider.configSchema.safeParse({
|
|
61
|
+
type: 'plan-tools',
|
|
62
|
+
});
|
|
63
|
+
expect(result.success).toBe(true);
|
|
64
|
+
if (result.success) {
|
|
65
|
+
expect(result.data.basePath).toBe('.dexto/plans');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
it('should validate config with custom basePath', () => {
|
|
69
|
+
const result = planToolsProvider.configSchema.safeParse({
|
|
70
|
+
type: 'plan-tools',
|
|
71
|
+
basePath: '/custom/path',
|
|
72
|
+
});
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
if (result.success) {
|
|
75
|
+
expect(result.data.basePath).toBe('/custom/path');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
it('should validate config with enabledTools', () => {
|
|
79
|
+
const result = planToolsProvider.configSchema.safeParse({
|
|
80
|
+
type: 'plan-tools',
|
|
81
|
+
enabledTools: ['plan_create', 'plan_read'],
|
|
82
|
+
});
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
if (result.success) {
|
|
85
|
+
expect(result.data.enabledTools).toEqual(['plan_create', 'plan_read']);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it('should reject invalid tool names', () => {
|
|
89
|
+
const result = planToolsProvider.configSchema.safeParse({
|
|
90
|
+
type: 'plan-tools',
|
|
91
|
+
enabledTools: ['invalid_tool'],
|
|
92
|
+
});
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it('should reject unknown properties', () => {
|
|
96
|
+
const result = planToolsProvider.configSchema.safeParse({
|
|
97
|
+
type: 'plan-tools',
|
|
98
|
+
unknownProp: 'value',
|
|
99
|
+
});
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('create', () => {
|
|
104
|
+
it('should create all tools by default', () => {
|
|
105
|
+
const config = planToolsProvider.configSchema.parse({
|
|
106
|
+
type: 'plan-tools',
|
|
107
|
+
});
|
|
108
|
+
const tools = planToolsProvider.create(config, createMockContext(mockLogger));
|
|
109
|
+
expect(tools).toHaveLength(4);
|
|
110
|
+
const toolIds = tools.map((t) => t.id);
|
|
111
|
+
expect(toolIds).toContain('plan_create');
|
|
112
|
+
expect(toolIds).toContain('plan_read');
|
|
113
|
+
expect(toolIds).toContain('plan_update');
|
|
114
|
+
expect(toolIds).toContain('plan_review');
|
|
115
|
+
});
|
|
116
|
+
it('should create only enabled tools', () => {
|
|
117
|
+
const config = planToolsProvider.configSchema.parse({
|
|
118
|
+
type: 'plan-tools',
|
|
119
|
+
enabledTools: ['plan_create', 'plan_read'],
|
|
120
|
+
});
|
|
121
|
+
const tools = planToolsProvider.create(config, createMockContext(mockLogger));
|
|
122
|
+
expect(tools).toHaveLength(2);
|
|
123
|
+
const toolIds = tools.map((t) => t.id);
|
|
124
|
+
expect(toolIds).toContain('plan_create');
|
|
125
|
+
expect(toolIds).toContain('plan_read');
|
|
126
|
+
expect(toolIds).not.toContain('plan_update');
|
|
127
|
+
});
|
|
128
|
+
it('should create single tool', () => {
|
|
129
|
+
const config = planToolsProvider.configSchema.parse({
|
|
130
|
+
type: 'plan-tools',
|
|
131
|
+
enabledTools: ['plan_update'],
|
|
132
|
+
});
|
|
133
|
+
const tools = planToolsProvider.create(config, createMockContext(mockLogger));
|
|
134
|
+
expect(tools).toHaveLength(1);
|
|
135
|
+
expect(tools[0].id).toBe('plan_update');
|
|
136
|
+
});
|
|
137
|
+
it('should use relative basePath from cwd', () => {
|
|
138
|
+
const config = planToolsProvider.configSchema.parse({
|
|
139
|
+
type: 'plan-tools',
|
|
140
|
+
basePath: '.dexto/plans',
|
|
141
|
+
});
|
|
142
|
+
planToolsProvider.create(config, createMockContext(mockLogger));
|
|
143
|
+
// Verify debug log was called with resolved path
|
|
144
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining(path.join(tempDir, '.dexto/plans')));
|
|
145
|
+
});
|
|
146
|
+
it('should use absolute basePath as-is', () => {
|
|
147
|
+
const absolutePath = '/absolute/path/to/plans';
|
|
148
|
+
const config = planToolsProvider.configSchema.parse({
|
|
149
|
+
type: 'plan-tools',
|
|
150
|
+
basePath: absolutePath,
|
|
151
|
+
});
|
|
152
|
+
planToolsProvider.create(config, createMockContext(mockLogger));
|
|
153
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining(absolutePath));
|
|
154
|
+
});
|
|
155
|
+
it('should log when creating subset of tools', () => {
|
|
156
|
+
const config = planToolsProvider.configSchema.parse({
|
|
157
|
+
type: 'plan-tools',
|
|
158
|
+
enabledTools: ['plan_create'],
|
|
159
|
+
});
|
|
160
|
+
planToolsProvider.create(config, createMockContext(mockLogger));
|
|
161
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('Creating subset of plan tools'));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('tool descriptions', () => {
|
|
165
|
+
it('should have descriptions for all tools', () => {
|
|
166
|
+
const config = planToolsProvider.configSchema.parse({
|
|
167
|
+
type: 'plan-tools',
|
|
168
|
+
});
|
|
169
|
+
const tools = planToolsProvider.create(config, createMockContext(mockLogger));
|
|
170
|
+
for (const tool of tools) {
|
|
171
|
+
expect(tool.description).toBeDefined();
|
|
172
|
+
expect(tool.description.length).toBeGreaterThan(0);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
it('should have input schemas for all tools', () => {
|
|
176
|
+
const config = planToolsProvider.configSchema.parse({
|
|
177
|
+
type: 'plan-tools',
|
|
178
|
+
});
|
|
179
|
+
const tools = planToolsProvider.create(config, createMockContext(mockLogger));
|
|
180
|
+
for (const tool of tools) {
|
|
181
|
+
expect(tool.inputSchema).toBeDefined();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Create Tool
|
|
3
|
+
*
|
|
4
|
+
* Creates a new implementation plan for the current session.
|
|
5
|
+
* Shows a preview for approval before saving.
|
|
6
|
+
*/
|
|
7
|
+
import type { InternalTool } from '@dexto/core';
|
|
8
|
+
import type { PlanService } from '../plan-service.js';
|
|
9
|
+
/**
|
|
10
|
+
* Creates the plan_create tool
|
|
11
|
+
*/
|
|
12
|
+
export declare function createPlanCreateTool(planService: PlanService): InternalTool;
|
|
13
|
+
//# sourceMappingURL=plan-create-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-create-tool.d.ts","sourceRoot":"","sources":["../../src/tools/plan-create-tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAyC,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAgBtD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,WAAW,GAAG,YAAY,CAgE3E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Create Tool
|
|
3
|
+
*
|
|
4
|
+
* Creates a new implementation plan for the current session.
|
|
5
|
+
* Shows a preview for approval before saving.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { PlanError } from '../errors.js';
|
|
9
|
+
const PlanCreateInputSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
title: z.string().describe('Plan title (e.g., "Add User Authentication")'),
|
|
12
|
+
content: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('Plan content in markdown format. Use - [ ] and - [x] for checkboxes to track progress.'),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
/**
|
|
18
|
+
* Creates the plan_create tool
|
|
19
|
+
*/
|
|
20
|
+
export function createPlanCreateTool(planService) {
|
|
21
|
+
return {
|
|
22
|
+
id: 'plan_create',
|
|
23
|
+
description: 'Create a new implementation plan for the current session. Shows the plan for approval before saving. Use markdown format for the plan content with clear steps and file references.',
|
|
24
|
+
inputSchema: PlanCreateInputSchema,
|
|
25
|
+
/**
|
|
26
|
+
* Generate preview for approval UI
|
|
27
|
+
*/
|
|
28
|
+
generatePreview: async (input, context) => {
|
|
29
|
+
const { content } = input;
|
|
30
|
+
if (!context?.sessionId) {
|
|
31
|
+
throw PlanError.sessionIdRequired();
|
|
32
|
+
}
|
|
33
|
+
// Check if plan already exists
|
|
34
|
+
const exists = await planService.exists(context.sessionId);
|
|
35
|
+
if (exists) {
|
|
36
|
+
throw PlanError.planAlreadyExists(context.sessionId);
|
|
37
|
+
}
|
|
38
|
+
// Return preview for approval UI
|
|
39
|
+
const lineCount = content.split('\n').length;
|
|
40
|
+
const planPath = planService.getPlanPath(context.sessionId);
|
|
41
|
+
return {
|
|
42
|
+
type: 'file',
|
|
43
|
+
path: planPath,
|
|
44
|
+
operation: 'create',
|
|
45
|
+
content,
|
|
46
|
+
size: content.length,
|
|
47
|
+
lineCount,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
execute: async (input, context) => {
|
|
51
|
+
const { title, content } = input;
|
|
52
|
+
if (!context?.sessionId) {
|
|
53
|
+
throw PlanError.sessionIdRequired();
|
|
54
|
+
}
|
|
55
|
+
const plan = await planService.create(context.sessionId, content, { title });
|
|
56
|
+
const planPath = planService.getPlanPath(context.sessionId);
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
path: planPath,
|
|
60
|
+
status: plan.meta.status,
|
|
61
|
+
title: plan.meta.title,
|
|
62
|
+
_display: {
|
|
63
|
+
type: 'file',
|
|
64
|
+
path: planPath,
|
|
65
|
+
operation: 'create',
|
|
66
|
+
size: content.length,
|
|
67
|
+
lineCount: content.split('\n').length,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-create-tool.test.d.ts","sourceRoot":"","sources":["../../src/tools/plan-create-tool.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|