@dexto/tools-plan 1.6.0 → 1.6.2

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.
Files changed (38) hide show
  1. package/dist/errors.cjs +126 -0
  2. package/dist/errors.js +99 -64
  3. package/dist/index.cjs +36 -0
  4. package/dist/index.d.cts +224 -0
  5. package/dist/index.js +9 -12
  6. package/dist/plan-service-getter.cjs +16 -0
  7. package/dist/plan-service-getter.js +0 -1
  8. package/dist/plan-service.cjs +247 -0
  9. package/dist/plan-service.js +201 -215
  10. package/dist/plan-service.test.cjs +227 -0
  11. package/dist/plan-service.test.js +200 -222
  12. package/dist/tool-factory-config.cjs +38 -0
  13. package/dist/tool-factory-config.js +13 -30
  14. package/dist/tool-factory.cjs +71 -0
  15. package/dist/tool-factory.js +39 -35
  16. package/dist/tool-factory.test.cjs +96 -0
  17. package/dist/tool-factory.test.js +90 -95
  18. package/dist/tools/plan-create-tool.cjs +102 -0
  19. package/dist/tools/plan-create-tool.d.ts.map +1 -1
  20. package/dist/tools/plan-create-tool.js +77 -82
  21. package/dist/tools/plan-create-tool.test.cjs +174 -0
  22. package/dist/tools/plan-create-tool.test.js +142 -134
  23. package/dist/tools/plan-read-tool.cjs +65 -0
  24. package/dist/tools/plan-read-tool.d.ts.map +1 -1
  25. package/dist/tools/plan-read-tool.js +39 -41
  26. package/dist/tools/plan-read-tool.test.cjs +109 -0
  27. package/dist/tools/plan-read-tool.test.js +78 -87
  28. package/dist/tools/plan-review-tool.cjs +98 -0
  29. package/dist/tools/plan-review-tool.d.ts.map +1 -1
  30. package/dist/tools/plan-review-tool.js +73 -87
  31. package/dist/tools/plan-update-tool.cjs +92 -0
  32. package/dist/tools/plan-update-tool.d.ts.map +1 -1
  33. package/dist/tools/plan-update-tool.js +65 -73
  34. package/dist/tools/plan-update-tool.test.cjs +203 -0
  35. package/dist/tools/plan-update-tool.test.js +171 -154
  36. package/dist/types.cjs +44 -0
  37. package/dist/types.js +17 -24
  38. package/package.json +8 -7
@@ -1,88 +1,74 @@
1
- /**
2
- * Plan Review Tool
3
- *
4
- * Requests user review of the current plan.
5
- * Shows the plan content for review with approval options:
6
- * - Approve: Proceed with implementation
7
- * - Approve + Accept Edits: Proceed and auto-approve file edits
8
- * - Request Changes: Provide feedback for iteration
9
- * - Reject: Reject the plan entirely
10
- *
11
- * Uses the tool approval pattern (not elicitation) so the user
12
- * can see the full plan content before deciding.
13
- */
14
- import { z } from 'zod';
15
- import { defineTool } from '@dexto/core';
16
- import { PlanError } from '../errors.js';
17
- const PlanReviewInputSchema = z
18
- .object({
19
- summary: z
20
- .string()
21
- .optional()
22
- .describe('Brief summary of the plan for context (shown above the plan content)'),
23
- })
24
- .strict();
25
- /**
26
- * Creates the plan_review tool
27
- *
28
- * @param getPlanService - Getter for the plan service
29
- */
30
- export function createPlanReviewTool(getPlanService) {
31
- return defineTool({
32
- id: 'plan_review',
33
- displayName: 'Review Plan',
34
- description: 'Request user review of the current plan. Shows the full plan content for review with options to approve, request changes, or reject. Use after creating or updating a plan to get user approval before implementation.',
35
- inputSchema: PlanReviewInputSchema,
36
- /**
37
- * Generate preview showing the plan content for review.
38
- * The ApprovalPrompt component detects plan_review and shows custom options.
39
- */
40
- generatePreview: async (input, context) => {
41
- const resolvedPlanService = await getPlanService(context);
42
- const { summary } = input;
43
- if (!context.sessionId) {
44
- throw PlanError.sessionIdRequired();
45
- }
46
- // Read the current plan
47
- const plan = await resolvedPlanService.read(context.sessionId);
48
- if (!plan) {
49
- throw PlanError.planNotFound(context.sessionId);
50
- }
51
- // Build content with optional summary header
52
- let displayContent = plan.content;
53
- if (summary) {
54
- displayContent = `## Summary\n${summary}\n\n---\n\n${plan.content}`;
55
- }
56
- const lineCount = displayContent.split('\n').length;
57
- const planPath = resolvedPlanService.getPlanPath(context.sessionId);
58
- return {
59
- type: 'file',
60
- path: planPath,
61
- operation: 'read', // 'read' indicates this is for viewing, not creating/modifying
62
- content: displayContent,
63
- size: Buffer.byteLength(displayContent, 'utf8'),
64
- lineCount,
65
- };
66
- },
67
- async execute(_input, context) {
68
- const resolvedPlanService = await getPlanService(context);
69
- // Tool execution means user approved the plan (selected Approve or Approve + Accept Edits)
70
- // Request Changes and Reject are handled as denials in the approval flow
71
- if (!context.sessionId) {
72
- throw PlanError.sessionIdRequired();
73
- }
74
- // Read plan to verify it still exists
75
- const plan = await resolvedPlanService.read(context.sessionId);
76
- if (!plan) {
77
- throw PlanError.planNotFound(context.sessionId);
78
- }
79
- // Update plan status to approved
80
- await resolvedPlanService.updateMeta(context.sessionId, { status: 'approved' });
81
- return {
82
- approved: true,
83
- message: 'Plan approved. You may now proceed with implementation.',
84
- planStatus: 'approved',
85
- };
86
- },
87
- });
1
+ import { z } from "zod";
2
+ import { createLocalToolCallHeader, defineTool, truncateForHeader } from "@dexto/core";
3
+ import { PlanError } from "../errors.js";
4
+ const PlanReviewInputSchema = z.object({
5
+ summary: z.string().optional().describe("Brief summary of the plan for context (shown above the plan content)")
6
+ }).strict();
7
+ function createPlanReviewTool(getPlanService) {
8
+ return defineTool({
9
+ id: "plan_review",
10
+ description: "Request user review of the current plan. Shows the full plan content for review with options to approve, request changes, or reject. Use after creating or updating a plan to get user approval before implementation.",
11
+ inputSchema: PlanReviewInputSchema,
12
+ presentation: {
13
+ describeHeader: (input) => createLocalToolCallHeader({
14
+ title: "Review Plan",
15
+ ...input.summary ? { argsText: truncateForHeader(input.summary, 140) } : {}
16
+ }),
17
+ /**
18
+ * Generate preview showing the plan content for review.
19
+ * The ApprovalPrompt component detects plan_review and shows custom options.
20
+ */
21
+ preview: async (input, context) => {
22
+ const resolvedPlanService = await getPlanService(context);
23
+ const { summary } = input;
24
+ if (!context.sessionId) {
25
+ throw PlanError.sessionIdRequired();
26
+ }
27
+ const plan = await resolvedPlanService.read(context.sessionId);
28
+ if (!plan) {
29
+ throw PlanError.planNotFound(context.sessionId);
30
+ }
31
+ let displayContent = plan.content;
32
+ if (summary) {
33
+ displayContent = `## Summary
34
+ ${summary}
35
+
36
+ ---
37
+
38
+ ${plan.content}`;
39
+ }
40
+ const lineCount = displayContent.split("\n").length;
41
+ const planPath = resolvedPlanService.getPlanPath(context.sessionId);
42
+ return {
43
+ type: "file",
44
+ title: "Review Plan",
45
+ path: planPath,
46
+ operation: "read",
47
+ // 'read' indicates this is for viewing, not creating/modifying
48
+ content: displayContent,
49
+ size: Buffer.byteLength(displayContent, "utf8"),
50
+ lineCount
51
+ };
52
+ }
53
+ },
54
+ async execute(_input, context) {
55
+ const resolvedPlanService = await getPlanService(context);
56
+ if (!context.sessionId) {
57
+ throw PlanError.sessionIdRequired();
58
+ }
59
+ const plan = await resolvedPlanService.read(context.sessionId);
60
+ if (!plan) {
61
+ throw PlanError.planNotFound(context.sessionId);
62
+ }
63
+ await resolvedPlanService.updateMeta(context.sessionId, { status: "approved" });
64
+ return {
65
+ approved: true,
66
+ message: "Plan approved. You may now proceed with implementation.",
67
+ planStatus: "approved"
68
+ };
69
+ }
70
+ });
88
71
  }
72
+ export {
73
+ createPlanReviewTool
74
+ };
@@ -0,0 +1,92 @@
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 plan_update_tool_exports = {};
20
+ __export(plan_update_tool_exports, {
21
+ createPlanUpdateTool: () => createPlanUpdateTool
22
+ });
23
+ module.exports = __toCommonJS(plan_update_tool_exports);
24
+ var import_zod = require("zod");
25
+ var import_diff = require("diff");
26
+ var import_core = require("@dexto/core");
27
+ var import_errors = require("../errors.js");
28
+ const PlanUpdateInputSchema = import_zod.z.object({
29
+ content: import_zod.z.string().describe("Updated plan content in markdown format")
30
+ }).strict();
31
+ function generateDiffPreview(filePath, originalContent, newContent) {
32
+ const unified = (0, import_diff.createPatch)(filePath, originalContent, newContent, "before", "after", {
33
+ context: 3
34
+ });
35
+ const additions = (unified.match(/^\+[^+]/gm) || []).length;
36
+ const deletions = (unified.match(/^-[^-]/gm) || []).length;
37
+ return {
38
+ type: "diff",
39
+ title: "Update Plan",
40
+ unified,
41
+ filename: filePath,
42
+ additions,
43
+ deletions
44
+ };
45
+ }
46
+ function createPlanUpdateTool(getPlanService) {
47
+ return (0, import_core.defineTool)({
48
+ id: "plan_update",
49
+ description: "Update the existing implementation plan for this session. Shows a diff preview for approval before saving. The plan must already exist (use plan_create first).",
50
+ inputSchema: PlanUpdateInputSchema,
51
+ presentation: {
52
+ describeHeader: () => (0, import_core.createLocalToolCallHeader)({
53
+ title: "Update Plan"
54
+ }),
55
+ /**
56
+ * Generate diff preview for approval UI
57
+ */
58
+ preview: async (input, context) => {
59
+ const resolvedPlanService = await getPlanService(context);
60
+ const { content: newContent } = input;
61
+ if (!context.sessionId) {
62
+ throw import_errors.PlanError.sessionIdRequired();
63
+ }
64
+ const existing = await resolvedPlanService.read(context.sessionId);
65
+ if (!existing) {
66
+ throw import_errors.PlanError.planNotFound(context.sessionId);
67
+ }
68
+ const planPath = resolvedPlanService.getPlanPath(context.sessionId);
69
+ return generateDiffPreview(planPath, existing.content, newContent);
70
+ }
71
+ },
72
+ async execute(input, context) {
73
+ const resolvedPlanService = await getPlanService(context);
74
+ const { content } = input;
75
+ if (!context.sessionId) {
76
+ throw import_errors.PlanError.sessionIdRequired();
77
+ }
78
+ const result = await resolvedPlanService.update(context.sessionId, content);
79
+ const planPath = resolvedPlanService.getPlanPath(context.sessionId);
80
+ return {
81
+ success: true,
82
+ path: planPath,
83
+ status: result.meta.status,
84
+ _display: generateDiffPreview(planPath, result.oldContent, result.newContent)
85
+ };
86
+ }
87
+ });
88
+ }
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ createPlanUpdateTool
92
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"plan-update-tool.d.ts","sourceRoot":"","sources":["../../src/tools/plan-update-tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,IAAI,EAAyC,MAAM,aAAa,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE,QAAA,MAAM,qBAAqB;;;;;;EAId,CAAC;AAyBd;;GAEG;AACH,wBAAgB,oBAAoB,CAChC,cAAc,EAAE,iBAAiB,GAClC,IAAI,CAAC,OAAO,qBAAqB,CAAC,CAiDpC"}
1
+ {"version":3,"file":"plan-update-tool.d.ts","sourceRoot":"","sources":["../../src/tools/plan-update-tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,IAAI,EAAyC,MAAM,aAAa,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE,QAAA,MAAM,qBAAqB;;;;;;EAId,CAAC;AA0Bd;;GAEG;AACH,wBAAgB,oBAAoB,CAChC,cAAc,EAAE,iBAAiB,GAClC,IAAI,CAAC,OAAO,qBAAqB,CAAC,CAsDpC"}
@@ -1,76 +1,68 @@
1
- /**
2
- * Plan Update Tool
3
- *
4
- * Updates the implementation plan for the current session.
5
- * Shows a diff preview for approval before saving.
6
- */
7
- import { z } from 'zod';
8
- import { createPatch } from 'diff';
9
- import { defineTool } from '@dexto/core';
10
- import { PlanError } from '../errors.js';
11
- const PlanUpdateInputSchema = z
12
- .object({
13
- content: z.string().describe('Updated plan content in markdown format'),
14
- })
15
- .strict();
16
- /**
17
- * Generate diff preview for plan update
18
- */
1
+ import { z } from "zod";
2
+ import { createPatch } from "diff";
3
+ import { createLocalToolCallHeader, defineTool } from "@dexto/core";
4
+ import { PlanError } from "../errors.js";
5
+ const PlanUpdateInputSchema = z.object({
6
+ content: z.string().describe("Updated plan content in markdown format")
7
+ }).strict();
19
8
  function generateDiffPreview(filePath, originalContent, newContent) {
20
- const unified = createPatch(filePath, originalContent, newContent, 'before', 'after', {
21
- context: 3,
22
- });
23
- const additions = (unified.match(/^\+[^+]/gm) || []).length;
24
- const deletions = (unified.match(/^-[^-]/gm) || []).length;
25
- return {
26
- type: 'diff',
27
- unified,
28
- filename: filePath,
29
- additions,
30
- deletions,
31
- };
9
+ const unified = createPatch(filePath, originalContent, newContent, "before", "after", {
10
+ context: 3
11
+ });
12
+ const additions = (unified.match(/^\+[^+]/gm) || []).length;
13
+ const deletions = (unified.match(/^-[^-]/gm) || []).length;
14
+ return {
15
+ type: "diff",
16
+ title: "Update Plan",
17
+ unified,
18
+ filename: filePath,
19
+ additions,
20
+ deletions
21
+ };
32
22
  }
33
- /**
34
- * Creates the plan_update tool
35
- */
36
- export function createPlanUpdateTool(getPlanService) {
37
- return defineTool({
38
- id: 'plan_update',
39
- displayName: 'Update Plan',
40
- description: 'Update the existing implementation plan for this session. Shows a diff preview for approval before saving. The plan must already exist (use plan_create first).',
41
- inputSchema: PlanUpdateInputSchema,
42
- /**
43
- * Generate diff preview for approval UI
44
- */
45
- generatePreview: async (input, context) => {
46
- const resolvedPlanService = await getPlanService(context);
47
- const { content: newContent } = input;
48
- if (!context.sessionId) {
49
- throw PlanError.sessionIdRequired();
50
- }
51
- // Read existing plan
52
- const existing = await resolvedPlanService.read(context.sessionId);
53
- if (!existing) {
54
- throw PlanError.planNotFound(context.sessionId);
55
- }
56
- // Generate diff preview
57
- const planPath = resolvedPlanService.getPlanPath(context.sessionId);
58
- return generateDiffPreview(planPath, existing.content, newContent);
59
- },
60
- async execute(input, context) {
61
- const resolvedPlanService = await getPlanService(context);
62
- const { content } = input;
63
- if (!context.sessionId) {
64
- throw PlanError.sessionIdRequired();
65
- }
66
- const result = await resolvedPlanService.update(context.sessionId, content);
67
- const planPath = resolvedPlanService.getPlanPath(context.sessionId);
68
- return {
69
- success: true,
70
- path: planPath,
71
- status: result.meta.status,
72
- _display: generateDiffPreview(planPath, result.oldContent, result.newContent),
73
- };
74
- },
75
- });
23
+ function createPlanUpdateTool(getPlanService) {
24
+ return defineTool({
25
+ id: "plan_update",
26
+ description: "Update the existing implementation plan for this session. Shows a diff preview for approval before saving. The plan must already exist (use plan_create first).",
27
+ inputSchema: PlanUpdateInputSchema,
28
+ presentation: {
29
+ describeHeader: () => createLocalToolCallHeader({
30
+ title: "Update Plan"
31
+ }),
32
+ /**
33
+ * Generate diff preview for approval UI
34
+ */
35
+ preview: async (input, context) => {
36
+ const resolvedPlanService = await getPlanService(context);
37
+ const { content: newContent } = input;
38
+ if (!context.sessionId) {
39
+ throw PlanError.sessionIdRequired();
40
+ }
41
+ const existing = await resolvedPlanService.read(context.sessionId);
42
+ if (!existing) {
43
+ throw PlanError.planNotFound(context.sessionId);
44
+ }
45
+ const planPath = resolvedPlanService.getPlanPath(context.sessionId);
46
+ return generateDiffPreview(planPath, existing.content, newContent);
47
+ }
48
+ },
49
+ async execute(input, context) {
50
+ const resolvedPlanService = await getPlanService(context);
51
+ const { content } = input;
52
+ if (!context.sessionId) {
53
+ throw PlanError.sessionIdRequired();
54
+ }
55
+ const result = await resolvedPlanService.update(context.sessionId, content);
56
+ const planPath = resolvedPlanService.getPlanPath(context.sessionId);
57
+ return {
58
+ success: true,
59
+ path: planPath,
60
+ status: result.meta.status,
61
+ _display: generateDiffPreview(planPath, result.oldContent, result.newContent)
62
+ };
63
+ }
64
+ });
76
65
  }
66
+ export {
67
+ createPlanUpdateTool
68
+ };
@@ -0,0 +1,203 @@
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 __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var import_vitest = require("vitest");
25
+ var path = __toESM(require("node:path"), 1);
26
+ var fs = __toESM(require("node:fs/promises"), 1);
27
+ var os = __toESM(require("node:os"), 1);
28
+ var import_plan_update_tool = require("./plan-update-tool.js");
29
+ var import_plan_service = require("../plan-service.js");
30
+ var import_errors = require("../errors.js");
31
+ var import_core = require("@dexto/core");
32
+ const createMockLogger = () => {
33
+ const logger = {
34
+ debug: import_vitest.vi.fn(),
35
+ silly: import_vitest.vi.fn(),
36
+ info: import_vitest.vi.fn(),
37
+ warn: import_vitest.vi.fn(),
38
+ error: import_vitest.vi.fn(),
39
+ trackException: import_vitest.vi.fn(),
40
+ createChild: import_vitest.vi.fn(() => logger),
41
+ createFileOnlyChild: import_vitest.vi.fn(() => logger),
42
+ setLevel: import_vitest.vi.fn(),
43
+ getLevel: import_vitest.vi.fn(() => "debug"),
44
+ getLogFilePath: import_vitest.vi.fn(() => null),
45
+ destroy: import_vitest.vi.fn(async () => void 0)
46
+ };
47
+ return logger;
48
+ };
49
+ function createToolContext(logger, overrides = {}) {
50
+ return { logger, ...overrides };
51
+ }
52
+ (0, import_vitest.describe)("plan_update tool", () => {
53
+ let logger;
54
+ let tempDir;
55
+ let planService;
56
+ (0, import_vitest.beforeEach)(async () => {
57
+ logger = createMockLogger();
58
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-plan-update-test-"));
59
+ tempDir = await fs.realpath(rawTempDir);
60
+ planService = new import_plan_service.PlanService({ basePath: tempDir }, logger);
61
+ import_vitest.vi.clearAllMocks();
62
+ });
63
+ (0, import_vitest.afterEach)(async () => {
64
+ try {
65
+ await fs.rm(tempDir, { recursive: true, force: true });
66
+ } catch {
67
+ }
68
+ });
69
+ (0, import_vitest.describe)("generatePreview", () => {
70
+ (0, import_vitest.it)("should return DiffDisplayData with unified diff", async () => {
71
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
72
+ const sessionId = "test-session";
73
+ const originalContent = "# Plan\n\n## Steps\n1. First step";
74
+ const newContent = "# Plan\n\n## Steps\n1. First step\n2. Second step";
75
+ const previewFn = tool.presentation?.preview;
76
+ (0, import_vitest.expect)(previewFn).toBeDefined();
77
+ await planService.create(sessionId, originalContent);
78
+ const preview = await previewFn(
79
+ { content: newContent },
80
+ createToolContext(logger, { sessionId })
81
+ );
82
+ (0, import_vitest.expect)(preview.type).toBe("diff");
83
+ (0, import_vitest.expect)(preview.title).toBe("Update Plan");
84
+ (0, import_vitest.expect)(preview.filename).toContain(sessionId);
85
+ (0, import_vitest.expect)(preview.filename).toMatch(/plan\.md$/);
86
+ (0, import_vitest.expect)(preview.unified).toContain("-1. First step");
87
+ (0, import_vitest.expect)(preview.unified).toContain("+1. First step");
88
+ (0, import_vitest.expect)(preview.unified).toContain("+2. Second step");
89
+ (0, import_vitest.expect)(preview.additions).toBeGreaterThan(0);
90
+ });
91
+ (0, import_vitest.it)("should throw error when plan does not exist", async () => {
92
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
93
+ const sessionId = "test-session";
94
+ const previewFn = tool.presentation?.preview;
95
+ (0, import_vitest.expect)(previewFn).toBeDefined();
96
+ try {
97
+ await previewFn(
98
+ { content: "# New Content" },
99
+ createToolContext(logger, { sessionId })
100
+ );
101
+ import_vitest.expect.fail("Should have thrown an error");
102
+ } catch (error) {
103
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
104
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
105
+ }
106
+ });
107
+ (0, import_vitest.it)("should throw error when sessionId is missing", async () => {
108
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
109
+ const previewFn = tool.presentation?.preview;
110
+ (0, import_vitest.expect)(previewFn).toBeDefined();
111
+ try {
112
+ await previewFn({ content: "# Content" }, createToolContext(logger));
113
+ import_vitest.expect.fail("Should have thrown an error");
114
+ } catch (error) {
115
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
116
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.SESSION_ID_REQUIRED);
117
+ }
118
+ });
119
+ (0, import_vitest.it)("should show deletions in diff", async () => {
120
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
121
+ const sessionId = "test-session";
122
+ const originalContent = "# Plan\n\nLine to remove\nKeep this";
123
+ const newContent = "# Plan\n\nKeep this";
124
+ const previewFn = tool.presentation?.preview;
125
+ (0, import_vitest.expect)(previewFn).toBeDefined();
126
+ await planService.create(sessionId, originalContent);
127
+ const preview = await previewFn(
128
+ { content: newContent },
129
+ createToolContext(logger, { sessionId })
130
+ );
131
+ (0, import_vitest.expect)(preview.deletions).toBeGreaterThan(0);
132
+ (0, import_vitest.expect)(preview.unified).toContain("-Line to remove");
133
+ });
134
+ });
135
+ (0, import_vitest.describe)("execute", () => {
136
+ (0, import_vitest.it)("should update plan content and return success", async () => {
137
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
138
+ const sessionId = "test-session";
139
+ const originalContent = "# Original Plan";
140
+ const newContent = "# Updated Plan";
141
+ await planService.create(sessionId, originalContent);
142
+ const result = await tool.execute(
143
+ { content: newContent },
144
+ createToolContext(logger, { sessionId })
145
+ );
146
+ (0, import_vitest.expect)(result.success).toBe(true);
147
+ (0, import_vitest.expect)(result.path).toContain(sessionId);
148
+ (0, import_vitest.expect)(result.path).toMatch(/plan\.md$/);
149
+ const plan = await planService.read(sessionId);
150
+ (0, import_vitest.expect)(plan.content).toBe(newContent);
151
+ });
152
+ (0, import_vitest.it)("should include _display data with diff", async () => {
153
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
154
+ const sessionId = "test-session";
155
+ await planService.create(sessionId, "# Original");
156
+ const result = await tool.execute(
157
+ { content: "# Updated" },
158
+ createToolContext(logger, { sessionId })
159
+ );
160
+ (0, import_vitest.expect)(result._display).toBeDefined();
161
+ (0, import_vitest.expect)(result._display.type).toBe("diff");
162
+ (0, import_vitest.expect)(result._display.title).toBe("Update Plan");
163
+ (0, import_vitest.expect)(result._display.unified).toContain("-# Original");
164
+ (0, import_vitest.expect)(result._display.unified).toContain("+# Updated");
165
+ });
166
+ (0, import_vitest.it)("should throw error when plan does not exist", async () => {
167
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
168
+ const sessionId = "non-existent";
169
+ try {
170
+ await tool.execute(
171
+ { content: "# Content" },
172
+ createToolContext(logger, { sessionId })
173
+ );
174
+ import_vitest.expect.fail("Should have thrown an error");
175
+ } catch (error) {
176
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
177
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
178
+ }
179
+ });
180
+ (0, import_vitest.it)("should throw error when sessionId is missing", async () => {
181
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
182
+ try {
183
+ await tool.execute({ content: "# Content" }, createToolContext(logger));
184
+ import_vitest.expect.fail("Should have thrown an error");
185
+ } catch (error) {
186
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
187
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.SESSION_ID_REQUIRED);
188
+ }
189
+ });
190
+ (0, import_vitest.it)("should preserve plan status after update", async () => {
191
+ const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
192
+ const sessionId = "test-session";
193
+ await planService.create(sessionId, "# Plan");
194
+ await planService.updateMeta(sessionId, { status: "approved" });
195
+ await tool.execute(
196
+ { content: "# Updated Plan" },
197
+ createToolContext(logger, { sessionId })
198
+ );
199
+ const plan = await planService.read(sessionId);
200
+ (0, import_vitest.expect)(plan.meta.status).toBe("approved");
201
+ });
202
+ });
203
+ });