opencode-planpilot 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,24 @@
1
1
  # opencode-planpilot
2
2
 
3
- Planpilot rewritten in TypeScript for OpenCode. Provides plan/step/goal workflow with auto-continue for AI steps and a native `planpilot` tool.
3
+ Planpilot for OpenCode. Provides plan/step/goal workflow with auto-continue for AI steps and a native `planpilot` tool.
4
4
 
5
5
  ## Features
6
- - Plan/step/goal hierarchy with automatic status rollups
6
+ - Plan/step/goal hierarchy with status auto-propagation upward (goals -> steps -> plan)
7
7
  - SQLite storage with markdown plan snapshots
8
- - Native OpenCode tool plus optional CLI
8
+ - Native OpenCode tool for plan/step/goal operations
9
9
  - Auto-continue on `session.idle` when next step is assigned to `ai`
10
10
 
11
- ## Requirements
12
- - Bun runtime (uses `bun:sqlite` at runtime)
13
-
14
11
  ## Install
15
12
 
16
13
  Add to `opencode.json`:
17
14
 
18
15
  ```json
19
16
  {
20
- "$schema": "https://opencode.ai/config.json",
21
17
  "plugin": ["opencode-planpilot"]
22
18
  }
23
19
  ```
24
20
 
25
21
  OpenCode installs npm plugins automatically at startup.
26
22
 
27
- ## Details
28
- Usage and storage info: `docs/planpilot.md`
29
-
30
23
  ## License
31
24
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-planpilot",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Planpilot plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": {
@@ -14,18 +14,10 @@
14
14
  "packageManager": "bun@1.3.6",
15
15
  "main": "./src/index.ts",
16
16
  "types": "./src/index.ts",
17
- "bin": {
18
- "planpilot": "src/cli.ts",
19
- "opencode-planpilot": "src/cli.ts"
20
- },
21
17
  "exports": {
22
18
  ".": {
23
19
  "import": "./src/index.ts",
24
20
  "types": "./src/index.ts"
25
- },
26
- "./cli": {
27
- "import": "./src/cli.ts",
28
- "types": "./src/cli.ts"
29
21
  }
30
22
  },
31
23
  "files": [
@@ -49,7 +41,7 @@
49
41
  },
50
42
  "devDependencies": {
51
43
  "@opencode-ai/plugin": "*",
52
- "@types/node": "^25.0.8",
44
+ "@types/node": "^25.0.9",
53
45
  "bun-types": "^1.3.6",
54
46
  "eslint": "^9.39.2",
55
47
  "@eslint/js": "^9.39.2",
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env bun
2
1
  import fs from "fs"
3
2
  import { openDatabase, resolvePlanMarkdownPath, ensureParentDir } from "./lib/db"
4
3
  import { PlanpilotApp } from "./lib/app"
@@ -15,36 +14,29 @@ import { AppError, invalidInput } from "./lib/errors"
15
14
  import { ensureNonEmpty, projectMatchesPath, resolveMaybeRealpath } from "./lib/util"
16
15
  import { formatGoalDetail, formatPlanDetail, formatPlanMarkdown, formatStepDetail } from "./lib/format"
17
16
 
18
- const CWD_FLAG = "--cwd"
19
- const SESSION_ID_FLAG = "--session-id"
20
17
  const DEFAULT_PAGE = 1
21
18
  const DEFAULT_LIMIT = 20
22
19
 
23
- export type CliIO = {
20
+ export type CommandIO = {
24
21
  log: (...args: any[]) => void
25
- error: (...args: any[]) => void
26
22
  }
27
23
 
28
- const defaultIO: CliIO = {
29
- log: (...args) => {
30
- console.log(...args)
31
- },
32
- error: (...args) => {
33
- console.error(...args)
34
- },
24
+ export type CommandContext = {
25
+ sessionId: string
26
+ cwd: string
35
27
  }
36
28
 
37
- let currentIO: CliIO = defaultIO
29
+ const noopIO: CommandIO = {
30
+ log: () => {},
31
+ }
32
+
33
+ let currentIO: CommandIO = noopIO
38
34
 
39
35
  function log(...args: any[]) {
40
36
  currentIO.log(...args)
41
37
  }
42
38
 
43
- function error(...args: any[]) {
44
- currentIO.error(...args)
45
- }
46
-
47
- async function withIO<T>(io: CliIO, fn: () => Promise<T> | T): Promise<T> {
39
+ async function withIO<T>(io: CommandIO, fn: () => Promise<T> | T): Promise<T> {
48
40
  const prev = currentIO
49
41
  currentIO = io
50
42
  try {
@@ -54,7 +46,7 @@ async function withIO<T>(io: CliIO, fn: () => Promise<T> | T): Promise<T> {
54
46
  }
55
47
  }
56
48
 
57
- export function formatCliError(err: unknown): string {
49
+ export function formatCommandError(err: unknown): string {
58
50
  if (err instanceof AppError) {
59
51
  return `Error: ${err.toDisplayString()}`
60
52
  }
@@ -64,28 +56,24 @@ export function formatCliError(err: unknown): string {
64
56
  return `Error: ${String(err)}`
65
57
  }
66
58
 
67
- export async function runCLI(argv: string[] = process.argv.slice(2), io: CliIO = defaultIO) {
59
+ export async function runCommand(argv: string[], context: CommandContext, io: CommandIO = noopIO) {
68
60
  return withIO(io, async () => {
69
- const { cwd, cwdFlagPresent, sessionId, remaining } = parseGlobalArgs(argv)
70
-
71
- if (!remaining.length) {
72
- throw invalidInput("missing command")
61
+ if (!argv.length) {
62
+ throw invalidInput("missing argv")
73
63
  }
74
64
 
75
- const [section, subcommand, ...args] = remaining
76
-
77
- const resolvedSessionId = resolveSessionId(sessionId)
65
+ const [section, subcommand, ...args] = argv
78
66
 
79
67
  const db = openDatabase()
80
- const resolvedCwd = cwd ? resolveMaybeRealpath(cwd) : undefined
81
- const app = new PlanpilotApp(db, resolvedSessionId, resolvedCwd)
68
+ const resolvedCwd = resolveMaybeRealpath(context.cwd)
69
+ const app = new PlanpilotApp(db, context.sessionId, resolvedCwd)
82
70
 
83
71
  let planIds: number[] = []
84
72
  let shouldSync = false
85
73
 
86
74
  switch (section) {
87
75
  case "plan": {
88
- const result = await handlePlan(app, subcommand, args, { cwd, cwdFlagPresent })
76
+ const result = await handlePlan(app, subcommand, args, { cwd: context.cwd })
89
77
  planIds = result.planIds
90
78
  shouldSync = result.shouldSync
91
79
  break
@@ -112,57 +100,20 @@ export async function runCLI(argv: string[] = process.argv.slice(2), io: CliIO =
112
100
  })
113
101
  }
114
102
 
115
- function parseGlobalArgs(argv: string[]) {
116
- let cwd: string | undefined
117
- let sessionId: string | undefined
118
- let cwdFlagPresent = false
119
- const remaining: string[] = []
120
-
121
- for (let i = 0; i < argv.length; i += 1) {
122
- const token = argv[i]
123
- if (token === CWD_FLAG) {
124
- cwdFlagPresent = true
125
- cwd = argv[i + 1]
126
- i += 1
127
- continue
128
- }
129
- if (token === SESSION_ID_FLAG) {
130
- sessionId = argv[i + 1]
131
- i += 1
132
- continue
133
- }
134
- remaining.push(token)
135
- }
136
-
137
- return { cwd, cwdFlagPresent, sessionId, remaining }
138
- }
139
-
140
- function resolveSessionId(sessionId: string | undefined): string {
141
- if (!sessionId) {
142
- throw invalidInput(`${SESSION_ID_FLAG} is required`)
143
- }
144
- const trimmed = sessionId.trim()
145
- if (!trimmed) {
146
- throw invalidInput(`${SESSION_ID_FLAG} is empty`)
147
- }
148
- return trimmed
149
- }
150
-
151
- function requireCwd(cwdFlagPresent: boolean, cwd: string | undefined): string {
152
- if (!cwdFlagPresent) {
153
- throw invalidInput(`${CWD_FLAG} is required`)
154
- }
103
+ function requireCwd(cwd: string | undefined): string {
155
104
  if (!cwd || !cwd.trim()) {
156
- throw invalidInput(`${CWD_FLAG} is empty`)
105
+ throw invalidInput("cwd is required")
157
106
  }
158
107
  return cwd
159
108
  }
160
109
 
110
+ export type { CommandContext as PlanpilotCommandContext, CommandIO as PlanpilotCommandIO }
111
+
161
112
  async function handlePlan(
162
113
  app: PlanpilotApp,
163
114
  subcommand: string | undefined,
164
115
  args: string[],
165
- context: { cwd: string | undefined; cwdFlagPresent: boolean },
116
+ context: { cwd: string | undefined },
166
117
  ) {
167
118
  switch (subcommand) {
168
119
  case "add":
@@ -212,6 +163,8 @@ async function handleStep(app: PlanpilotApp, subcommand: string | undefined, arg
212
163
  return { planIds: handleStepShow(app, args), shouldSync: false }
213
164
  case "show-next":
214
165
  return { planIds: handleStepShowNext(app), shouldSync: false }
166
+ case "wait":
167
+ return { planIds: handleStepWait(app, args), shouldSync: true }
215
168
  case "comment":
216
169
  return { planIds: handleStepComment(app, args), shouldSync: true }
217
170
  case "update":
@@ -287,7 +240,7 @@ function handlePlanAddTree(app: PlanpilotApp, args: string[]): number[] {
287
240
  function handlePlanList(
288
241
  app: PlanpilotApp,
289
242
  args: string[],
290
- context: { cwd: string | undefined; cwdFlagPresent: boolean },
243
+ context: { cwd: string | undefined },
291
244
  ): number[] {
292
245
  const { options, positionals } = parseOptions(args)
293
246
  if (positionals.length) {
@@ -304,7 +257,7 @@ function handlePlanList(
304
257
  }
305
258
 
306
259
  const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
307
- const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
260
+ const cwd = requireCwd(context.cwd)
308
261
 
309
262
  const order = options.order ? parsePlanOrder(options.order) : "updated"
310
263
  const desc = options.desc ?? true
@@ -346,7 +299,7 @@ function handlePlanList(
346
299
  function handlePlanCount(
347
300
  app: PlanpilotApp,
348
301
  args: string[],
349
- context: { cwd: string | undefined; cwdFlagPresent: boolean },
302
+ context: { cwd: string | undefined },
350
303
  ): number[] {
351
304
  const { options, positionals } = parseOptions(args)
352
305
  if (positionals.length) {
@@ -363,7 +316,7 @@ function handlePlanCount(
363
316
  }
364
317
 
365
318
  const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
366
- const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
319
+ const cwd = requireCwd(context.cwd)
367
320
 
368
321
  let plans = app.listPlans()
369
322
  if (!plans.length) {
@@ -383,7 +336,7 @@ function handlePlanCount(
383
336
  function handlePlanSearch(
384
337
  app: PlanpilotApp,
385
338
  args: string[],
386
- context: { cwd: string | undefined; cwdFlagPresent: boolean },
339
+ context: { cwd: string | undefined },
387
340
  ): number[] {
388
341
  const { options, positionals } = parseOptions(args)
389
342
  if (positionals.length) {
@@ -393,7 +346,7 @@ function handlePlanSearch(
393
346
  throw invalidInput("plan search requires at least one --search")
394
347
  }
395
348
  const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
396
- const cwd = requireCwd(context.cwdFlagPresent, context.cwd)
349
+ const cwd = requireCwd(context.cwd)
397
350
 
398
351
  const order = options.order ? parsePlanOrder(options.order) : "updated"
399
352
  const desc = options.desc ?? true
@@ -700,6 +653,30 @@ function handleStepShowNext(app: PlanpilotApp): number[] {
700
653
  return []
701
654
  }
702
655
 
656
+ function handleStepWait(app: PlanpilotApp, args: string[]): number[] {
657
+ if (!args.length) {
658
+ throw invalidInput("step wait requires <id>")
659
+ }
660
+ const stepId = parseNumber(args[0], "step id")
661
+ const options = parseOptions(args.slice(1)).options
662
+ if (options.clear) {
663
+ const result = app.clearStepWait(stepId)
664
+ log(`Step ID: ${result.step.id} wait cleared.`)
665
+ return [result.step.plan_id]
666
+ }
667
+ if (options.delay === undefined) {
668
+ throw invalidInput("step wait requires --delay <ms> or --clear")
669
+ }
670
+ const delayMs = parseNumber(options.delay, "delay")
671
+ if (delayMs < 0) {
672
+ throw invalidInput("delay must be >= 0")
673
+ }
674
+ const reason = options.reason ? String(options.reason) : undefined
675
+ const result = app.setStepWait(stepId, delayMs, reason)
676
+ log(`Step ID: ${result.step.id} waiting until ${result.until}.`)
677
+ return [result.step.plan_id]
678
+ }
679
+
703
680
  function handleStepUpdate(app: PlanpilotApp, args: string[]): number[] {
704
681
  if (!args.length) {
705
682
  throw invalidInput("step update requires <id>")
@@ -1034,18 +1011,31 @@ function parseOptions(args: string[]) {
1034
1011
  options.order = expectValue(args, i, token)
1035
1012
  i += 2
1036
1013
  break
1037
- case "--to":
1038
- options.to = expectValue(args, i, token)
1039
- i += 2
1040
- break
1041
- case "--goal":
1042
- if (!options.goals) options.goals = []
1043
- options.goals.push(expectValue(args, i, token))
1044
- i += 2
1045
- break
1046
- default:
1047
- throw invalidInput(`unexpected argument: ${token}`)
1048
- }
1014
+ case "--to":
1015
+ options.to = expectValue(args, i, token)
1016
+ i += 2
1017
+ break
1018
+ case "--delay":
1019
+ options.delay = expectValue(args, i, token)
1020
+ i += 2
1021
+ break
1022
+ case "--reason":
1023
+ options.reason = expectValue(args, i, token)
1024
+ i += 2
1025
+ break
1026
+ case "--clear":
1027
+ options.clear = true
1028
+ i += 1
1029
+ break
1030
+ case "--goal":
1031
+ if (!options.goals) options.goals = []
1032
+ options.goals.push(expectValue(args, i, token))
1033
+ i += 2
1034
+ break
1035
+ default:
1036
+ throw invalidInput(`unexpected argument: ${token}`)
1037
+ }
1038
+
1049
1039
  }
1050
1040
  return { options, positionals }
1051
1041
  }
@@ -1475,9 +1465,3 @@ function syncPlanMarkdown(app: PlanpilotApp, planIds: number[]) {
1475
1465
  })
1476
1466
  }
1477
1467
 
1478
- if (import.meta.main) {
1479
- runCLI(process.argv.slice(2), defaultIO).catch((err) => {
1480
- error(formatCliError(err))
1481
- process.exit(1)
1482
- })
1483
- }
package/src/index.ts CHANGED
@@ -1,23 +1,74 @@
1
1
  import { tool, type Plugin } from "@opencode-ai/plugin"
2
- import { runCLI, formatCliError } from "./cli"
2
+ import { runCommand, formatCommandError } from "./command"
3
3
  import { PlanpilotApp } from "./lib/app"
4
- import { parseCommandArgs } from "./lib/argv"
5
4
  import { openDatabase } from "./lib/db"
6
5
  import { invalidInput } from "./lib/errors"
7
6
  import { formatStepDetail } from "./lib/format"
8
- import { loadPlanpilotInstructions } from "./lib/instructions"
7
+ import { parseWaitFromComment } from "./lib/util"
8
+
9
+ const PLANPILOT_TOOL_DESCRIPTION = [
10
+ "Planpilot planner for plan workflows.",
11
+ "Hints: 1. Model is plan/step/goal with ai/human executors and status auto-propagation upward (goals -> steps -> plan). 2. Keep comments short and decision-focused. 3. Add human steps only when AI cannot act. 4. Use `step wait` when ending a reply while waiting on external tasks.",
12
+ "",
13
+ "Usage:",
14
+ "- argv is tokenized: [section, subcommand, ...args]",
15
+ "- section: plan | step | goal",
16
+ "",
17
+ "Plan commands:",
18
+ "- plan add <title> <content>",
19
+ "- plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <content>]... [--step ...]...",
20
+ "- plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
21
+ "- plan count [--scope project|all] [--status todo|done|all]",
22
+ "- plan search --search <term> [--search <term> ...] [--search-mode any|all] [--search-field plan|title|content|comment|steps|goals|all] [--match-case] [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
23
+ "- plan show <id>",
24
+ "- plan export <id> <path>",
25
+ "- plan comment <id> <comment> [<id> <comment> ...]",
26
+ "- plan update <id> [--title <title>] [--content <content>] [--status todo|done] [--comment <comment>]",
27
+ "- plan done <id>",
28
+ "- plan remove <id>",
29
+ "- plan activate <id> [--force]",
30
+ "- plan show-active",
31
+ "- plan deactivate",
32
+ "",
33
+ "Step commands:",
34
+ "- step add <plan_id> <content...> [--executor ai|human] [--at <pos>]",
35
+ "- step add-tree <plan_id> <content> [--executor ai|human] [--goal <content> ...]",
36
+ "- step list <plan_id> [--status todo|done|all] [--executor ai|human] [--limit N] [--page N]",
37
+ "- step count <plan_id> [--status todo|done|all] [--executor ai|human]",
38
+ "- step show <id>",
39
+ "- step show-next",
40
+ "- step wait <id> --delay <ms> [--reason <text>]",
41
+ "- step wait <id> --clear",
42
+ "- step comment <id> <comment> [<id> <comment> ...]",
43
+ "- step update <id> [--content <content>] [--status todo|done] [--executor ai|human] [--comment <comment>]",
44
+ "- step done <id> [--all-goals]",
45
+ "- step move <id> --to <pos>",
46
+ "- step remove <id...>",
47
+ "",
48
+ "Goal commands:",
49
+ "- goal add <step_id> <content...>",
50
+ "- goal list <step_id> [--status todo|done|all] [--limit N] [--page N]",
51
+ "- goal count <step_id> [--status todo|done|all]",
52
+ "- goal show <id>",
53
+ "- goal comment <id> <comment> [<id> <comment> ...]",
54
+ "- goal update <id> [--content <content>] [--status todo|done] [--comment <comment>]",
55
+ "- goal done <id...>",
56
+ "- goal remove <id...>",
57
+ ].join("\n")
9
58
 
10
59
  export const PlanpilotPlugin: Plugin = async (ctx) => {
11
60
  const inFlight = new Set<string>()
12
61
  const skipNextAuto = new Set<string>()
13
62
  const lastIdleAt = new Map<string, number>()
63
+ const waitTimers = new Map<string, ReturnType<typeof setTimeout>>()
14
64
 
15
- const PLANPILOT_GUIDANCE = [
16
- "Planpilot guidance:",
17
- "- Do not read plan files from disk or follow plan file placeholders.",
18
- "- Use the planpilot tool for plan/step/goal info (plan show-active, step show-next, goal list <step_id>).",
19
- "- If you cannot continue or need human input, insert a new step with executor `human` before the next pending step using planpilot so auto-continue pauses.",
20
- ].join("\n")
65
+ const clearWaitTimer = (sessionID: string) => {
66
+ const existing = waitTimers.get(sessionID)
67
+ if (existing) {
68
+ clearTimeout(existing)
69
+ waitTimers.delete(sessionID)
70
+ }
71
+ }
21
72
 
22
73
  const log = async (level: "debug" | "info" | "warn" | "error", message: string, extra?: Record<string, any>) => {
23
74
  try {
@@ -132,6 +183,32 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
132
183
  const next = app.nextStep(active.plan_id)
133
184
  if (!next) return
134
185
  if (next.executor !== "ai") return
186
+ const wait = parseWaitFromComment(next.comment)
187
+ if (wait && wait.until > now) {
188
+ clearWaitTimer(sessionID)
189
+ await log("info", "auto-continue delayed by step wait", {
190
+ sessionID,
191
+ stepId: next.id,
192
+ until: wait.until,
193
+ reason: wait.reason,
194
+ })
195
+ const msUntil = Math.max(0, wait.until - now)
196
+ const timer = setTimeout(() => {
197
+ waitTimers.delete(sessionID)
198
+ handleSessionIdle(sessionID).catch((err) => {
199
+ void log("warn", "auto-continue retry failed", {
200
+ sessionID,
201
+ error: err instanceof Error ? err.message : String(err),
202
+ })
203
+ })
204
+ }, msUntil)
205
+ waitTimers.set(sessionID, timer)
206
+ return
207
+ }
208
+ if (!wait) {
209
+ clearWaitTimer(sessionID)
210
+ }
211
+
135
212
  const goals = app.goalsForStep(next.id)
136
213
  const detail = formatStepDetail(next, goals)
137
214
  if (!detail.trim()) return
@@ -143,12 +220,13 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
143
220
  }
144
221
  if (autoContext?.aborted || autoContext?.ready === false) return
145
222
 
146
- const message =
147
- "Planpilot (auto):\n" +
148
- "Before acting, think through the next step and its goals. Record implementation details using Planpilot comments (plan/step/goal --comment or comment commands). Continue with the next step (executor: ai). Do not ask for confirmation; proceed and report results.\n\n" +
149
- PLANPILOT_GUIDANCE +
150
- "\n\n" +
151
- detail.trimEnd()
223
+ const timestamp = new Date().toISOString()
224
+ const message = `Planpilot plugin auto message @ ${timestamp}
225
+ Hints:
226
+ - If the next step needs human action, insert a human step before it.
227
+ - If you need to wait for something to finish, use the step wait subcommand.
228
+ Next step details:
229
+ ${detail.trimEnd()}`
152
230
 
153
231
  const promptBody: any = {
154
232
  agent: autoContext?.agent ?? undefined,
@@ -173,72 +251,41 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
173
251
  return {
174
252
  tool: {
175
253
  planpilot: tool({
176
- description:
177
- "Planpilot planner. Use for all plan/step/goal operations. Provide either argv (array) or command (string). " +
178
- "Do not include --session-id/--cwd; they are injected automatically from the current session.",
254
+ description: PLANPILOT_TOOL_DESCRIPTION,
179
255
  args: {
180
- argv: tool.schema.array(tool.schema.string()).optional(),
181
- command: tool.schema.string().min(1),
256
+ argv: tool.schema.array(tool.schema.string()).min(1),
182
257
  },
183
258
  async execute(args, toolCtx) {
184
- let argv: string[] = []
185
- if (Array.isArray(args.argv) && args.argv.length) {
186
- argv = args.argv
187
- } else if (typeof args.command === "string" && args.command.trim()) {
188
- argv = parseCommandArgs(args.command)
189
- } else {
190
- return formatCliError(invalidInput("missing command"))
191
- }
192
-
193
- const cwd = (ctx.directory ?? "").trim()
194
- if (!cwd) {
195
- return formatCliError(invalidInput(`${"--cwd"} is required`))
259
+ const argv = Array.isArray(args.argv) ? args.argv : []
260
+ if (!argv.length) {
261
+ return formatCommandError(invalidInput("missing argv"))
196
262
  }
197
263
 
198
264
  if (containsForbiddenFlags(argv)) {
199
- return formatCliError(invalidInput("do not pass --cwd or --session-id"))
265
+ return formatCommandError(invalidInput("argv cannot include --cwd or --session-id"))
200
266
  }
201
267
 
202
- const finalArgv = [...argv]
203
- if (!finalArgv.includes("--cwd")) {
204
- finalArgv.unshift("--cwd", cwd)
205
- }
206
- if (!finalArgv.includes("--session-id")) {
207
- finalArgv.unshift("--session-id", toolCtx.sessionID)
268
+ const cwd = (ctx.directory ?? "").trim()
269
+ if (!cwd) {
270
+ return formatCommandError(invalidInput("cwd is required"))
208
271
  }
209
272
 
210
273
  const output: string[] = []
211
274
  const io = {
212
275
  log: (...parts: any[]) => output.push(parts.map(String).join(" ")),
213
- error: (...parts: any[]) => output.push(parts.map(String).join(" ")),
214
276
  }
215
277
 
216
278
  try {
217
- await runCLI(finalArgv, io)
279
+ await runCommand(argv, { sessionId: toolCtx.sessionID, cwd }, io)
218
280
  } catch (err) {
219
- return formatCliError(err)
281
+ return formatCommandError(err)
220
282
  }
221
283
 
222
284
  return output.join("\n").trimEnd()
223
285
  },
224
286
  }),
225
287
  },
226
- "experimental.chat.system.transform": async (_input, output) => {
227
- const instructions = loadPlanpilotInstructions().trim()
228
- const alreadyInjected = output.system.some((entry) => entry.includes("Planpilot (OpenCode Tool)"))
229
- if (instructions && !alreadyInjected) {
230
- output.system.push(instructions)
231
- }
232
- const guidanceInjected = output.system.some((entry) => entry.includes("Planpilot guidance:"))
233
- if (!guidanceInjected) {
234
- output.system.push(PLANPILOT_GUIDANCE)
235
- }
236
- },
237
- "experimental.session.compacting": async ({ sessionID }, output) => {
238
- const hasGuidance = output.context.some((entry) => entry.includes("Planpilot guidance:"))
239
- if (!hasGuidance) {
240
- output.context.push(PLANPILOT_GUIDANCE)
241
- }
288
+ "experimental.session.compacting": async ({ sessionID }) => {
242
289
  skipNextAuto.add(sessionID)
243
290
  lastIdleAt.set(sessionID, Date.now())
244
291
  },
@@ -263,3 +310,4 @@ function containsForbiddenFlags(argv: string[]): boolean {
263
310
  return false
264
311
  })
265
312
  }
313
+
package/src/lib/app.ts CHANGED
@@ -22,7 +22,15 @@ import {
22
22
  type StepRow,
23
23
  type StepStatus,
24
24
  } from "./models"
25
- import { ensureNonEmpty, joinIds, normalizeCommentEntries, uniqueIds } from "./util"
25
+ import {
26
+ ensureNonEmpty,
27
+ joinIds,
28
+ normalizeCommentEntries,
29
+ parseWaitFromComment,
30
+ removeWaitFromComment,
31
+ uniqueIds,
32
+ upsertWaitInComment,
33
+ } from "./util"
26
34
  import { invalidInput, notFound } from "./errors"
27
35
  import { formatStepDetail } from "./format"
28
36
 
@@ -820,6 +828,44 @@ export class PlanpilotApp {
820
828
  return tx()
821
829
  }
822
830
 
831
+ setStepWait(stepId: number, delayMs: number, reason?: string): { step: StepRow; until: number } {
832
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
833
+ throw invalidInput("delay must be a non-negative number")
834
+ }
835
+ const tx = this.db.transaction(() => {
836
+ const step = this.getStep(stepId)
837
+ const now = Date.now()
838
+ const until = now + Math.trunc(delayMs)
839
+ const comment = upsertWaitInComment(step.comment, until, reason)
840
+ this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId)
841
+ const updated = this.getStep(stepId)
842
+ this.touchPlan(updated.plan_id)
843
+ return { step: updated, until }
844
+ })
845
+
846
+ return tx()
847
+ }
848
+
849
+ clearStepWait(stepId: number): { step: StepRow } {
850
+ const tx = this.db.transaction(() => {
851
+ const step = this.getStep(stepId)
852
+ const comment = step.comment ? removeWaitFromComment(step.comment) : null
853
+ const now = Date.now()
854
+ this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId)
855
+ const updated = this.getStep(stepId)
856
+ this.touchPlan(updated.plan_id)
857
+ return { step: updated }
858
+ })
859
+
860
+ return tx()
861
+ }
862
+
863
+ getStepWait(stepId: number): { step: StepRow; wait: { until: number; reason?: string } | null } {
864
+ const step = this.getStep(stepId)
865
+ const wait = parseWaitFromComment(step.comment)
866
+ return { step, wait }
867
+ }
868
+
823
869
  commentGoals(entries: Array<[number, string]>): number[] {
824
870
  const normalized = normalizeCommentEntries(entries)
825
871
  if (!normalized.length) return []
package/src/lib/util.ts CHANGED
@@ -49,6 +49,60 @@ export function normalizeCommentEntries(entries: Array<[number, string]>): Array
49
49
  return ordered
50
50
  }
51
51
 
52
+ const WAIT_UNTIL_PREFIX = "@wait-until="
53
+ const WAIT_REASON_PREFIX = "@wait-reason="
54
+
55
+ export type WaitComment = {
56
+ until: number
57
+ reason?: string
58
+ }
59
+
60
+ export function parseWaitFromComment(comment?: string | null): WaitComment | null {
61
+ if (!comment) return null
62
+ let until: number | null = null
63
+ let reason: string | undefined
64
+ for (const line of comment.split(/\r?\n/)) {
65
+ const trimmed = line.trim()
66
+ if (trimmed.startsWith(WAIT_UNTIL_PREFIX)) {
67
+ const raw = trimmed.slice(WAIT_UNTIL_PREFIX.length).trim()
68
+ const value = Number(raw)
69
+ if (Number.isFinite(value)) {
70
+ until = value
71
+ }
72
+ continue
73
+ }
74
+ if (trimmed.startsWith(WAIT_REASON_PREFIX)) {
75
+ const raw = trimmed.slice(WAIT_REASON_PREFIX.length).trim()
76
+ if (raw) reason = raw
77
+ }
78
+ }
79
+ if (until === null) return null
80
+ return { until, reason }
81
+ }
82
+
83
+ function isWaitLine(line: string): boolean {
84
+ const trimmed = line.trim()
85
+ return trimmed.startsWith(WAIT_UNTIL_PREFIX) || trimmed.startsWith(WAIT_REASON_PREFIX)
86
+ }
87
+
88
+ export function upsertWaitInComment(comment: string | null, until: number, reason?: string): string {
89
+ const lines = comment ? comment.split(/\r?\n/) : []
90
+ const filtered = lines.filter((line) => !isWaitLine(line))
91
+ const waitLines = [`${WAIT_UNTIL_PREFIX}${Math.trunc(until)}`]
92
+ const reasonValue = reason?.trim()
93
+ if (reasonValue) {
94
+ waitLines.push(`${WAIT_REASON_PREFIX}${reasonValue}`)
95
+ }
96
+ return [...waitLines, ...filtered].join("\n").trimEnd()
97
+ }
98
+
99
+ export function removeWaitFromComment(comment?: string | null): string | null {
100
+ if (!comment) return null
101
+ const lines = comment.split(/\r?\n/).filter((line) => !isWaitLine(line))
102
+ const cleaned = lines.join("\n").trimEnd()
103
+ return cleaned.length ? cleaned : null
104
+ }
105
+
52
106
  export function resolveMaybeRealpath(value: string): string {
53
107
  try {
54
108
  return fs.realpathSync.native(value)
package/docs/planpilot.md DELETED
@@ -1,220 +0,0 @@
1
- # Planpilot (OpenCode Tool)
2
-
3
- ## Tool Name
4
- `planpilot`
5
-
6
- ## What it is
7
- Planpilot is a planner/tracker for multi-step work. It manages plans, steps, and goals, and automatically rolls up status.
8
-
9
- ## OpenCode Tool Usage (Required)
10
- Use the custom `planpilot` tool. Do **not** call the CLI via `bash` for planning.
11
-
12
- ### Tool schema
13
- - `argv?: string[]` - preferred; avoids quoting issues
14
- - `command?: string` - optional; one command string
15
-
16
- Rules:
17
- - Provide either `argv` or `command`.
18
- - Do **not** pass `--session-id` or `--cwd`; the tool injects them automatically from the current session.
19
- - If both `argv` and `command` are provided, `argv` is used.
20
-
21
- Example tool args:
22
-
23
- ```json
24
- { "argv": ["plan", "add", "Release v1.2", "Plan description"] }
25
- ```
26
-
27
- ```json
28
- { "command": "plan add \"Release v1.2\" \"Plan description\"" }
29
- ```
30
-
31
- ## CLI (Optional)
32
- A CLI binary named `planpilot` is available for manual use. If you use the CLI directly, you must pass `--session-id` and `--cwd` yourself.
33
-
34
- ## Storage
35
- - Database: `~/.config/opencode/.planpilot/planpilot.db`
36
- - Plan snapshots: `~/.config/opencode/.planpilot/plans/`
37
- - Override base directory: `OPENCODE_PLANPILOT_DIR` or `OPENCODE_PLANPILOT_HOME`
38
-
39
- ## Hierarchy
40
- - Plan contains steps; step contains goals.
41
- - Goals are the smallest units of work; steps group goals; plans group steps.
42
-
43
- ## AI Workflow Guidelines
44
- - Use Planpilot for all planning, status, and progress tracking; do not use built-in plan/todo tools or other methods to track plan/step/goal status.
45
- - Do not read plan files from disk or follow plan file placeholders; use the planpilot tool for plan/step/goal info.
46
- - Treat tool output as authoritative. Do not invent IDs; only use IDs shown by `list`/`show`.
47
- - If the tool is missing or unavailable, ask the user to enable/install the plugin.
48
- - Record implementation details using Planpilot comments (plan/step/goal `--comment` or `comment` commands). Before starting a step or goal, think through the next actions and capture that context in comments so the plan stays actionable.
49
- - Before starting a new plan, do deep analysis; then create the plan with clear steps and goals.
50
- - When creating a plan or step, prefer `add-tree` to define all steps/goals upfront.
51
- - Prefer assigning steps to `ai`; only assign `human` for truly critical/high-risk items or when passwords, `sudo` access, irreversible git history rewrites, or remote git changes are required. If `human` steps are necessary, batch them, make the step content explicit about what the human must do, and only ask for user input when the next step is assigned to `human`.
52
- - Adjust plans/steps/goals only when necessary; avoid frequent arbitrary changes.
53
- - Update status promptly as work completes; mark goals done, and let steps/plans auto-refresh unless a step/plan has no children (then use `step done`/`plan done`).
54
- - In each reply turn, complete at most one step; do not advance multiple steps in a single response.
55
-
56
- ## Status Management
57
- - Status values: `todo`, `done`.
58
- - Goals are manual (`goal done`); steps/plans auto-refresh from child status, and use `step done`/`plan done` only when they have no children (`step done --all-goals` marks all goals done and then marks the step done). Auto status changes print as `Auto status updates:` with reasons.
59
- - Parent status auto-flips to `todo` on incomplete child work and to `done` when all children are done. If a plan has 0 steps or a step has 0 goals, no auto-flip happens; use `plan done` / `step done` as needed.
60
- - If the user completed a `human` step, verify/mark each goal and clearly list what remains.
61
- - When a step becomes `done` and there is another pending step, the CLI will print the next-step instruction: for `ai`, end the turn so Planpilot can surface it; for `human`, show the step detail and tell the user to complete the goals, then end the turn. When a plan becomes `done` (automatic or manual), the CLI will prompt you to summarize completed results and end the turn.
62
-
63
- ## Active Plan Management
64
- - Use `plan activate` / `plan deactivate` to manage, and no active plan means the plan is paused until reactivated. Plans auto-deactivate on `done` (manual or automatic) or removal.
65
- - Each plan can be active in only one session at a time; use `plan activate --force` to take over. Default to no `--force`, and if activation fails due to another session, ask the user whether to take over.
66
- - Use `plan show-active` to know which plan is active and get its details.
67
-
68
- ## Auto-Continue Behavior (OpenCode Plugin)
69
- - The plugin listens to `session.idle` / `session.status` (idle) events.
70
- - If there is an active plan and the next pending step is assigned to `ai`, it appends a `Planpilot (auto):` message to the prompt and submits it so the model continues the plan.
71
- - It does nothing when there is no active plan or when the next step is assigned to `human`.
72
-
73
- ## ID Notes
74
- - Plan/step/goal IDs are database IDs and may be non-contiguous or not start at 1; always use the actual IDs shown by `list`/`show`.
75
-
76
- ## Commands
77
-
78
- - IMPORTANT: Do NOT pass `--cwd` or `--session-id` when using the tool. These are injected automatically.
79
-
80
- ### plan
81
- - Plan data is stored under OpenCode config: `~/.config/opencode/.planpilot/` (overridable via `OPENCODE_PLANPILOT_DIR` or `OPENCODE_PLANPILOT_HOME`).
82
- - `plan add <title> <content>`: create a plan.
83
- - Output: `Created plan ID: <id>: <title>`.
84
- - `plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <goal> ...] [--step <content> ...]`: create a plan with steps/goals in one command.
85
- - Output: `Created plan ID: <id>: <title> (steps: <n>, goals: <n>)`.
86
- - Behavior: the newly created plan is automatically activated for the current session.
87
- - Repeatable groups: you can repeat the `--step ... [--executor ...] [--goal ...]` group multiple times.
88
- - Each `--executor` / `--goal` applies to the most recent `--step`.
89
- - Example (tool args):
90
- ```json
91
- {"argv":["plan","add-tree","Release v1.2","Plan description","--step","Cut release branch","--executor","human","--goal","Create branch","--goal","Tag base","--step","Build artifacts","--executor","ai","--goal","Build packages"]}
92
- ```
93
- - Another example (3 steps, some without goals/executor):
94
- ```json
95
- {"argv":["plan","add-tree","Onboarding","Setup plan","--step","Create accounts","--goal","GitHub","--goal","Slack","--step","Install tooling","--executor","ai","--step","Read handbook"]}
96
- ```
97
- - `plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]`: list plans. Default: `--scope project`, `--status all`, `--limit 20`, `--page 1`, order by `updated` desc (most recent first).
98
- - Output: prints a header line, then one line per plan with `ID STAT STEPS TITLE COMMENT` (`STEPS` is `done/total`); use `plan show` for full details.
99
- - Output (empty): `No plans found.`
100
- - Order: defaults to `updated` with most recent first. Use `--order` and `--desc` to override.
101
- - Pagination: If `--page` is provided without `--limit`, the default limit is used.
102
- - If `--page` exceeds the total pages, a warning is printed and no rows are returned.
103
- - Footer: prints `Page N / Limit N` after the list.
104
- - `plan count [--scope project|all] [--status todo|done|all]`: count plans matching the filters. Output: `Total: <n>`.
105
- - `plan search --search <term> [--search <term> ...] [--search-mode any|all] [--search-field plan|title|content|comment|steps|goals|all] [--match-case] [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]`: search plans. Default: `--scope project`, `--status all`, `--limit 20`, `--page 1`, order by `updated` desc.
106
- - Output: same format as `plan list`.
107
- - Output (empty): `No plans found.`
108
- - Advanced search:
109
- - `--search <term>` (repeatable): filter plans by text (required).
110
- - `--search-mode any|all`: match any term or require all terms (default: `all`).
111
- - `--search-field plan|title|content|comment|steps|goals|all` (default: `plan`).
112
- - `--match-case`: make search case-sensitive.
113
- - Pagination: If `--page` is provided without `--limit`, the default limit is used.
114
- - If `--page` exceeds the total pages, a warning is printed and no rows are returned.
115
- - Footer: prints `Page N / Limit N` after the list.
116
- - `plan show <id>`: prints plan details and nested steps/goals (includes ids for plan/step/goal).
117
- - Output: plan header includes `Plan ID: <id>`, `Title`, `Status`, `Content`, `Created`, `Updated`, and `Comment` when present.
118
- - Output: each step line includes step id and executor; progress (`goals done/total`) is shown only when the step has goals. Each goal line includes goal id.
119
- - `plan export <id> <path>`: export plan details to a markdown file.
120
- - Output: `Exported plan ID: <id> to <path>`.
121
- - `plan update <id> [--title <title>] [--content <content>] [--status todo|done] [--comment <comment>]`: update fields; `--status done` is allowed only when all steps are done or the plan has no steps.
122
- - Output: `Updated plan ID: <id>: <title>`.
123
- - Errors: multi-line `Error: Invalid input:` with `cannot mark plan done; next pending step:` on the next line, followed by the same step detail output as `step show`.
124
- - `plan done <id>`: mark plan done (same rule as `plan update --status done`).
125
- - Output: `Plan ID: <id> marked done.`
126
- - Output (active plan): `Active plan deactivated because plan is done.`
127
- - Errors: multi-line `Error: Invalid input:` with `cannot mark plan done; next pending step:` on the next line, followed by the same step detail output as `step show`.
128
- - `plan comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more plans.
129
- - Output (single): `Updated plan comment for plan ID: <id>.`
130
- - Output (batch): `Updated plan comments for <n> plans.`
131
- - Each plan comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
132
- - Example:
133
- ```json
134
- {"argv":["plan","comment","12","high priority","15","waiting on input"]}
135
- ```
136
- - `plan remove <id>`: remove plan (and its steps/goals).
137
- - Output: `Plan ID: <id> removed.`
138
- - `plan activate <id> [--force]`: set the active plan.
139
- - Output: `Active plan set to <id>: <title>`.
140
- - `--force` takes over a plan already active in another session.
141
- - Errors: `Error: Invalid input: cannot activate plan; plan is done`.
142
- - Errors: `Error: Invalid input: plan id <id> is already active in session <session_id> (use --force to take over)`.
143
- - `plan show-active`: prints the active plan details (same format as `plan show`).
144
- - Output: the same plan detail format as `plan show`.
145
- - Output (empty): `No active plan.`
146
- - Output (missing): `Active plan ID: <id> not found.`
147
- - `plan deactivate`: unset the active plan (does not delete any plan).
148
- - Output: `Active plan deactivated.`
149
-
150
- ### step
151
- - `step add <plan_id> <content1> [<content2> ...] [--at <pos>] [--executor ai|human]`: add steps.
152
- - Output (single): `Created step ID: <id> for plan ID: <plan_id>`.
153
- - Output (batch): `Created <n> steps for plan ID: <plan_id>`.
154
- - `step add-tree <plan_id> <content> [--executor ai|human] [--goal <goal> ...]`: create one step with goals in one command.
155
- - Output: `Created step ID: <id> for plan ID: <plan_id> (goals: <n>)`.
156
- - Example:
157
- ```json
158
- {"argv":["step","add-tree","1","Draft summary","--executor","ai","--goal","Collect inputs","--goal","Write draft"]}
159
- ```
160
- - `step list <plan_id> [--status todo|done|all] [--executor ai|human] [--limit N] [--page N]`: list steps. Default: `--status all`, `--limit 20`, `--page 1`. Steps are always returned in their plan order.
161
- - Output: prints a header line, then one line per step with `ID STAT ORD EXEC GOALS CONTENT COMMENT` (`ORD` is the step order within the plan; `GOALS` is `done/total`); use `step show` for full details.
162
- - Output (empty): `No steps found for plan ID: <plan_id>.`
163
- - Pagination: If `--page` is provided without `--limit`, the default limit is used.
164
- - If `--page` exceeds the total pages, a warning is printed and no rows are returned.
165
- - Footer: prints `Page N / Limit N` after the list.
166
- - `step count <plan_id> [--status todo|done|all] [--executor ai|human]`: count steps matching the filters. Output: `Total: <n>`.
167
- - `step show <id>`: prints a single step with full details and its nested goals (includes ids for step/goal).
168
- - Output: step header includes `Step ID: <id>`, `Plan ID`, `Status`, `Executor`, `Content`, `Created`, `Updated`, and `Comment` when present.
169
- - Output: lists all goals with `[status]` and goal id.
170
- - `step show-next`: show the next pending step for the active plan (same format as `step show`).
171
- - Output (empty): `No active plan.` or `No pending step.`.
172
- - `step update <id> [--content <content>] [--status todo|done] [--executor ai|human] [--comment <comment>]`: update fields; `--status done` is allowed only when all goals are done or the step has no goals.
173
- - Output: `Updated step ID: <id>.`.
174
- - Errors: `Error: Invalid input: cannot mark step done; next pending goal: <content> (id <id>)`.
175
- - `step comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more steps.
176
- - Output (single): `Updated step comments for plan ID: <plan_id>.`
177
- - Output (batch): `Updated step comments for <n> plans.`
178
- - Each step comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
179
- - Example:
180
- ```json
181
- {"argv":["step","comment","45","blocked by API","46","ready to start"]}
182
- ```
183
- - `step done <id> [--all-goals]`: mark step done (same rule as `step update --status done`). Use `--all-goals` to mark all goals in the step done first, then mark the step done.
184
- - Output: `Step ID: <id> marked done.`
185
- - Errors: `Error: Invalid input: cannot mark step done; next pending goal: <content> (id <id>)`.
186
- - `step move <id> --to <pos>`: reorder and print the same one-line list as `step list`.
187
- - Output: `Reordered steps for plan ID: <plan_id>:` + list.
188
- - `step remove <id1> [<id2> ...]`: remove step(s).
189
- - Output (single): `Step ID: <id> removed.`
190
- - Output (batch): `Removed <n> steps.`
191
- - Errors: `Error: Not found: step id(s) not found: <id1>[, <id2> ...]`.
192
-
193
- ### goal
194
- - `goal add <step_id> <content1> [<content2> ...]`: add goals to a step.
195
- - Output (single): `Created goal ID: <id> for step ID: <step_id>`.
196
- - Output (batch): `Created <n> goals for step ID: <step_id>`.
197
- - `goal list <step_id> [--status todo|done|all] [--limit N] [--page N]`: list goals. Default: `--status all`, `--limit 20`, `--page 1`, order by `updated` desc (most recent first).
198
- - Output: prints a header line, then one line per goal with `ID STAT CONTENT COMMENT`.
199
- - Output (empty): `No goals found for step ID: <step_id>.`
200
- - Pagination: If `--page` is provided without `--limit`, the default limit is used.
201
- - If `--page` exceeds the total pages, a warning is printed and no rows are returned.
202
- - Footer: prints `Page N / Limit N` after the list.
203
- - `goal count <step_id> [--status todo|done|all]`: count goals matching the filters. Output: `Total: <n>`.
204
- - `goal update <id> [--content <content>] [--status todo|done] [--comment <comment>]`: update fields.
205
- - Output: `Updated goal <id>.`
206
- - `goal comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more goals.
207
- - Output (single): `Updated goal comments for plan ID: <plan_id>.`
208
- - Output (batch): `Updated goal comments for <n> plans.`
209
- - Each goal comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
210
- - Example:
211
- ```json
212
- {"argv":["goal","comment","78","done","81","needs review"]}
213
- ```
214
- - `goal done <id1> [<id2> ...]`: mark one or more goals done.
215
- - Output (single): `Goal ID: <id> marked done.`
216
- - Output (batch): `Goals marked done: <n>.`
217
- - `goal remove <id1> [<id2> ...]`: remove goal(s).
218
- - Output (single): `Goal ID: <id> removed.`
219
- - Output (batch): `Removed <n> goals.`
220
- - Errors: `Error: Not found: goal id(s) not found: <id1>[, <id2> ...]`.
package/src/lib/argv.ts DELETED
@@ -1,58 +0,0 @@
1
- import { invalidInput } from "./errors"
2
-
3
- export function parseCommandArgs(input: string): string[] {
4
- const args: string[] = []
5
- let current = ""
6
- let inSingle = false
7
- let inDouble = false
8
- let escapeNext = false
9
-
10
- for (let i = 0; i < input.length; i += 1) {
11
- const ch = input[i]
12
-
13
- if (escapeNext) {
14
- current += ch
15
- escapeNext = false
16
- continue
17
- }
18
-
19
- if (ch === "\\" && !inSingle) {
20
- escapeNext = true
21
- continue
22
- }
23
-
24
- if (ch === "'" && !inDouble) {
25
- inSingle = !inSingle
26
- continue
27
- }
28
-
29
- if (ch === '"' && !inSingle) {
30
- inDouble = !inDouble
31
- continue
32
- }
33
-
34
- if (!inSingle && !inDouble && /\s/.test(ch)) {
35
- if (current.length) {
36
- args.push(current)
37
- current = ""
38
- }
39
- continue
40
- }
41
-
42
- current += ch
43
- }
44
-
45
- if (escapeNext) {
46
- current += "\\"
47
- }
48
-
49
- if (inSingle || inDouble) {
50
- throw invalidInput("unterminated quote in command")
51
- }
52
-
53
- if (current.length) {
54
- args.push(current)
55
- }
56
-
57
- return args
58
- }
@@ -1,26 +0,0 @@
1
- import fs from "fs"
2
- import path from "path"
3
- import { fileURLToPath } from "url"
4
-
5
- let cached: string | null = null
6
-
7
- export function loadPlanpilotInstructions(): string {
8
- if (cached !== null) return cached
9
- try {
10
- const moduleDir = path.dirname(fileURLToPath(import.meta.url))
11
- const candidates = [
12
- path.resolve(moduleDir, "../../docs/planpilot.md"),
13
- path.resolve(moduleDir, "../../../docs/planpilot.md"),
14
- ]
15
- for (const candidate of candidates) {
16
- if (fs.existsSync(candidate)) {
17
- cached = fs.readFileSync(candidate, "utf8")
18
- return cached
19
- }
20
- }
21
- cached = ""
22
- } catch {
23
- cached = ""
24
- }
25
- return cached
26
- }