@plannotator/opencode 0.19.17 → 0.19.18

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/index.js CHANGED
@@ -45044,7 +45044,7 @@ var init_opencode_sdk = __esm(() => {
45044
45044
 
45045
45045
  // index.ts
45046
45046
  import { tool } from "@opencode-ai/plugin";
45047
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
45047
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
45048
45048
  import path4 from "path";
45049
45049
 
45050
45050
  // ../../packages/server/remote.ts
@@ -67743,13 +67743,6 @@ var PLAN_TOOL_NAMES = {
67743
67743
  function getPlanToolName(runtime4) {
67744
67744
  return runtime4 && PLAN_TOOL_NAMES[runtime4] || "ExitPlanMode";
67745
67745
  }
67746
- function buildPlanFileRule(toolName, planFilePath) {
67747
- if (!planFilePath)
67748
- return "";
67749
- return `- Your plan is saved at: ${planFilePath}
67750
- You can edit this file to make targeted changes, then pass its path to ${toolName}.
67751
- `;
67752
- }
67753
67746
  var DEFAULT_REVIEW_APPROVED_PROMPT = `# Code Review
67754
67747
 
67755
67748
  Code review completed \u2014 no changes requested.`;
@@ -68762,21 +68755,63 @@ function getReviewHtml() {
68762
68755
  return _reviewHtml;
68763
68756
  }
68764
68757
  var DEFAULT_PLAN_TIMEOUT_SECONDS = 345600;
68765
- function isFilePath(value) {
68766
- return path4.isAbsolute(value) && value.endsWith(".md") && existsSync10(value);
68758
+ var MAX_PLAN_SIZE = 5 * 1024 * 1024;
68759
+ function getPlanBackingPath(directory) {
68760
+ const planDir = path4.join(directory, ".opencode", "plans");
68761
+ return path4.join(planDir, "_active-plan.md");
68762
+ }
68763
+ function applyEdits(existingLines, edits) {
68764
+ const sorted = [...edits].sort((a3, b3) => a3.start - b3.start);
68765
+ const lines = [...existingLines];
68766
+ let offset = 0;
68767
+ for (const edit of sorted) {
68768
+ const start = edit.start - 1 + offset;
68769
+ const end = edit.end != null ? edit.end + offset : lines.length;
68770
+ const newLines = edit.content ? edit.content.split(`
68771
+ `) : [];
68772
+ const removedCount = end - start;
68773
+ lines.splice(start, removedCount, ...newLines);
68774
+ offset += newLines.length - removedCount;
68775
+ }
68776
+ return lines;
68767
68777
  }
68768
- function resolvePlanContent(plan) {
68769
- if (isFilePath(plan)) {
68770
- const content = readFileSync7(plan, "utf-8");
68771
- if (!content.trim()) {
68772
- throw new Error(`Plan file at ${plan} is empty. Write your plan content first, then call submit_plan.`);
68778
+ function validateEdits(existingLines, edits) {
68779
+ const lineCount = existingLines.length;
68780
+ for (const edit of edits) {
68781
+ if (!Number.isInteger(edit.start) || edit.start < 1) {
68782
+ return `start must be a positive integer >= 1, got ${edit.start}`;
68783
+ }
68784
+ if (edit.start > lineCount + 1) {
68785
+ return `start (${edit.start}) exceeds file length + 1 (${lineCount + 1})`;
68786
+ }
68787
+ if (edit.end != null) {
68788
+ if (!Number.isInteger(edit.end) || edit.end < edit.start) {
68789
+ return `end (${edit.end}) must be >= start (${edit.start})`;
68790
+ }
68791
+ if (edit.end > lineCount) {
68792
+ return `end (${edit.end}) exceeds file length (${lineCount})`;
68793
+ }
68773
68794
  }
68774
- return { content, filePath: plan };
68775
68795
  }
68776
- if (path4.isAbsolute(plan) && plan.endsWith(".md")) {
68777
- throw new Error(`File not found: ${plan}. Check the path and try again.`);
68796
+ const sorted = [...edits].sort((a3, b3) => a3.start - b3.start);
68797
+ for (let i4 = 1;i4 < sorted.length; i4++) {
68798
+ const prev = sorted[i4 - 1];
68799
+ const curr = sorted[i4];
68800
+ if (prev.start > lineCount)
68801
+ continue;
68802
+ const prevEnd = prev.end ?? lineCount;
68803
+ if (curr.start <= prevEnd) {
68804
+ return `edits overlap: [${prev.start},${prevEnd}] and [${curr.start},${curr.end ?? "end"}]`;
68805
+ }
68778
68806
  }
68779
- return { content: plan };
68807
+ return null;
68808
+ }
68809
+ function formatWithLineNumbers(content) {
68810
+ const lines = content.split(`
68811
+ `);
68812
+ const width = String(lines.length).length;
68813
+ return lines.map((line, i4) => `${String(i4 + 1).padStart(width)}| ${line}`).join(`
68814
+ `);
68780
68815
  }
68781
68816
  function getPlanningPrompt() {
68782
68817
  return `## Plannotator \u2014 Plan Review
@@ -68785,10 +68820,26 @@ You have a plan submission tool called \`submit_plan\`. It opens an interactive
68785
68820
 
68786
68821
  **How to use it:**
68787
68822
 
68788
- - Pass your plan as markdown text \u2014 \`submit_plan(plan: "# My Plan\\n...")\`.
68789
- - Or pass an absolute file path to a .md file \u2014 \`submit_plan(plan: "/path/to/plan.md")\`.
68823
+ \`submit_plan\` accepts an array of line-range edits. On first submission, pass the full plan as a single edit starting at line 1:
68824
+
68825
+ \`\`\`json
68826
+ { "edits": [{ "start": 1, "content": "# My Plan\\n\\n## Goals\\n..." }] }
68827
+ \`\`\`
68828
+
68829
+ If the user denies and requests changes, apply surgical edits using line ranges. The tool response includes your plan with line numbers so you can target specific ranges:
68830
+
68831
+ \`\`\`json
68832
+ { "edits": [
68833
+ { "start": 12, "end": 14, "content": "revised section content" },
68834
+ { "start": 30, "end": 30, "content": "" }
68835
+ ] }
68836
+ \`\`\`
68790
68837
 
68791
- The tool auto-detects whether you passed text or a file path. Both open the same review UI.
68838
+ Edit semantics:
68839
+ - \`start\` and \`end\` are 1-indexed, inclusive line numbers
68840
+ - Omit \`end\` to replace from \`start\` through end of file (use this for the initial full write)
68841
+ - Empty \`content\` with \`start\`/\`end\` deletes those lines
68842
+ - Multiple edits in one call are applied in order; line numbers refer to the state before edits
68792
68843
 
68793
68844
  ### Before you write a plan
68794
68845
 
@@ -68956,9 +69007,9 @@ tools (except writing markdown files), or otherwise make changes to the system.
68956
69007
  return;
68957
69008
  output.system.push(`## Plan Submission
68958
69009
 
68959
- When you have completed your plan, call the \`submit_plan\` tool to submit it for user review. Pass your plan as markdown text, or pass an absolute file path to a .md file.
69010
+ When you have completed your plan, call the \`submit_plan\` tool to submit it for user review. Pass your full plan as a single edit: \`{ "edits": [{ "start": 1, "content": "..." }] }\`.
68960
69011
 
68961
- The user will review your plan in a visual UI where they can annotate, approve, or request changes. If rejected, revise based on their feedback and call submit_plan again.
69012
+ The user will review your plan in a visual UI where they can annotate, approve, or request changes. If rejected, the response includes your plan with line numbers; use targeted edits to revise specific sections.
68962
69013
 
68963
69014
  Do NOT proceed with implementation until your plan is approved.`);
68964
69015
  },
@@ -69007,9 +69058,13 @@ Do NOT proceed with implementation until your plan is approved.`);
69007
69058
  if (shouldRegisterSubmitPlan(workflowOptions)) {
69008
69059
  plugin.tool = {
69009
69060
  submit_plan: tool({
69010
- description: "Planning tool used to submit a plan to the user for review. Before calling this tool you must conduct interactive and exploratory analysis in order to submit a quality plan. Ask questions. Explore the codebase for context if needed. Only call submit_plan once you have enough details to create a quality plan. Work with the user to get those details. Pass either markdown text or an absolute path to a .md file.",
69061
+ description: "Submit a plan for user review via line-range edits. First call: pass a single edit with start=1 and your full plan as content (omit end). Subsequent calls after denial: pass targeted edits using the line numbers from the previous response. The tool manages a backing file; you never touch the file directly.",
69011
69062
  args: {
69012
- plan: tool.schema.string().describe("The plan \u2014 either markdown text or an absolute path to a .md file on disk.")
69063
+ edits: tool.schema.array(tool.schema.object({
69064
+ start: tool.schema.number().describe("1-indexed start line (inclusive)"),
69065
+ end: tool.schema.number().optional().describe("1-indexed end line (inclusive). Omit to replace from start through end of file."),
69066
+ content: tool.schema.string().describe("Replacement content. Empty string deletes the line range.")
69067
+ })).describe("Array of line-range edits to apply to the plan.")
69013
69068
  },
69014
69069
  async execute(args, context) {
69015
69070
  const invokingAgent = context.agent;
@@ -69018,18 +69073,37 @@ Do NOT proceed with implementation until your plan is approved.`);
69018
69073
 
69019
69074
  Use /plannotator-last or /plannotator-annotate for manual review, or set workflow to all-agents to allow broader submit_plan access.`;
69020
69075
  }
69021
- let planContent;
69022
- let sourceFilePath;
69076
+ if (!args.edits || args.edits.length === 0) {
69077
+ return "Error: No edits provided. Pass at least one edit with start and content.";
69078
+ }
69079
+ const backingPath = getPlanBackingPath(ctx.directory);
69080
+ const backingDir = path4.dirname(backingPath);
69081
+ mkdirSync8(backingDir, { recursive: true });
69082
+ let existingContent = "";
69083
+ if (existsSync10(backingPath)) {
69084
+ existingContent = readFileSync7(backingPath, "utf-8");
69085
+ }
69086
+ const existingLines = existingContent ? existingContent.split(`
69087
+ `) : [];
69088
+ const validationError = validateEdits(existingLines, args.edits);
69089
+ if (validationError) {
69090
+ return `Error: ${validationError}`;
69091
+ }
69092
+ let resultLines;
69023
69093
  try {
69024
- const resolved = resolvePlanContent(args.plan);
69025
- planContent = resolved.content;
69026
- sourceFilePath = resolved.filePath;
69094
+ resultLines = applyEdits(existingLines, args.edits);
69027
69095
  } catch (err) {
69028
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
69096
+ return `Error applying edits: ${err instanceof Error ? err.message : String(err)}`;
69097
+ }
69098
+ const planContent = resultLines.join(`
69099
+ `);
69100
+ if (planContent.length > MAX_PLAN_SIZE) {
69101
+ return `Error: Plan content exceeds the maximum size of ${MAX_PLAN_SIZE / (1024 * 1024)}MB.`;
69029
69102
  }
69030
69103
  if (!planContent.trim()) {
69031
- return "Error: Plan content is empty. Write your plan first, then call submit_plan.";
69104
+ return "Error: Plan content is empty after applying edits.";
69032
69105
  }
69106
+ writeFileSync5(backingPath, planContent, "utf-8");
69033
69107
  const sharingEnabled = await getSharingEnabled();
69034
69108
  const server2 = await startPlannotatorServer({
69035
69109
  plan: planContent,
@@ -69077,7 +69151,7 @@ Use /plannotator-last or /plannotator-annotate for manual review, or set workflo
69077
69151
  }
69078
69152
  if (result.feedback) {
69079
69153
  return getPlanApprovedWithNotesPrompt("opencode", undefined, {
69080
- planFilePath: sourceFilePath,
69154
+ planFilePath: backingPath,
69081
69155
  doneMsg: result.savedPath ? `Saved to: ${result.savedPath}` : "",
69082
69156
  feedback: result.feedback,
69083
69157
  proceedSuffix: shouldSwitchAgent ? `
@@ -69086,15 +69160,28 @@ Proceed with implementation, incorporating these notes where applicable.` : ""
69086
69160
  });
69087
69161
  }
69088
69162
  return getPlanApprovedPrompt("opencode", undefined, {
69089
- planFilePath: sourceFilePath,
69163
+ planFilePath: backingPath,
69090
69164
  doneMsg: result.savedPath ? ` Saved to: ${result.savedPath}` : ""
69091
69165
  });
69092
69166
  } else {
69167
+ const lineNumberedPlan = formatWithLineNumbers(planContent);
69168
+ const totalLines = planContent.split(`
69169
+ `).length;
69093
69170
  return getPlanDeniedPrompt("opencode", undefined, {
69094
69171
  toolName: getPlanToolName("opencode"),
69095
- planFileRule: buildPlanFileRule(getPlanToolName("opencode"), sourceFilePath),
69172
+ planFileRule: "",
69096
69173
  feedback: result.feedback || "Plan changes requested"
69097
- }) + "\n\nAfter making your revisions, call `submit_plan` again to resubmit for review.";
69174
+ }) + `
69175
+
69176
+ ## Current Plan (${totalLines} lines)
69177
+
69178
+ The plan below shows the current state with line numbers. Use these exact line numbers in your next \`submit_plan\` call:
69179
+
69180
+ \`\`\`
69181
+ ${lineNumberedPlan}
69182
+ \`\`\`
69183
+
69184
+ Call \`submit_plan\` with targeted edits to address the feedback above.`;
69098
69185
  }
69099
69186
  }
69100
69187
  })
@@ -69104,6 +69191,10 @@ Proceed with implementation, incorporating these notes where applicable.` : ""
69104
69191
  };
69105
69192
  var opencode_plugin_default = PlannotatorPlugin;
69106
69193
  export {
69194
+ validateEdits,
69195
+ getPlanBackingPath,
69196
+ formatWithLineNumbers,
69107
69197
  opencode_plugin_default as default,
69198
+ applyEdits,
69108
69199
  PlannotatorPlugin
69109
69200
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/opencode",
3
- "version": "0.19.17",
3
+ "version": "0.19.18",
4
4
  "description": "Plannotator plugin for OpenCode - interactive plan review with visual annotation",
5
5
  "author": "backnotprop",
6
6
  "license": "MIT OR Apache-2.0",
@@ -38,7 +38,7 @@
38
38
  "@opencode-ai/plugin": "^1.1.10"
39
39
  },
40
40
  "devDependencies": {
41
- "@plannotator/server": "0.19.17",
41
+ "@plannotator/server": "0.19.18",
42
42
  "@plannotator/shared": "0.0.1"
43
43
  },
44
44
  "peerDependencies": {