opencode-planpilot 0.2.3 → 0.3.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 +41 -10
- package/README.zh-CN.md +55 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3815 -0
- package/dist/index.js.map +1 -0
- package/dist/studio-bridge.d.ts +2 -0
- package/dist/studio-bridge.js +1985 -0
- package/dist/studio-bridge.js.map +1 -0
- package/dist/studio-web/planpilot-todo-bar.d.ts +33 -0
- package/dist/studio-web/planpilot-todo-bar.js +704 -0
- package/dist/studio-web/planpilot-todo-bar.js.map +1 -0
- package/dist/studio.manifest.json +968 -0
- package/package.json +6 -1
- package/src/index.ts +583 -26
- package/src/lib/config.ts +436 -0
- package/src/prompt.ts +7 -1
- package/src/studio/bridge.ts +746 -0
- package/src/studio-web/main.ts +1030 -0
- package/src/studio-web/planpilot-todo-bar.ts +974 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { PlanpilotApp } from "../lib/app"
|
|
2
|
+
import { AppError, invalidInput } from "../lib/errors"
|
|
3
|
+
import {
|
|
4
|
+
loadPlanpilotConfig,
|
|
5
|
+
normalizePlanpilotConfig,
|
|
6
|
+
savePlanpilotConfig,
|
|
7
|
+
type PlanpilotConfig,
|
|
8
|
+
} from "../lib/config"
|
|
9
|
+
import { openDatabase } from "../lib/db"
|
|
10
|
+
import { parseWaitFromComment } from "../lib/util"
|
|
11
|
+
import type {
|
|
12
|
+
GoalQuery,
|
|
13
|
+
GoalStatus,
|
|
14
|
+
PlanDetail,
|
|
15
|
+
PlanOrder,
|
|
16
|
+
PlanStatus,
|
|
17
|
+
StepDetail,
|
|
18
|
+
StepExecutor,
|
|
19
|
+
StepOrder,
|
|
20
|
+
StepQuery,
|
|
21
|
+
StepStatus,
|
|
22
|
+
} from "../lib/models"
|
|
23
|
+
|
|
24
|
+
type JsonPrimitive = string | number | boolean | null
|
|
25
|
+
type JsonObject = object
|
|
26
|
+
type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
|
|
27
|
+
|
|
28
|
+
type BridgeRequest = {
|
|
29
|
+
action: string
|
|
30
|
+
payload?: unknown
|
|
31
|
+
context?: unknown
|
|
32
|
+
plugin?: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type BridgeSuccess = {
|
|
36
|
+
ok: true
|
|
37
|
+
data: JsonValue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type BridgeFailure = {
|
|
41
|
+
ok: false
|
|
42
|
+
error: {
|
|
43
|
+
code: string
|
|
44
|
+
message: string
|
|
45
|
+
details: JsonValue | null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type BridgeResponse = BridgeSuccess | BridgeFailure
|
|
50
|
+
|
|
51
|
+
type BridgeRequestContext = {
|
|
52
|
+
sessionId: string
|
|
53
|
+
cwd?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ActionHandler = (payload: unknown, context: BridgeRequestContext) => JsonValue
|
|
57
|
+
|
|
58
|
+
const DEFAULT_SESSION_ID = "studio"
|
|
59
|
+
|
|
60
|
+
const PLAN_UPDATE_ALLOWED = new Set(["title", "content", "status", "comment"])
|
|
61
|
+
const STEP_UPDATE_ALLOWED = new Set(["content", "status", "executor", "comment"])
|
|
62
|
+
const GOAL_UPDATE_ALLOWED = new Set(["content", "status", "comment"])
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const response = await runBridge()
|
|
66
|
+
process.stdout.write(JSON.stringify(response))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runBridge(): Promise<BridgeResponse> {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readStdinOnce()
|
|
72
|
+
const request = parseRequest(raw)
|
|
73
|
+
const context = resolveRequestContext(request)
|
|
74
|
+
const data = dispatch(request.action, request.payload, context)
|
|
75
|
+
return ok(data)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return fail(error)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseRequest(raw: string): BridgeRequest {
|
|
82
|
+
if (!raw.trim()) {
|
|
83
|
+
throw invalidInput("bridge request is empty")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let parsed: unknown
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
91
|
+
throw invalidInput(`bridge request is not valid JSON: ${message}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const root = asObject(parsed, "bridge request")
|
|
95
|
+
const action = expectString(root.action, "action")
|
|
96
|
+
return {
|
|
97
|
+
action,
|
|
98
|
+
payload: root.payload,
|
|
99
|
+
context: root.context,
|
|
100
|
+
plugin: root.plugin,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveRequestContext(request: BridgeRequest): BridgeRequestContext {
|
|
105
|
+
const context = asObjectOptional(request.context)
|
|
106
|
+
const plugin = asObjectOptional(request.plugin)
|
|
107
|
+
const sessionId =
|
|
108
|
+
readNonEmptyString(context?.sessionId) ?? readNonEmptyString(context?.sessionID) ?? DEFAULT_SESSION_ID
|
|
109
|
+
const cwd =
|
|
110
|
+
readNonEmptyString(context?.cwd) ??
|
|
111
|
+
readNonEmptyString(context?.directory) ??
|
|
112
|
+
readNonEmptyString(plugin?.rootPath) ??
|
|
113
|
+
undefined
|
|
114
|
+
return { sessionId, cwd }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createApp(context: BridgeRequestContext): PlanpilotApp {
|
|
118
|
+
return new PlanpilotApp(openDatabase(), context.sessionId, context.cwd)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function dispatch(action: string, payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
122
|
+
const handler = ACTIONS[action]
|
|
123
|
+
if (!handler) {
|
|
124
|
+
throw invalidInput(`unknown action: ${action}`)
|
|
125
|
+
}
|
|
126
|
+
return handler(payload, context)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ACTIONS: Record<string, ActionHandler> = {
|
|
130
|
+
"runtime.snapshot": actionRuntimeSnapshot,
|
|
131
|
+
"runtime.next": actionRuntimeNext,
|
|
132
|
+
"runtime.pause": actionRuntimePause,
|
|
133
|
+
"runtime.resume": actionRuntimeResume,
|
|
134
|
+
"events.poll": actionEventsPoll,
|
|
135
|
+
"config.get": actionConfigGet,
|
|
136
|
+
"config.set": actionConfigSet,
|
|
137
|
+
"plan.list": actionPlanList,
|
|
138
|
+
"plan.get": actionPlanGet,
|
|
139
|
+
"plan.createTree": actionPlanAddTree,
|
|
140
|
+
"plan.addTree": actionPlanAddTree,
|
|
141
|
+
"plan.update": actionPlanUpdate,
|
|
142
|
+
"plan.done": actionPlanDone,
|
|
143
|
+
"plan.remove": actionPlanRemove,
|
|
144
|
+
"plan.activate": actionPlanActivate,
|
|
145
|
+
"plan.deactivate": actionPlanDeactivate,
|
|
146
|
+
"plan.active": actionPlanActive,
|
|
147
|
+
"step.list": actionStepList,
|
|
148
|
+
"step.get": actionStepGet,
|
|
149
|
+
"step.add": actionStepAdd,
|
|
150
|
+
"step.addTree": actionStepAddTree,
|
|
151
|
+
"step.update": actionStepUpdate,
|
|
152
|
+
"step.done": actionStepDone,
|
|
153
|
+
"step.remove": actionStepRemove,
|
|
154
|
+
"step.move": actionStepMove,
|
|
155
|
+
"step.wait": actionStepWait,
|
|
156
|
+
"goal.list": actionGoalList,
|
|
157
|
+
"goal.get": actionGoalGet,
|
|
158
|
+
"goal.add": actionGoalAdd,
|
|
159
|
+
"goal.update": actionGoalUpdate,
|
|
160
|
+
"goal.done": actionGoalDone,
|
|
161
|
+
"goal.remove": actionGoalRemove,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function actionRuntimeSnapshot(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
165
|
+
const app = createApp(context)
|
|
166
|
+
return buildRuntimeSnapshot(app, loadPlanpilotConfig().config)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function actionRuntimeNext(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
170
|
+
const app = createApp(context)
|
|
171
|
+
const active = app.getActivePlan()
|
|
172
|
+
if (!active) {
|
|
173
|
+
return {
|
|
174
|
+
activePlan: null,
|
|
175
|
+
nextStep: null,
|
|
176
|
+
cursor: buildRuntimeCursor(app, loadPlanpilotConfig().config),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nextStep = app.nextStep(active.plan_id)
|
|
181
|
+
return {
|
|
182
|
+
activePlan: active,
|
|
183
|
+
nextStep: nextStep ? serializeStepDetail(app.getStepDetail(nextStep.id)) : null,
|
|
184
|
+
cursor: buildRuntimeCursor(app, loadPlanpilotConfig().config),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function actionRuntimePause(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
189
|
+
const loaded = loadPlanpilotConfig()
|
|
190
|
+
const config = normalizePlanpilotConfig(loaded.config)
|
|
191
|
+
config.runtime.paused = true
|
|
192
|
+
savePlanpilotConfig(config)
|
|
193
|
+
const app = createApp(context)
|
|
194
|
+
return buildRuntimeSnapshot(app, config)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function actionRuntimeResume(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
198
|
+
const loaded = loadPlanpilotConfig()
|
|
199
|
+
const config = normalizePlanpilotConfig(loaded.config)
|
|
200
|
+
config.runtime.paused = false
|
|
201
|
+
savePlanpilotConfig(config)
|
|
202
|
+
const app = createApp(context)
|
|
203
|
+
return buildRuntimeSnapshot(app, config)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function actionEventsPoll(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
207
|
+
const app = createApp(context)
|
|
208
|
+
const config = loadPlanpilotConfig().config
|
|
209
|
+
const currentCursor = buildRuntimeCursor(app, config)
|
|
210
|
+
|
|
211
|
+
const input = asObjectOptional(payload)
|
|
212
|
+
const previousCursor = readNonEmptyString(input?.cursor) ?? ""
|
|
213
|
+
if (previousCursor === currentCursor) {
|
|
214
|
+
return {
|
|
215
|
+
cursor: currentCursor,
|
|
216
|
+
events: [],
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
cursor: currentCursor,
|
|
222
|
+
events: [
|
|
223
|
+
{
|
|
224
|
+
event: "planpilot.runtime.changed",
|
|
225
|
+
id: currentCursor,
|
|
226
|
+
data: buildRuntimeSnapshot(app, config),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function actionConfigGet(_payload: unknown, _context: BridgeRequestContext): JsonValue {
|
|
233
|
+
const loaded = loadPlanpilotConfig()
|
|
234
|
+
return loaded.config as unknown as JsonValue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function actionConfigSet(payload: unknown, _context: BridgeRequestContext): JsonValue {
|
|
238
|
+
const root = asObject(payload, "config.set payload")
|
|
239
|
+
const raw = "config" in root ? root.config : payload
|
|
240
|
+
const normalized = normalizePlanpilotConfig(raw)
|
|
241
|
+
const saved = savePlanpilotConfig(normalized)
|
|
242
|
+
return {
|
|
243
|
+
path: saved.path,
|
|
244
|
+
config: saved.config as unknown as JsonValue,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function actionPlanList(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
249
|
+
const app = createApp(context)
|
|
250
|
+
const input = asObjectOptional(payload)
|
|
251
|
+
const order = parsePlanOrderOptional(input?.order)
|
|
252
|
+
const desc = readBoolean(input?.desc) ?? true
|
|
253
|
+
const plans = app.listPlans(order, desc)
|
|
254
|
+
return plans as unknown as JsonValue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function actionPlanGet(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
258
|
+
const app = createApp(context)
|
|
259
|
+
const input = asObject(payload, "plan.get payload")
|
|
260
|
+
const id = expectInt(input.id, "id")
|
|
261
|
+
return serializePlanDetail(app.getPlanDetail(id))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function actionPlanAddTree(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
265
|
+
const app = createApp(context)
|
|
266
|
+
const input = asObject(payload, "plan.createTree payload")
|
|
267
|
+
const title = expectString(input.title, "title")
|
|
268
|
+
const content = expectString(input.content, "content")
|
|
269
|
+
const stepsInput = expectArray(input.steps, "steps")
|
|
270
|
+
const steps = stepsInput.map((item, index) => {
|
|
271
|
+
const step = asObject(item, `steps[${index}]`)
|
|
272
|
+
return {
|
|
273
|
+
content: expectString(step.content, `steps[${index}].content`),
|
|
274
|
+
executor: parseExecutorOptional(step.executor) ?? "ai",
|
|
275
|
+
goals: readStringArray(step.goals),
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const result = app.addPlanTree({ title, content }, steps)
|
|
280
|
+
return {
|
|
281
|
+
plan: result.plan,
|
|
282
|
+
stepCount: result.stepCount,
|
|
283
|
+
goalCount: result.goalCount,
|
|
284
|
+
detail: serializePlanDetail(app.getPlanDetail(result.plan.id)),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function actionPlanUpdate(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
289
|
+
const app = createApp(context)
|
|
290
|
+
const input = asObject(payload, "plan.update payload")
|
|
291
|
+
const id = expectInt(input.id, "id")
|
|
292
|
+
assertAllowedKeys(input, PLAN_UPDATE_ALLOWED, "plan.update")
|
|
293
|
+
const result = app.updatePlanWithActiveClear(id, {
|
|
294
|
+
title: readString(input.title),
|
|
295
|
+
content: readString(input.content),
|
|
296
|
+
status: parsePlanStatusOptional(input.status),
|
|
297
|
+
comment: readNullableString(input.comment),
|
|
298
|
+
})
|
|
299
|
+
return {
|
|
300
|
+
plan: result.plan,
|
|
301
|
+
cleared: result.cleared,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function actionPlanDone(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
306
|
+
const app = createApp(context)
|
|
307
|
+
const input = asObject(payload, "plan.done payload")
|
|
308
|
+
const id = expectInt(input.id, "id")
|
|
309
|
+
return app.updatePlanWithActiveClear(id, { status: "done" }) as unknown as JsonValue
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function actionPlanRemove(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
313
|
+
const app = createApp(context)
|
|
314
|
+
const input = asObject(payload, "plan.remove payload")
|
|
315
|
+
const id = expectInt(input.id, "id")
|
|
316
|
+
app.deletePlan(id)
|
|
317
|
+
return { removed: id }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function actionPlanActivate(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
321
|
+
const app = createApp(context)
|
|
322
|
+
const input = asObject(payload, "plan.activate payload")
|
|
323
|
+
const id = expectInt(input.id, "id")
|
|
324
|
+
const force = readBoolean(input.force) ?? false
|
|
325
|
+
const plan = app.getPlan(id)
|
|
326
|
+
if (plan.status === "done") {
|
|
327
|
+
throw invalidInput("cannot activate plan; plan is done")
|
|
328
|
+
}
|
|
329
|
+
return app.setActivePlan(id, force) as unknown as JsonValue
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function actionPlanDeactivate(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
333
|
+
const app = createApp(context)
|
|
334
|
+
const active = app.getActivePlan()
|
|
335
|
+
app.clearActivePlan()
|
|
336
|
+
return {
|
|
337
|
+
activePlan: active,
|
|
338
|
+
deactivated: true,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function actionPlanActive(_payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
343
|
+
const app = createApp(context)
|
|
344
|
+
const active = app.getActivePlan()
|
|
345
|
+
if (!active) {
|
|
346
|
+
return { activePlan: null, detail: null }
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
activePlan: active,
|
|
350
|
+
detail: serializePlanDetail(app.getPlanDetail(active.plan_id)),
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function actionStepList(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
355
|
+
const app = createApp(context)
|
|
356
|
+
const input = asObject(payload, "step.list payload")
|
|
357
|
+
const planId = expectInt(input.planId, "planId")
|
|
358
|
+
const query: StepQuery = {
|
|
359
|
+
status: parseStepStatusOptional(input.status),
|
|
360
|
+
executor: parseExecutorOptional(input.executor),
|
|
361
|
+
limit: parseIntOptional(input.limit),
|
|
362
|
+
offset: parseIntOptional(input.offset),
|
|
363
|
+
order: parseStepOrderOptional(input.order),
|
|
364
|
+
desc: readBoolean(input.desc),
|
|
365
|
+
}
|
|
366
|
+
const steps = app.listStepsFiltered(planId, query)
|
|
367
|
+
return steps as unknown as JsonValue
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function actionStepGet(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
371
|
+
const app = createApp(context)
|
|
372
|
+
const input = asObject(payload, "step.get payload")
|
|
373
|
+
const id = expectInt(input.id, "id")
|
|
374
|
+
return serializeStepDetail(app.getStepDetail(id))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function actionStepAdd(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
378
|
+
const app = createApp(context)
|
|
379
|
+
const input = asObject(payload, "step.add payload")
|
|
380
|
+
const planId = expectInt(input.planId, "planId")
|
|
381
|
+
const contents = resolveContents(input, "content", "contents")
|
|
382
|
+
const executor = parseExecutorOptional(input.executor) ?? "ai"
|
|
383
|
+
const at = parseIntOptional(input.at)
|
|
384
|
+
const result = app.addStepsBatch(planId, contents, "todo", executor, at)
|
|
385
|
+
return {
|
|
386
|
+
steps: result.steps,
|
|
387
|
+
changes: result.changes,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function actionStepAddTree(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
392
|
+
const app = createApp(context)
|
|
393
|
+
const input = asObject(payload, "step.addTree payload")
|
|
394
|
+
const planId = expectInt(input.planId, "planId")
|
|
395
|
+
const content = expectString(input.content, "content")
|
|
396
|
+
const executor = parseExecutorOptional(input.executor) ?? "ai"
|
|
397
|
+
const goals = readStringArray(input.goals)
|
|
398
|
+
return app.addStepTree(planId, content, executor, goals) as unknown as JsonValue
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function actionStepUpdate(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
402
|
+
const app = createApp(context)
|
|
403
|
+
const input = asObject(payload, "step.update payload")
|
|
404
|
+
const id = expectInt(input.id, "id")
|
|
405
|
+
assertAllowedKeys(input, STEP_UPDATE_ALLOWED, "step.update")
|
|
406
|
+
return app.updateStep(id, {
|
|
407
|
+
content: readString(input.content),
|
|
408
|
+
status: parseStepStatusOptional(input.status),
|
|
409
|
+
executor: parseExecutorOptional(input.executor),
|
|
410
|
+
comment: readNullableString(input.comment),
|
|
411
|
+
}) as unknown as JsonValue
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function actionStepDone(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
415
|
+
const app = createApp(context)
|
|
416
|
+
const input = asObject(payload, "step.done payload")
|
|
417
|
+
const id = expectInt(input.id, "id")
|
|
418
|
+
const allGoals = readBoolean(input.allGoals) ?? false
|
|
419
|
+
return app.setStepDoneWithGoals(id, allGoals) as unknown as JsonValue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function actionStepRemove(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
423
|
+
const app = createApp(context)
|
|
424
|
+
const input = asObject(payload, "step.remove payload")
|
|
425
|
+
const ids = resolveIds(input)
|
|
426
|
+
return app.deleteSteps(ids) as unknown as JsonValue
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function actionStepMove(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
430
|
+
const app = createApp(context)
|
|
431
|
+
const input = asObject(payload, "step.move payload")
|
|
432
|
+
const id = expectInt(input.id, "id")
|
|
433
|
+
const to = expectInt(input.to, "to")
|
|
434
|
+
return app.moveStep(id, to) as unknown as JsonValue
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function actionStepWait(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
438
|
+
const app = createApp(context)
|
|
439
|
+
const input = asObject(payload, "step.wait payload")
|
|
440
|
+
const id = expectInt(input.id, "id")
|
|
441
|
+
const clear = readBoolean(input.clear) ?? false
|
|
442
|
+
if (clear) {
|
|
443
|
+
return app.clearStepWait(id) as unknown as JsonValue
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const delayMs = expectInt(input.delayMs, "delayMs")
|
|
447
|
+
const reason = readString(input.reason)
|
|
448
|
+
return app.setStepWait(id, delayMs, reason) as unknown as JsonValue
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function actionGoalList(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
452
|
+
const app = createApp(context)
|
|
453
|
+
const input = asObject(payload, "goal.list payload")
|
|
454
|
+
const stepId = expectInt(input.stepId, "stepId")
|
|
455
|
+
const query: GoalQuery = {
|
|
456
|
+
status: parseGoalStatusOptional(input.status),
|
|
457
|
+
limit: parseIntOptional(input.limit),
|
|
458
|
+
offset: parseIntOptional(input.offset),
|
|
459
|
+
}
|
|
460
|
+
return app.listGoalsFiltered(stepId, query) as unknown as JsonValue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function actionGoalGet(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
464
|
+
const app = createApp(context)
|
|
465
|
+
const input = asObject(payload, "goal.get payload")
|
|
466
|
+
const id = expectInt(input.id, "id")
|
|
467
|
+
return app.getGoalDetail(id) as unknown as JsonValue
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function actionGoalAdd(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
471
|
+
const app = createApp(context)
|
|
472
|
+
const input = asObject(payload, "goal.add payload")
|
|
473
|
+
const stepId = expectInt(input.stepId, "stepId")
|
|
474
|
+
const contents = resolveContents(input, "content", "contents")
|
|
475
|
+
return app.addGoalsBatch(stepId, contents, "todo") as unknown as JsonValue
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function actionGoalUpdate(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
479
|
+
const app = createApp(context)
|
|
480
|
+
const input = asObject(payload, "goal.update payload")
|
|
481
|
+
const id = expectInt(input.id, "id")
|
|
482
|
+
assertAllowedKeys(input, GOAL_UPDATE_ALLOWED, "goal.update")
|
|
483
|
+
return app.updateGoal(id, {
|
|
484
|
+
content: readString(input.content),
|
|
485
|
+
status: parseGoalStatusOptional(input.status),
|
|
486
|
+
comment: readNullableString(input.comment),
|
|
487
|
+
}) as unknown as JsonValue
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function actionGoalDone(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
491
|
+
const app = createApp(context)
|
|
492
|
+
const input = asObject(payload, "goal.done payload")
|
|
493
|
+
const ids = resolveIds(input)
|
|
494
|
+
if (ids.length === 1) {
|
|
495
|
+
return app.setGoalStatus(ids[0], "done") as unknown as JsonValue
|
|
496
|
+
}
|
|
497
|
+
return app.setGoalsStatus(ids, "done") as unknown as JsonValue
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function actionGoalRemove(payload: unknown, context: BridgeRequestContext): JsonValue {
|
|
501
|
+
const app = createApp(context)
|
|
502
|
+
const input = asObject(payload, "goal.remove payload")
|
|
503
|
+
const ids = resolveIds(input)
|
|
504
|
+
return app.deleteGoals(ids) as unknown as JsonValue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function resolveContents(input: Record<string, unknown>, contentKey: string, contentsKey: string): string[] {
|
|
508
|
+
const content = readString(input[contentKey])
|
|
509
|
+
const contents = readStringArray(input[contentsKey])
|
|
510
|
+
const out = content ? [content, ...contents] : contents
|
|
511
|
+
if (!out.length) {
|
|
512
|
+
throw invalidInput(`expected ${contentKey} or ${contentsKey}`)
|
|
513
|
+
}
|
|
514
|
+
return out
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function resolveIds(input: Record<string, unknown>): number[] {
|
|
518
|
+
const fromSingle = parseIntOptional(input.id)
|
|
519
|
+
const fromMany = readIntArray(input.ids)
|
|
520
|
+
const ids = fromSingle !== undefined ? [fromSingle, ...fromMany] : fromMany
|
|
521
|
+
if (!ids.length) {
|
|
522
|
+
throw invalidInput("expected id or ids")
|
|
523
|
+
}
|
|
524
|
+
return Array.from(new Set(ids))
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function parsePlanOrderOptional(value: unknown): PlanOrder | undefined {
|
|
528
|
+
const raw = readString(value)
|
|
529
|
+
if (!raw) return undefined
|
|
530
|
+
if (raw === "id" || raw === "title" || raw === "created" || raw === "updated") return raw
|
|
531
|
+
throw invalidInput(`invalid plan order '${raw}', expected id|title|created|updated`)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function parseStepOrderOptional(value: unknown): StepOrder | undefined {
|
|
535
|
+
const raw = readString(value)
|
|
536
|
+
if (!raw) return undefined
|
|
537
|
+
if (raw === "order" || raw === "id" || raw === "created" || raw === "updated") return raw
|
|
538
|
+
throw invalidInput(`invalid step order '${raw}', expected order|id|created|updated`)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parsePlanStatusOptional(value: unknown): PlanStatus | undefined {
|
|
542
|
+
const raw = readString(value)
|
|
543
|
+
if (!raw) return undefined
|
|
544
|
+
if (raw === "todo" || raw === "done") return raw
|
|
545
|
+
throw invalidInput(`invalid plan status '${raw}', expected todo|done`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function parseStepStatusOptional(value: unknown): StepStatus | undefined {
|
|
549
|
+
const raw = parsePlanStatusOptional(value)
|
|
550
|
+
return raw as StepStatus | undefined
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function parseGoalStatusOptional(value: unknown): GoalStatus | undefined {
|
|
554
|
+
const raw = parsePlanStatusOptional(value)
|
|
555
|
+
return raw as GoalStatus | undefined
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function parseExecutorOptional(value: unknown): StepExecutor | undefined {
|
|
559
|
+
const raw = readString(value)
|
|
560
|
+
if (!raw) return undefined
|
|
561
|
+
if (raw === "ai" || raw === "human") return raw
|
|
562
|
+
throw invalidInput(`invalid executor '${raw}', expected ai|human`)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function assertAllowedKeys(input: Record<string, unknown>, allowed: Set<string>, action: string) {
|
|
566
|
+
for (const key of Object.keys(input)) {
|
|
567
|
+
if (key === "id") continue
|
|
568
|
+
if (!allowed.has(key)) {
|
|
569
|
+
throw invalidInput(`${action} does not support '${key}'`)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildRuntimeSnapshot(app: PlanpilotApp, config: PlanpilotConfig): JsonValue {
|
|
575
|
+
const active = app.getActivePlan()
|
|
576
|
+
const next = active ? app.nextStep(active.plan_id) : null
|
|
577
|
+
return {
|
|
578
|
+
paused: config.runtime.paused,
|
|
579
|
+
activePlan: active,
|
|
580
|
+
nextStep: next ? serializeStepDetail(app.getStepDetail(next.id)) : null,
|
|
581
|
+
cursor: buildRuntimeCursor(app, config),
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildRuntimeCursor(app: PlanpilotApp, config: PlanpilotConfig): string {
|
|
586
|
+
const plans = app.listPlans("updated", true)
|
|
587
|
+
const latestPlanUpdated = plans.length ? plans[0].updated_at : 0
|
|
588
|
+
const active = app.getActivePlan()
|
|
589
|
+
const activeUpdated = active?.updated_at ?? 0
|
|
590
|
+
const activePlanId = active?.plan_id ?? 0
|
|
591
|
+
const next = active ? app.nextStep(active.plan_id) : null
|
|
592
|
+
const nextStepId = next?.id ?? 0
|
|
593
|
+
const paused = config.runtime.paused ? 1 : 0
|
|
594
|
+
return [paused, latestPlanUpdated, activeUpdated, activePlanId, nextStepId].join(":")
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function serializePlanDetail(detail: PlanDetail): JsonValue {
|
|
598
|
+
const goals = detail.steps.map((step) => ({
|
|
599
|
+
stepId: step.id,
|
|
600
|
+
goals: detail.goals.get(step.id) ?? [],
|
|
601
|
+
}))
|
|
602
|
+
return {
|
|
603
|
+
plan: detail.plan,
|
|
604
|
+
steps: detail.steps,
|
|
605
|
+
goals,
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function serializeStepDetail(detail: StepDetail): JsonValue {
|
|
610
|
+
const wait = parseWaitFromComment(detail.step.comment)
|
|
611
|
+
return {
|
|
612
|
+
step: detail.step,
|
|
613
|
+
goals: detail.goals,
|
|
614
|
+
wait,
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function ok(data: JsonValue): BridgeSuccess {
|
|
619
|
+
return { ok: true, data }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function fail(error: unknown): BridgeFailure {
|
|
623
|
+
if (error instanceof AppError) {
|
|
624
|
+
const code =
|
|
625
|
+
error.kind === "InvalidInput"
|
|
626
|
+
? "invalid_input"
|
|
627
|
+
: error.kind === "NotFound"
|
|
628
|
+
? "not_found"
|
|
629
|
+
: error.kind === "Db"
|
|
630
|
+
? "db_error"
|
|
631
|
+
: error.kind === "Io"
|
|
632
|
+
? "io_error"
|
|
633
|
+
: "json_error"
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
error: {
|
|
637
|
+
code,
|
|
638
|
+
message: error.toDisplayString(),
|
|
639
|
+
details: null,
|
|
640
|
+
},
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
error: {
|
|
648
|
+
code: "internal_error",
|
|
649
|
+
message,
|
|
650
|
+
details: null,
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function asObject(value: unknown, label: string): Record<string, unknown> {
|
|
656
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
657
|
+
throw invalidInput(`${label} must be an object`)
|
|
658
|
+
}
|
|
659
|
+
return value as Record<string, unknown>
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function asObjectOptional(value: unknown): Record<string, unknown> | undefined {
|
|
663
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
664
|
+
return undefined
|
|
665
|
+
}
|
|
666
|
+
return value as Record<string, unknown>
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function expectString(value: unknown, label: string): string {
|
|
670
|
+
const parsed = readString(value)
|
|
671
|
+
if (!parsed) {
|
|
672
|
+
throw invalidInput(`${label} is required`)
|
|
673
|
+
}
|
|
674
|
+
return parsed
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function readString(value: unknown): string | undefined {
|
|
678
|
+
if (typeof value !== "string") return undefined
|
|
679
|
+
const trimmed = value.trim()
|
|
680
|
+
return trimmed.length ? trimmed : undefined
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function readNullableString(value: unknown): string | undefined {
|
|
684
|
+
if (value === null || value === undefined) return undefined
|
|
685
|
+
return readString(value)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
689
|
+
return readString(value)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function readBoolean(value: unknown): boolean | undefined {
|
|
693
|
+
return typeof value === "boolean" ? value : undefined
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function expectInt(value: unknown, label: string): number {
|
|
697
|
+
const parsed = parseIntOptional(value)
|
|
698
|
+
if (parsed === undefined) {
|
|
699
|
+
throw invalidInput(`${label} must be an integer`)
|
|
700
|
+
}
|
|
701
|
+
return parsed
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function parseIntOptional(value: unknown): number | undefined {
|
|
705
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
706
|
+
return undefined
|
|
707
|
+
}
|
|
708
|
+
return value
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function readStringArray(value: unknown): string[] {
|
|
712
|
+
if (!Array.isArray(value)) return []
|
|
713
|
+
const out: string[] = []
|
|
714
|
+
for (const item of value) {
|
|
715
|
+
const text = readString(item)
|
|
716
|
+
if (text) out.push(text)
|
|
717
|
+
}
|
|
718
|
+
return out
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function readIntArray(value: unknown): number[] {
|
|
722
|
+
if (!Array.isArray(value)) return []
|
|
723
|
+
const out: number[] = []
|
|
724
|
+
for (const item of value) {
|
|
725
|
+
const id = parseIntOptional(item)
|
|
726
|
+
if (id !== undefined) out.push(id)
|
|
727
|
+
}
|
|
728
|
+
return out
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function expectArray(value: unknown, label: string): unknown[] {
|
|
732
|
+
if (!Array.isArray(value)) {
|
|
733
|
+
throw invalidInput(`${label} must be an array`)
|
|
734
|
+
}
|
|
735
|
+
return value
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function readStdinOnce(): Promise<string> {
|
|
739
|
+
const chunks: Buffer[] = []
|
|
740
|
+
for await (const chunk of process.stdin) {
|
|
741
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
742
|
+
}
|
|
743
|
+
return Buffer.concat(chunks).toString("utf8")
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
void main()
|