opencode-planpilot 0.2.3 → 0.2.4
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 +397 -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,1985 @@
|
|
|
1
|
+
// src/lib/models.ts
|
|
2
|
+
function createEmptyStatusChanges() {
|
|
3
|
+
return { steps: [], plans: [], active_plans_cleared: [] };
|
|
4
|
+
}
|
|
5
|
+
function mergeStatusChanges(target, other) {
|
|
6
|
+
target.steps.push(...other.steps);
|
|
7
|
+
target.plans.push(...other.plans);
|
|
8
|
+
target.active_plans_cleared.push(...other.active_plans_cleared);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/lib/errors.ts
|
|
12
|
+
var AppError = class extends Error {
|
|
13
|
+
kind;
|
|
14
|
+
detail;
|
|
15
|
+
constructor(kind, detail) {
|
|
16
|
+
super(detail);
|
|
17
|
+
this.kind = kind;
|
|
18
|
+
this.detail = detail;
|
|
19
|
+
}
|
|
20
|
+
toDisplayString() {
|
|
21
|
+
const label = this.kind === "InvalidInput" ? "Invalid input" : this.kind === "NotFound" ? "Not found" : null;
|
|
22
|
+
if (!label) {
|
|
23
|
+
return this.detail;
|
|
24
|
+
}
|
|
25
|
+
if (this.detail.includes("\n")) {
|
|
26
|
+
return `${label}:
|
|
27
|
+
${this.detail}`;
|
|
28
|
+
}
|
|
29
|
+
return `${label}: ${this.detail}`;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function invalidInput(message) {
|
|
33
|
+
return new AppError("InvalidInput", message);
|
|
34
|
+
}
|
|
35
|
+
function notFound(message) {
|
|
36
|
+
return new AppError("NotFound", message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/lib/util.ts
|
|
40
|
+
function ensureNonEmpty(label, value) {
|
|
41
|
+
if (value.trim().length === 0) {
|
|
42
|
+
throw invalidInput(`${label} cannot be empty`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function formatDateTimeUTC(timestamp) {
|
|
46
|
+
const date = new Date(timestamp);
|
|
47
|
+
const yyyy = date.getUTCFullYear();
|
|
48
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
49
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
50
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
51
|
+
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
52
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
|
|
53
|
+
}
|
|
54
|
+
function uniqueIds(ids) {
|
|
55
|
+
const seen = /* @__PURE__ */ new Set();
|
|
56
|
+
const unique = [];
|
|
57
|
+
for (const id of ids) {
|
|
58
|
+
if (!seen.has(id)) {
|
|
59
|
+
seen.add(id);
|
|
60
|
+
unique.push(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return unique;
|
|
64
|
+
}
|
|
65
|
+
function joinIds(ids) {
|
|
66
|
+
return ids.map((id) => String(id)).join(", ");
|
|
67
|
+
}
|
|
68
|
+
function normalizeCommentEntries(entries) {
|
|
69
|
+
const seen = /* @__PURE__ */ new Map();
|
|
70
|
+
const ordered = [];
|
|
71
|
+
for (const [id, comment] of entries) {
|
|
72
|
+
const idx = seen.get(id);
|
|
73
|
+
if (idx !== void 0) {
|
|
74
|
+
ordered[idx][1] = comment;
|
|
75
|
+
} else {
|
|
76
|
+
seen.set(id, ordered.length);
|
|
77
|
+
ordered.push([id, comment]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return ordered;
|
|
81
|
+
}
|
|
82
|
+
var WAIT_UNTIL_PREFIX = "@wait-until=";
|
|
83
|
+
var WAIT_REASON_PREFIX = "@wait-reason=";
|
|
84
|
+
function parseWaitFromComment(comment) {
|
|
85
|
+
if (!comment) return null;
|
|
86
|
+
let until = null;
|
|
87
|
+
let reason;
|
|
88
|
+
for (const line of comment.split(/\r?\n/)) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (trimmed.startsWith(WAIT_UNTIL_PREFIX)) {
|
|
91
|
+
const raw = trimmed.slice(WAIT_UNTIL_PREFIX.length).trim();
|
|
92
|
+
const value = Number(raw);
|
|
93
|
+
if (Number.isFinite(value)) {
|
|
94
|
+
until = value;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (trimmed.startsWith(WAIT_REASON_PREFIX)) {
|
|
99
|
+
const raw = trimmed.slice(WAIT_REASON_PREFIX.length).trim();
|
|
100
|
+
if (raw) reason = raw;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (until === null) return null;
|
|
104
|
+
return { until, reason };
|
|
105
|
+
}
|
|
106
|
+
function isWaitLine(line) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
return trimmed.startsWith(WAIT_UNTIL_PREFIX) || trimmed.startsWith(WAIT_REASON_PREFIX);
|
|
109
|
+
}
|
|
110
|
+
function upsertWaitInComment(comment, until, reason) {
|
|
111
|
+
const lines = comment ? comment.split(/\r?\n/) : [];
|
|
112
|
+
const filtered = lines.filter((line) => !isWaitLine(line));
|
|
113
|
+
const waitLines = [`${WAIT_UNTIL_PREFIX}${Math.trunc(until)}`];
|
|
114
|
+
const reasonValue = reason?.trim();
|
|
115
|
+
if (reasonValue) {
|
|
116
|
+
waitLines.push(`${WAIT_REASON_PREFIX}${reasonValue}`);
|
|
117
|
+
}
|
|
118
|
+
return [...waitLines, ...filtered].join("\n").trimEnd();
|
|
119
|
+
}
|
|
120
|
+
function removeWaitFromComment(comment) {
|
|
121
|
+
if (!comment) return null;
|
|
122
|
+
const lines = comment.split(/\r?\n/).filter((line) => !isWaitLine(line));
|
|
123
|
+
const cleaned = lines.join("\n").trimEnd();
|
|
124
|
+
return cleaned.length ? cleaned : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/lib/format.ts
|
|
128
|
+
function hasText(value) {
|
|
129
|
+
return value !== void 0 && value !== null && value.trim().length > 0;
|
|
130
|
+
}
|
|
131
|
+
function formatStepDetail(step, goals) {
|
|
132
|
+
let output = "";
|
|
133
|
+
output += `Step ID: ${step.id}
|
|
134
|
+
`;
|
|
135
|
+
output += `Plan ID: ${step.plan_id}
|
|
136
|
+
`;
|
|
137
|
+
output += `Status: ${step.status}
|
|
138
|
+
`;
|
|
139
|
+
output += `Executor: ${step.executor}
|
|
140
|
+
`;
|
|
141
|
+
output += `Content: ${step.content}
|
|
142
|
+
`;
|
|
143
|
+
if (hasText(step.comment)) {
|
|
144
|
+
output += `Comment: ${step.comment ?? ""}
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
output += `Created: ${formatDateTimeUTC(step.created_at)}
|
|
148
|
+
`;
|
|
149
|
+
output += `Updated: ${formatDateTimeUTC(step.updated_at)}
|
|
150
|
+
`;
|
|
151
|
+
output += "\n";
|
|
152
|
+
if (!goals.length) {
|
|
153
|
+
output += "Goals: (none)";
|
|
154
|
+
return output.trimEnd();
|
|
155
|
+
}
|
|
156
|
+
output += "Goals:\n";
|
|
157
|
+
for (const goal of goals) {
|
|
158
|
+
output += `- [${goal.status}] ${goal.content} (goal id ${goal.id})
|
|
159
|
+
`;
|
|
160
|
+
if (hasText(goal.comment)) {
|
|
161
|
+
output += ` Comment: ${goal.comment ?? ""}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return output.trimEnd();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/lib/app.ts
|
|
169
|
+
var PlanpilotApp = class {
|
|
170
|
+
db;
|
|
171
|
+
sessionId;
|
|
172
|
+
cwd;
|
|
173
|
+
constructor(db, sessionId, cwd) {
|
|
174
|
+
this.db = db;
|
|
175
|
+
this.sessionId = sessionId;
|
|
176
|
+
this.cwd = cwd;
|
|
177
|
+
}
|
|
178
|
+
addPlan(input) {
|
|
179
|
+
ensureNonEmpty("plan title", input.title);
|
|
180
|
+
ensureNonEmpty("plan content", input.content);
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const result = this.db.prepare(
|
|
183
|
+
`INSERT INTO plans (title, content, status, comment, last_session_id, last_cwd, created_at, updated_at)
|
|
184
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`
|
|
185
|
+
).run(input.title, input.content, "todo", this.sessionId, this.cwd ?? null, now, now);
|
|
186
|
+
const plan = this.getPlan(result.lastInsertRowid);
|
|
187
|
+
return plan;
|
|
188
|
+
}
|
|
189
|
+
addPlanTree(input, steps) {
|
|
190
|
+
ensureNonEmpty("plan title", input.title);
|
|
191
|
+
ensureNonEmpty("plan content", input.content);
|
|
192
|
+
steps.forEach((step) => {
|
|
193
|
+
ensureNonEmpty("step content", step.content);
|
|
194
|
+
step.goals.forEach((goal) => ensureNonEmpty("goal content", goal));
|
|
195
|
+
});
|
|
196
|
+
const tx = this.db.transaction(() => {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const planResult = this.db.prepare(
|
|
199
|
+
`INSERT INTO plans (title, content, status, comment, last_session_id, last_cwd, created_at, updated_at)
|
|
200
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`
|
|
201
|
+
).run(input.title, input.content, "todo", this.sessionId, this.cwd ?? null, now, now);
|
|
202
|
+
const plan = this.getPlan(planResult.lastInsertRowid);
|
|
203
|
+
let stepCount = 0;
|
|
204
|
+
let goalCount = 0;
|
|
205
|
+
steps.forEach((step, idx) => {
|
|
206
|
+
const stepResult = this.db.prepare(
|
|
207
|
+
`INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
|
|
208
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
209
|
+
).run(plan.id, step.content, "todo", step.executor, idx + 1, now, now);
|
|
210
|
+
const stepId = stepResult.lastInsertRowid;
|
|
211
|
+
stepCount += 1;
|
|
212
|
+
step.goals.forEach((goal) => {
|
|
213
|
+
this.db.prepare(
|
|
214
|
+
`INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
|
|
215
|
+
VALUES (?, ?, ?, NULL, ?, ?)`
|
|
216
|
+
).run(stepId, goal, "todo", now, now);
|
|
217
|
+
goalCount += 1;
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
return { plan, stepCount, goalCount };
|
|
221
|
+
});
|
|
222
|
+
return tx();
|
|
223
|
+
}
|
|
224
|
+
listPlans(order, desc) {
|
|
225
|
+
const orderBy = order ?? "updated";
|
|
226
|
+
const direction = desc ? "DESC" : "ASC";
|
|
227
|
+
const orderColumn = orderBy === "id" ? "id" : orderBy === "title" ? "title" : orderBy === "created" ? "created_at" : "updated_at";
|
|
228
|
+
return this.db.prepare(`SELECT * FROM plans ORDER BY ${orderColumn} ${direction}, id ASC`).all();
|
|
229
|
+
}
|
|
230
|
+
getPlan(id) {
|
|
231
|
+
const row = this.db.prepare("SELECT * FROM plans WHERE id = ?").get(id);
|
|
232
|
+
if (!row) throw notFound(`plan id ${id}`);
|
|
233
|
+
return row;
|
|
234
|
+
}
|
|
235
|
+
getStep(id) {
|
|
236
|
+
const row = this.db.prepare("SELECT * FROM steps WHERE id = ?").get(id);
|
|
237
|
+
if (!row) throw notFound(`step id ${id}`);
|
|
238
|
+
return row;
|
|
239
|
+
}
|
|
240
|
+
getGoal(id) {
|
|
241
|
+
const row = this.db.prepare("SELECT * FROM goals WHERE id = ?").get(id);
|
|
242
|
+
if (!row) throw notFound(`goal id ${id}`);
|
|
243
|
+
return row;
|
|
244
|
+
}
|
|
245
|
+
planWithSteps(id) {
|
|
246
|
+
const plan = this.getPlan(id);
|
|
247
|
+
const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(id);
|
|
248
|
+
return { plan, steps };
|
|
249
|
+
}
|
|
250
|
+
getPlanDetail(id) {
|
|
251
|
+
const plan = this.getPlan(id);
|
|
252
|
+
const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(id);
|
|
253
|
+
const stepIds = steps.map((step) => step.id);
|
|
254
|
+
const goals = this.goalsForSteps(stepIds);
|
|
255
|
+
const goalsMap = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const step of steps) {
|
|
257
|
+
goalsMap.set(step.id, goals.get(step.id) ?? []);
|
|
258
|
+
}
|
|
259
|
+
return { plan, steps, goals: goalsMap };
|
|
260
|
+
}
|
|
261
|
+
getStepDetail(id) {
|
|
262
|
+
const step = this.getStep(id);
|
|
263
|
+
const goals = this.goalsForStep(step.id);
|
|
264
|
+
return { step, goals };
|
|
265
|
+
}
|
|
266
|
+
getGoalDetail(id) {
|
|
267
|
+
const goal = this.getGoal(id);
|
|
268
|
+
const step = this.getStep(goal.step_id);
|
|
269
|
+
return { goal, step };
|
|
270
|
+
}
|
|
271
|
+
getPlanDetails(plans) {
|
|
272
|
+
if (!plans.length) return [];
|
|
273
|
+
const planIds = plans.map((plan) => plan.id);
|
|
274
|
+
const steps = this.db.prepare(`SELECT * FROM steps WHERE plan_id IN (${planIds.map(() => "?").join(",")}) ORDER BY plan_id ASC, sort_order ASC, id ASC`).all(...planIds);
|
|
275
|
+
const stepIds = steps.map((step) => step.id);
|
|
276
|
+
const goalsByStep = this.goalsForSteps(stepIds);
|
|
277
|
+
const stepsByPlan = /* @__PURE__ */ new Map();
|
|
278
|
+
for (const step of steps) {
|
|
279
|
+
const list = stepsByPlan.get(step.plan_id);
|
|
280
|
+
if (list) list.push(step);
|
|
281
|
+
else stepsByPlan.set(step.plan_id, [step]);
|
|
282
|
+
}
|
|
283
|
+
return plans.map((plan) => {
|
|
284
|
+
const planSteps = stepsByPlan.get(plan.id) ?? [];
|
|
285
|
+
const goalsMap = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const step of planSteps) {
|
|
287
|
+
goalsMap.set(step.id, goalsByStep.get(step.id) ?? []);
|
|
288
|
+
}
|
|
289
|
+
return { plan, steps: planSteps, goals: goalsMap };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
getStepsDetail(steps) {
|
|
293
|
+
if (!steps.length) return [];
|
|
294
|
+
const stepIds = steps.map((step) => step.id);
|
|
295
|
+
const goalsMap = this.goalsForSteps(stepIds);
|
|
296
|
+
return steps.map((step) => ({
|
|
297
|
+
step,
|
|
298
|
+
goals: goalsMap.get(step.id) ?? []
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
getActivePlan() {
|
|
302
|
+
const row = this.db.prepare("SELECT * FROM active_plan WHERE session_id = ?").get(this.sessionId);
|
|
303
|
+
return row ?? null;
|
|
304
|
+
}
|
|
305
|
+
setActivePlan(planId, takeover) {
|
|
306
|
+
this.getPlan(planId);
|
|
307
|
+
const tx = this.db.transaction(() => {
|
|
308
|
+
const existing = this.db.prepare("SELECT * FROM active_plan WHERE plan_id = ?").get(planId);
|
|
309
|
+
if (existing && existing.session_id !== this.sessionId && !takeover) {
|
|
310
|
+
throw invalidInput(
|
|
311
|
+
`plan id ${planId} is already active in session ${existing.session_id} (use --force to take over)`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
this.db.prepare("DELETE FROM active_plan WHERE session_id = ?").run(this.sessionId);
|
|
315
|
+
this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(planId);
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
this.db.prepare("INSERT INTO active_plan (session_id, plan_id, updated_at) VALUES (?, ?, ?)").run(this.sessionId, planId, now);
|
|
318
|
+
this.touchPlan(planId);
|
|
319
|
+
const created = this.db.prepare("SELECT * FROM active_plan WHERE session_id = ?").get(this.sessionId);
|
|
320
|
+
if (!created) throw notFound("active plan not found after insert");
|
|
321
|
+
return created;
|
|
322
|
+
});
|
|
323
|
+
return tx();
|
|
324
|
+
}
|
|
325
|
+
clearActivePlan() {
|
|
326
|
+
this.db.prepare("DELETE FROM active_plan WHERE session_id = ?").run(this.sessionId);
|
|
327
|
+
}
|
|
328
|
+
updatePlanWithActiveClear(id, changes) {
|
|
329
|
+
const tx = this.db.transaction(() => {
|
|
330
|
+
const plan = this.updatePlanWithConn(id, changes);
|
|
331
|
+
let cleared = false;
|
|
332
|
+
if (plan.status === "done") {
|
|
333
|
+
cleared = this.clearActivePlansForPlanWithConn(plan.id);
|
|
334
|
+
}
|
|
335
|
+
return { plan, cleared };
|
|
336
|
+
});
|
|
337
|
+
return tx();
|
|
338
|
+
}
|
|
339
|
+
deletePlan(id) {
|
|
340
|
+
const tx = this.db.transaction(() => {
|
|
341
|
+
this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(id);
|
|
342
|
+
const stepIds = this.db.prepare("SELECT id FROM steps WHERE plan_id = ?").all(id).map((row) => row.id);
|
|
343
|
+
if (stepIds.length) {
|
|
344
|
+
this.db.prepare(`DELETE FROM goals WHERE step_id IN (${stepIds.map(() => "?").join(",")})`).run(...stepIds);
|
|
345
|
+
this.db.prepare("DELETE FROM steps WHERE plan_id = ?").run(id);
|
|
346
|
+
}
|
|
347
|
+
const result = this.db.prepare("DELETE FROM plans WHERE id = ?").run(id);
|
|
348
|
+
if (result.changes === 0) {
|
|
349
|
+
throw notFound(`plan id ${id}`);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
tx();
|
|
353
|
+
}
|
|
354
|
+
addStepsBatch(planId, contents, status, executor, at) {
|
|
355
|
+
if (!this.db.prepare("SELECT 1 FROM plans WHERE id = ?").get(planId)) {
|
|
356
|
+
throw notFound(`plan id ${planId}`);
|
|
357
|
+
}
|
|
358
|
+
if (!contents.length) {
|
|
359
|
+
return { steps: [], changes: createEmptyStatusChanges() };
|
|
360
|
+
}
|
|
361
|
+
contents.forEach((content) => ensureNonEmpty("step content", content));
|
|
362
|
+
const tx = this.db.transaction(() => {
|
|
363
|
+
const existing = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
|
|
364
|
+
this.normalizeStepsInPlace(existing);
|
|
365
|
+
const total = existing.length;
|
|
366
|
+
const insertPos = at !== void 0 && at !== null ? at > 0 ? Math.min(at, total + 1) : 1 : total + 1;
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
const shiftBy = contents.length;
|
|
369
|
+
if (shiftBy > 0) {
|
|
370
|
+
for (let idx = existing.length - 1; idx >= 0; idx -= 1) {
|
|
371
|
+
const step = existing[idx];
|
|
372
|
+
if (step.sort_order >= insertPos) {
|
|
373
|
+
const newOrder = step.sort_order + shiftBy;
|
|
374
|
+
this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(newOrder, now, step.id);
|
|
375
|
+
step.sort_order = newOrder;
|
|
376
|
+
step.updated_at = now;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const created = [];
|
|
381
|
+
contents.forEach((content, idx) => {
|
|
382
|
+
const sortOrder = insertPos + idx;
|
|
383
|
+
const result = this.db.prepare(
|
|
384
|
+
`INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
|
|
385
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
386
|
+
).run(planId, content, status, executor, sortOrder, now, now);
|
|
387
|
+
const step = this.getStep(result.lastInsertRowid);
|
|
388
|
+
created.push(step);
|
|
389
|
+
});
|
|
390
|
+
const changes = this.refreshPlanStatus(planId);
|
|
391
|
+
this.touchPlan(planId);
|
|
392
|
+
return { steps: created, changes };
|
|
393
|
+
});
|
|
394
|
+
return tx();
|
|
395
|
+
}
|
|
396
|
+
addStepTree(planId, content, executor, goals) {
|
|
397
|
+
ensureNonEmpty("step content", content);
|
|
398
|
+
goals.forEach((goal) => ensureNonEmpty("goal content", goal));
|
|
399
|
+
const tx = this.db.transaction(() => {
|
|
400
|
+
if (!this.db.prepare("SELECT 1 FROM plans WHERE id = ?").get(planId)) {
|
|
401
|
+
throw notFound(`plan id ${planId}`);
|
|
402
|
+
}
|
|
403
|
+
const existing = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
|
|
404
|
+
this.normalizeStepsInPlace(existing);
|
|
405
|
+
const sortOrder = existing.length + 1;
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const stepResult = this.db.prepare(
|
|
408
|
+
`INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
|
|
409
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
410
|
+
).run(planId, content, "todo", executor, sortOrder, now, now);
|
|
411
|
+
const step = this.getStep(stepResult.lastInsertRowid);
|
|
412
|
+
const createdGoals = [];
|
|
413
|
+
for (const goalContent of goals) {
|
|
414
|
+
const goalResult = this.db.prepare(
|
|
415
|
+
`INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
|
|
416
|
+
VALUES (?, ?, ?, NULL, ?, ?)`
|
|
417
|
+
).run(step.id, goalContent, "todo", now, now);
|
|
418
|
+
createdGoals.push(this.getGoal(goalResult.lastInsertRowid));
|
|
419
|
+
}
|
|
420
|
+
const changes = this.refreshPlanStatus(planId);
|
|
421
|
+
this.touchPlan(planId);
|
|
422
|
+
return { step, goals: createdGoals, changes };
|
|
423
|
+
});
|
|
424
|
+
return tx();
|
|
425
|
+
}
|
|
426
|
+
listStepsFiltered(planId, query) {
|
|
427
|
+
this.getPlan(planId);
|
|
428
|
+
const conditions = ["plan_id = ?"];
|
|
429
|
+
const params = [planId];
|
|
430
|
+
if (query.status) {
|
|
431
|
+
conditions.push("status = ?");
|
|
432
|
+
params.push(query.status);
|
|
433
|
+
}
|
|
434
|
+
if (query.executor) {
|
|
435
|
+
conditions.push("executor = ?");
|
|
436
|
+
params.push(query.executor);
|
|
437
|
+
}
|
|
438
|
+
const order = query.order ?? "order";
|
|
439
|
+
const direction = query.desc ? "DESC" : "ASC";
|
|
440
|
+
const orderColumn = order === "id" ? "id" : order === "created" ? "created_at" : order === "updated" ? "updated_at" : "sort_order";
|
|
441
|
+
let sql = `SELECT * FROM steps WHERE ${conditions.join(" AND ")} ORDER BY ${orderColumn} ${direction}, id ASC`;
|
|
442
|
+
if (query.limit !== void 0) {
|
|
443
|
+
sql += " LIMIT ?";
|
|
444
|
+
params.push(query.limit);
|
|
445
|
+
}
|
|
446
|
+
if (query.offset !== void 0) {
|
|
447
|
+
sql += " OFFSET ?";
|
|
448
|
+
params.push(query.offset);
|
|
449
|
+
}
|
|
450
|
+
return this.db.prepare(sql).all(...params);
|
|
451
|
+
}
|
|
452
|
+
countSteps(planId, query) {
|
|
453
|
+
this.getPlan(planId);
|
|
454
|
+
const conditions = ["plan_id = ?"];
|
|
455
|
+
const params = [planId];
|
|
456
|
+
if (query.status) {
|
|
457
|
+
conditions.push("status = ?");
|
|
458
|
+
params.push(query.status);
|
|
459
|
+
}
|
|
460
|
+
if (query.executor) {
|
|
461
|
+
conditions.push("executor = ?");
|
|
462
|
+
params.push(query.executor);
|
|
463
|
+
}
|
|
464
|
+
const row = this.db.prepare(`SELECT COUNT(*) as count FROM steps WHERE ${conditions.join(" AND ")}`).get(...params);
|
|
465
|
+
return row.count;
|
|
466
|
+
}
|
|
467
|
+
nextStep(planId) {
|
|
468
|
+
const row = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? AND status = ? ORDER BY sort_order ASC, id ASC LIMIT 1").get(planId, "todo");
|
|
469
|
+
return row ?? null;
|
|
470
|
+
}
|
|
471
|
+
updateStep(id, changes) {
|
|
472
|
+
const tx = this.db.transaction(() => {
|
|
473
|
+
if (changes.content !== void 0) {
|
|
474
|
+
ensureNonEmpty("step content", changes.content);
|
|
475
|
+
}
|
|
476
|
+
if (changes.status === "done") {
|
|
477
|
+
const pending = this.nextGoalForStep(id);
|
|
478
|
+
if (pending) {
|
|
479
|
+
throw invalidInput(`cannot mark step done; next pending goal: ${pending.content} (id ${pending.id})`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const existing = this.getStep(id);
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
const updated = {
|
|
485
|
+
content: changes.content ?? existing.content,
|
|
486
|
+
status: changes.status ?? existing.status,
|
|
487
|
+
executor: changes.executor ?? existing.executor,
|
|
488
|
+
comment: changes.comment !== void 0 ? changes.comment : existing.comment
|
|
489
|
+
};
|
|
490
|
+
this.db.prepare(
|
|
491
|
+
`UPDATE steps SET content = ?, status = ?, executor = ?, comment = ?, updated_at = ? WHERE id = ?`
|
|
492
|
+
).run(updated.content, updated.status, updated.executor, updated.comment, now, id);
|
|
493
|
+
const step = this.getStep(id);
|
|
494
|
+
const statusChanges = createEmptyStatusChanges();
|
|
495
|
+
if (changes.status !== void 0) {
|
|
496
|
+
mergeStatusChanges(statusChanges, this.refreshPlanStatus(step.plan_id));
|
|
497
|
+
}
|
|
498
|
+
this.touchPlan(step.plan_id);
|
|
499
|
+
return { step, changes: statusChanges };
|
|
500
|
+
});
|
|
501
|
+
return tx();
|
|
502
|
+
}
|
|
503
|
+
setStepDoneWithGoals(id, allGoals) {
|
|
504
|
+
const tx = this.db.transaction(() => {
|
|
505
|
+
let changes = createEmptyStatusChanges();
|
|
506
|
+
if (allGoals) {
|
|
507
|
+
const goalChanges = this.setAllGoalsDoneForStep(id);
|
|
508
|
+
mergeStatusChanges(changes, goalChanges);
|
|
509
|
+
} else {
|
|
510
|
+
const pending = this.nextGoalForStep(id);
|
|
511
|
+
if (pending) {
|
|
512
|
+
throw invalidInput(`cannot mark step done; next pending goal: ${pending.content} (id ${pending.id})`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const existing = this.getStep(id);
|
|
516
|
+
if (existing.status !== "done") {
|
|
517
|
+
const now = Date.now();
|
|
518
|
+
this.db.prepare("UPDATE steps SET status = ?, updated_at = ? WHERE id = ?").run("done", now, id);
|
|
519
|
+
}
|
|
520
|
+
const step = this.getStep(id);
|
|
521
|
+
mergeStatusChanges(changes, this.refreshPlanStatus(step.plan_id));
|
|
522
|
+
this.touchPlan(step.plan_id);
|
|
523
|
+
return { step, changes };
|
|
524
|
+
});
|
|
525
|
+
return tx();
|
|
526
|
+
}
|
|
527
|
+
moveStep(id, to) {
|
|
528
|
+
const tx = this.db.transaction(() => {
|
|
529
|
+
const target = this.getStep(id);
|
|
530
|
+
const planId = target.plan_id;
|
|
531
|
+
const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
|
|
532
|
+
const currentIndex = steps.findIndex((step) => step.id === id);
|
|
533
|
+
if (currentIndex === -1) throw notFound(`step id ${id}`);
|
|
534
|
+
let desiredIndex = Math.max(to - 1, 0);
|
|
535
|
+
if (desiredIndex >= steps.length) desiredIndex = steps.length - 1;
|
|
536
|
+
const [moving] = steps.splice(currentIndex, 1);
|
|
537
|
+
if (desiredIndex >= steps.length) steps.push(moving);
|
|
538
|
+
else steps.splice(desiredIndex, 0, moving);
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
steps.forEach((step, idx) => {
|
|
541
|
+
const desiredOrder = idx + 1;
|
|
542
|
+
if (step.sort_order !== desiredOrder) {
|
|
543
|
+
this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(desiredOrder, now, step.id);
|
|
544
|
+
step.sort_order = desiredOrder;
|
|
545
|
+
step.updated_at = now;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
return steps;
|
|
549
|
+
});
|
|
550
|
+
return tx();
|
|
551
|
+
}
|
|
552
|
+
deleteSteps(ids) {
|
|
553
|
+
const tx = this.db.transaction(() => {
|
|
554
|
+
if (!ids.length) return { deleted: 0, changes: createEmptyStatusChanges() };
|
|
555
|
+
const unique = uniqueIds(ids);
|
|
556
|
+
const steps = this.db.prepare(`SELECT * FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
|
|
557
|
+
const existing = new Set(steps.map((step) => step.id));
|
|
558
|
+
const missing = unique.filter((id) => !existing.has(id));
|
|
559
|
+
if (missing.length) {
|
|
560
|
+
throw notFound(`step id(s) not found: ${joinIds(missing)}`);
|
|
561
|
+
}
|
|
562
|
+
const planIds = Array.from(new Set(steps.map((step) => step.plan_id)));
|
|
563
|
+
if (unique.length) {
|
|
564
|
+
this.db.prepare(`DELETE FROM goals WHERE step_id IN (${unique.map(() => "?").join(",")})`).run(...unique);
|
|
565
|
+
}
|
|
566
|
+
const result = this.db.prepare(`DELETE FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).run(...unique);
|
|
567
|
+
planIds.forEach((planId) => this.normalizeStepsForPlan(planId));
|
|
568
|
+
const changes = createEmptyStatusChanges();
|
|
569
|
+
planIds.forEach((planId) => mergeStatusChanges(changes, this.refreshPlanStatus(planId)));
|
|
570
|
+
if (planIds.length) {
|
|
571
|
+
this.touchPlans(planIds);
|
|
572
|
+
}
|
|
573
|
+
return { deleted: result.changes, changes };
|
|
574
|
+
});
|
|
575
|
+
return tx();
|
|
576
|
+
}
|
|
577
|
+
addGoalsBatch(stepId, contents, status) {
|
|
578
|
+
if (!contents.length) return { goals: [], changes: createEmptyStatusChanges() };
|
|
579
|
+
contents.forEach((content) => ensureNonEmpty("goal content", content));
|
|
580
|
+
const tx = this.db.transaction(() => {
|
|
581
|
+
const step = this.getStep(stepId);
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const created = [];
|
|
584
|
+
contents.forEach((content) => {
|
|
585
|
+
const result = this.db.prepare(
|
|
586
|
+
`INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
|
|
587
|
+
VALUES (?, ?, ?, NULL, ?, ?)`
|
|
588
|
+
).run(stepId, content, status, now, now);
|
|
589
|
+
created.push(this.getGoal(result.lastInsertRowid));
|
|
590
|
+
});
|
|
591
|
+
const changes = this.refreshStepStatus(stepId);
|
|
592
|
+
this.touchPlan(step.plan_id);
|
|
593
|
+
return { goals: created, changes };
|
|
594
|
+
});
|
|
595
|
+
return tx();
|
|
596
|
+
}
|
|
597
|
+
listGoalsFiltered(stepId, query) {
|
|
598
|
+
this.getStep(stepId);
|
|
599
|
+
const conditions = ["step_id = ?"];
|
|
600
|
+
const params = [stepId];
|
|
601
|
+
if (query.status) {
|
|
602
|
+
conditions.push("status = ?");
|
|
603
|
+
params.push(query.status);
|
|
604
|
+
}
|
|
605
|
+
let sql = `SELECT * FROM goals WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, id DESC`;
|
|
606
|
+
if (query.limit !== void 0) {
|
|
607
|
+
sql += " LIMIT ?";
|
|
608
|
+
params.push(query.limit);
|
|
609
|
+
}
|
|
610
|
+
if (query.offset !== void 0) {
|
|
611
|
+
sql += " OFFSET ?";
|
|
612
|
+
params.push(query.offset);
|
|
613
|
+
}
|
|
614
|
+
return this.db.prepare(sql).all(...params);
|
|
615
|
+
}
|
|
616
|
+
countGoals(stepId, query) {
|
|
617
|
+
this.getStep(stepId);
|
|
618
|
+
const conditions = ["step_id = ?"];
|
|
619
|
+
const params = [stepId];
|
|
620
|
+
if (query.status) {
|
|
621
|
+
conditions.push("status = ?");
|
|
622
|
+
params.push(query.status);
|
|
623
|
+
}
|
|
624
|
+
const row = this.db.prepare(`SELECT COUNT(*) as count FROM goals WHERE ${conditions.join(" AND ")}`).get(...params);
|
|
625
|
+
return row.count;
|
|
626
|
+
}
|
|
627
|
+
updateGoal(id, changes) {
|
|
628
|
+
const tx = this.db.transaction(() => {
|
|
629
|
+
if (changes.content !== void 0) {
|
|
630
|
+
ensureNonEmpty("goal content", changes.content);
|
|
631
|
+
}
|
|
632
|
+
const existing = this.getGoal(id);
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
const updated = {
|
|
635
|
+
content: changes.content ?? existing.content,
|
|
636
|
+
status: changes.status ?? existing.status,
|
|
637
|
+
comment: changes.comment !== void 0 ? changes.comment : existing.comment
|
|
638
|
+
};
|
|
639
|
+
this.db.prepare("UPDATE goals SET content = ?, status = ?, comment = ?, updated_at = ? WHERE id = ?").run(updated.content, updated.status, updated.comment, now, id);
|
|
640
|
+
const goal = this.getGoal(id);
|
|
641
|
+
const statusChanges = createEmptyStatusChanges();
|
|
642
|
+
if (changes.status !== void 0) {
|
|
643
|
+
mergeStatusChanges(statusChanges, this.refreshStepStatus(goal.step_id));
|
|
644
|
+
}
|
|
645
|
+
const step = this.getStep(goal.step_id);
|
|
646
|
+
this.touchPlan(step.plan_id);
|
|
647
|
+
return { goal, changes: statusChanges };
|
|
648
|
+
});
|
|
649
|
+
return tx();
|
|
650
|
+
}
|
|
651
|
+
setGoalStatus(id, status) {
|
|
652
|
+
const tx = this.db.transaction(() => {
|
|
653
|
+
this.getGoal(id);
|
|
654
|
+
const now = Date.now();
|
|
655
|
+
this.db.prepare("UPDATE goals SET status = ?, updated_at = ? WHERE id = ?").run(status, now, id);
|
|
656
|
+
const goal = this.getGoal(id);
|
|
657
|
+
const changes = this.refreshStepStatus(goal.step_id);
|
|
658
|
+
const step = this.getStep(goal.step_id);
|
|
659
|
+
this.touchPlan(step.plan_id);
|
|
660
|
+
return { goal, changes };
|
|
661
|
+
});
|
|
662
|
+
return tx();
|
|
663
|
+
}
|
|
664
|
+
setGoalsStatus(ids, status) {
|
|
665
|
+
if (!ids.length) return { updated: 0, changes: createEmptyStatusChanges() };
|
|
666
|
+
const tx = this.db.transaction(() => {
|
|
667
|
+
const unique = uniqueIds(ids);
|
|
668
|
+
const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
|
|
669
|
+
const existing = new Set(goals.map((goal) => goal.id));
|
|
670
|
+
const missing = unique.filter((id) => !existing.has(id));
|
|
671
|
+
if (missing.length) {
|
|
672
|
+
throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
|
|
673
|
+
}
|
|
674
|
+
const now = Date.now();
|
|
675
|
+
const stepIds = [];
|
|
676
|
+
const stepSeen = /* @__PURE__ */ new Set();
|
|
677
|
+
goals.forEach((goal) => {
|
|
678
|
+
if (!stepSeen.has(goal.step_id)) {
|
|
679
|
+
stepSeen.add(goal.step_id);
|
|
680
|
+
stepIds.push(goal.step_id);
|
|
681
|
+
}
|
|
682
|
+
this.db.prepare("UPDATE goals SET status = ?, updated_at = ? WHERE id = ?").run(status, now, goal.id);
|
|
683
|
+
});
|
|
684
|
+
const changes = createEmptyStatusChanges();
|
|
685
|
+
stepIds.forEach((stepId) => mergeStatusChanges(changes, this.refreshStepStatus(stepId)));
|
|
686
|
+
const planIds = [];
|
|
687
|
+
if (stepIds.length) {
|
|
688
|
+
const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
|
|
689
|
+
const seen = /* @__PURE__ */ new Set();
|
|
690
|
+
steps.forEach((row) => {
|
|
691
|
+
if (!seen.has(row.plan_id)) {
|
|
692
|
+
seen.add(row.plan_id);
|
|
693
|
+
planIds.push(row.plan_id);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (planIds.length) this.touchPlans(planIds);
|
|
698
|
+
return { updated: unique.length, changes };
|
|
699
|
+
});
|
|
700
|
+
return tx();
|
|
701
|
+
}
|
|
702
|
+
deleteGoals(ids) {
|
|
703
|
+
const tx = this.db.transaction(() => {
|
|
704
|
+
if (!ids.length) return { deleted: 0, changes: createEmptyStatusChanges() };
|
|
705
|
+
const unique = uniqueIds(ids);
|
|
706
|
+
const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
|
|
707
|
+
const existing = new Set(goals.map((goal) => goal.id));
|
|
708
|
+
const missing = unique.filter((id) => !existing.has(id));
|
|
709
|
+
if (missing.length) {
|
|
710
|
+
throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
|
|
711
|
+
}
|
|
712
|
+
const stepIds = Array.from(new Set(goals.map((goal) => goal.step_id)));
|
|
713
|
+
const result = this.db.prepare(`DELETE FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).run(...unique);
|
|
714
|
+
const changes = createEmptyStatusChanges();
|
|
715
|
+
stepIds.forEach((stepId) => mergeStatusChanges(changes, this.refreshStepStatus(stepId)));
|
|
716
|
+
if (stepIds.length) {
|
|
717
|
+
const planIds = [];
|
|
718
|
+
const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
|
|
719
|
+
const seen = /* @__PURE__ */ new Set();
|
|
720
|
+
steps.forEach((row) => {
|
|
721
|
+
if (!seen.has(row.plan_id)) {
|
|
722
|
+
seen.add(row.plan_id);
|
|
723
|
+
planIds.push(row.plan_id);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
if (planIds.length) this.touchPlans(planIds);
|
|
727
|
+
}
|
|
728
|
+
return { deleted: result.changes, changes };
|
|
729
|
+
});
|
|
730
|
+
return tx();
|
|
731
|
+
}
|
|
732
|
+
commentPlans(entries) {
|
|
733
|
+
const normalized = normalizeCommentEntries(entries);
|
|
734
|
+
if (!normalized.length) return [];
|
|
735
|
+
const ids = normalized.map(([id]) => id);
|
|
736
|
+
const tx = this.db.transaction(() => {
|
|
737
|
+
const existing = this.db.prepare(`SELECT id FROM plans WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids).map((row) => row.id);
|
|
738
|
+
const existingSet = new Set(existing);
|
|
739
|
+
const missing = ids.filter((id) => !existingSet.has(id));
|
|
740
|
+
if (missing.length) {
|
|
741
|
+
throw notFound(`plan id(s) not found: ${joinIds(missing)}`);
|
|
742
|
+
}
|
|
743
|
+
const now = Date.now();
|
|
744
|
+
normalized.forEach(([planId, comment]) => {
|
|
745
|
+
if (this.cwd) {
|
|
746
|
+
this.db.prepare("UPDATE plans SET comment = ?, last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?").run(comment, this.sessionId, this.cwd, now, planId);
|
|
747
|
+
} else {
|
|
748
|
+
this.db.prepare("UPDATE plans SET comment = ?, last_session_id = ?, updated_at = ? WHERE id = ?").run(comment, this.sessionId, now, planId);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
return ids;
|
|
752
|
+
});
|
|
753
|
+
return tx();
|
|
754
|
+
}
|
|
755
|
+
commentSteps(entries) {
|
|
756
|
+
const normalized = normalizeCommentEntries(entries);
|
|
757
|
+
if (!normalized.length) return [];
|
|
758
|
+
const ids = normalized.map(([id]) => id);
|
|
759
|
+
const tx = this.db.transaction(() => {
|
|
760
|
+
const steps = this.db.prepare(`SELECT * FROM steps WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids);
|
|
761
|
+
const existing = new Set(steps.map((step) => step.id));
|
|
762
|
+
const missing = ids.filter((id) => !existing.has(id));
|
|
763
|
+
if (missing.length) {
|
|
764
|
+
throw notFound(`step id(s) not found: ${joinIds(missing)}`);
|
|
765
|
+
}
|
|
766
|
+
const planIds = Array.from(new Set(steps.map((step) => step.plan_id)));
|
|
767
|
+
const now = Date.now();
|
|
768
|
+
normalized.forEach(([stepId, comment]) => {
|
|
769
|
+
this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
|
|
770
|
+
});
|
|
771
|
+
if (planIds.length) this.touchPlans(planIds);
|
|
772
|
+
return planIds;
|
|
773
|
+
});
|
|
774
|
+
return tx();
|
|
775
|
+
}
|
|
776
|
+
setStepWait(stepId, delayMs, reason) {
|
|
777
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
|
778
|
+
throw invalidInput("delay must be a non-negative number");
|
|
779
|
+
}
|
|
780
|
+
const tx = this.db.transaction(() => {
|
|
781
|
+
const step = this.getStep(stepId);
|
|
782
|
+
const now = Date.now();
|
|
783
|
+
const until = now + Math.trunc(delayMs);
|
|
784
|
+
const comment = upsertWaitInComment(step.comment, until, reason);
|
|
785
|
+
this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
|
|
786
|
+
const updated = this.getStep(stepId);
|
|
787
|
+
this.touchPlan(updated.plan_id);
|
|
788
|
+
return { step: updated, until };
|
|
789
|
+
});
|
|
790
|
+
return tx();
|
|
791
|
+
}
|
|
792
|
+
clearStepWait(stepId) {
|
|
793
|
+
const tx = this.db.transaction(() => {
|
|
794
|
+
const step = this.getStep(stepId);
|
|
795
|
+
const comment = step.comment ? removeWaitFromComment(step.comment) : null;
|
|
796
|
+
const now = Date.now();
|
|
797
|
+
this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
|
|
798
|
+
const updated = this.getStep(stepId);
|
|
799
|
+
this.touchPlan(updated.plan_id);
|
|
800
|
+
return { step: updated };
|
|
801
|
+
});
|
|
802
|
+
return tx();
|
|
803
|
+
}
|
|
804
|
+
getStepWait(stepId) {
|
|
805
|
+
const step = this.getStep(stepId);
|
|
806
|
+
const wait = parseWaitFromComment(step.comment);
|
|
807
|
+
return { step, wait };
|
|
808
|
+
}
|
|
809
|
+
commentGoals(entries) {
|
|
810
|
+
const normalized = normalizeCommentEntries(entries);
|
|
811
|
+
if (!normalized.length) return [];
|
|
812
|
+
const ids = normalized.map(([id]) => id);
|
|
813
|
+
const tx = this.db.transaction(() => {
|
|
814
|
+
const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids);
|
|
815
|
+
const existing = new Set(goals.map((goal) => goal.id));
|
|
816
|
+
const missing = ids.filter((id) => !existing.has(id));
|
|
817
|
+
if (missing.length) {
|
|
818
|
+
throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
|
|
819
|
+
}
|
|
820
|
+
const stepIds = Array.from(new Set(goals.map((goal) => goal.step_id)));
|
|
821
|
+
const now = Date.now();
|
|
822
|
+
normalized.forEach(([goalId, comment]) => {
|
|
823
|
+
this.db.prepare("UPDATE goals SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, goalId);
|
|
824
|
+
});
|
|
825
|
+
if (stepIds.length) {
|
|
826
|
+
const planIds = [];
|
|
827
|
+
const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
|
|
828
|
+
const seen = /* @__PURE__ */ new Set();
|
|
829
|
+
steps.forEach((row) => {
|
|
830
|
+
if (!seen.has(row.plan_id)) {
|
|
831
|
+
seen.add(row.plan_id);
|
|
832
|
+
planIds.push(row.plan_id);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
if (planIds.length) this.touchPlans(planIds);
|
|
836
|
+
return planIds;
|
|
837
|
+
}
|
|
838
|
+
return [];
|
|
839
|
+
});
|
|
840
|
+
return tx();
|
|
841
|
+
}
|
|
842
|
+
goalsForStep(stepId) {
|
|
843
|
+
return this.db.prepare("SELECT * FROM goals WHERE step_id = ? ORDER BY id ASC").all(stepId);
|
|
844
|
+
}
|
|
845
|
+
goalsForSteps(stepIds) {
|
|
846
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
847
|
+
if (!stepIds.length) return grouped;
|
|
848
|
+
const rows = this.db.prepare(`SELECT * FROM goals WHERE step_id IN (${stepIds.map(() => "?").join(",")}) ORDER BY step_id ASC, id ASC`).all(...stepIds);
|
|
849
|
+
rows.forEach((goal) => {
|
|
850
|
+
const list = grouped.get(goal.step_id);
|
|
851
|
+
if (list) list.push(goal);
|
|
852
|
+
else grouped.set(goal.step_id, [goal]);
|
|
853
|
+
});
|
|
854
|
+
return grouped;
|
|
855
|
+
}
|
|
856
|
+
planIdsForSteps(ids) {
|
|
857
|
+
if (!ids.length) return [];
|
|
858
|
+
const unique = uniqueIds(ids);
|
|
859
|
+
const rows = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
|
|
860
|
+
const seen = /* @__PURE__ */ new Set();
|
|
861
|
+
const planIds = [];
|
|
862
|
+
rows.forEach((row) => {
|
|
863
|
+
if (!seen.has(row.plan_id)) {
|
|
864
|
+
seen.add(row.plan_id);
|
|
865
|
+
planIds.push(row.plan_id);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return planIds;
|
|
869
|
+
}
|
|
870
|
+
planIdsForGoals(ids) {
|
|
871
|
+
if (!ids.length) return [];
|
|
872
|
+
const unique = uniqueIds(ids);
|
|
873
|
+
const goals = this.db.prepare(`SELECT step_id FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
|
|
874
|
+
const stepIds = Array.from(new Set(goals.map((row) => row.step_id)));
|
|
875
|
+
if (!stepIds.length) return [];
|
|
876
|
+
const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
|
|
877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
878
|
+
const planIds = [];
|
|
879
|
+
steps.forEach((row) => {
|
|
880
|
+
if (!seen.has(row.plan_id)) {
|
|
881
|
+
seen.add(row.plan_id);
|
|
882
|
+
planIds.push(row.plan_id);
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
return planIds;
|
|
886
|
+
}
|
|
887
|
+
updatePlanWithConn(id, changes) {
|
|
888
|
+
if (changes.title !== void 0) ensureNonEmpty("plan title", changes.title);
|
|
889
|
+
if (changes.content !== void 0) ensureNonEmpty("plan content", changes.content);
|
|
890
|
+
if (changes.status === "done") {
|
|
891
|
+
const total = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ?").get(id);
|
|
892
|
+
if (total.count > 0) {
|
|
893
|
+
const next = this.nextStep(id);
|
|
894
|
+
if (next) {
|
|
895
|
+
const goals = this.goalsForStep(next.id);
|
|
896
|
+
const detail = formatStepDetail(next, goals);
|
|
897
|
+
throw invalidInput(`cannot mark plan done; next pending step:
|
|
898
|
+
${detail}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const existing = this.getPlan(id);
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
const updated = {
|
|
905
|
+
title: changes.title ?? existing.title,
|
|
906
|
+
content: changes.content ?? existing.content,
|
|
907
|
+
status: changes.status ?? existing.status,
|
|
908
|
+
comment: changes.comment !== void 0 ? changes.comment : existing.comment
|
|
909
|
+
};
|
|
910
|
+
if (this.cwd) {
|
|
911
|
+
this.db.prepare(
|
|
912
|
+
`UPDATE plans SET title = ?, content = ?, status = ?, comment = ?, last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?`
|
|
913
|
+
).run(updated.title, updated.content, updated.status, updated.comment, this.sessionId, this.cwd, now, id);
|
|
914
|
+
} else {
|
|
915
|
+
this.db.prepare(
|
|
916
|
+
`UPDATE plans SET title = ?, content = ?, status = ?, comment = ?, last_session_id = ?, updated_at = ? WHERE id = ?`
|
|
917
|
+
).run(updated.title, updated.content, updated.status, updated.comment, this.sessionId, now, id);
|
|
918
|
+
}
|
|
919
|
+
return this.getPlan(id);
|
|
920
|
+
}
|
|
921
|
+
refreshPlanStatus(planId) {
|
|
922
|
+
const total = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ?").get(planId);
|
|
923
|
+
if (total.count === 0) return createEmptyStatusChanges();
|
|
924
|
+
const done = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ? AND status = ?").get(planId, "done");
|
|
925
|
+
const status = done.count === total.count ? "done" : "todo";
|
|
926
|
+
const plan = this.getPlan(planId);
|
|
927
|
+
const changes = createEmptyStatusChanges();
|
|
928
|
+
if (plan.status !== status) {
|
|
929
|
+
const now = Date.now();
|
|
930
|
+
const reason = done.count === total.count ? `all steps are done (${done.count}/${total.count})` : `steps done ${done.count}/${total.count}`;
|
|
931
|
+
this.db.prepare("UPDATE plans SET status = ?, updated_at = ? WHERE id = ?").run(status, now, planId);
|
|
932
|
+
changes.plans.push({ plan_id: planId, from: plan.status, to: status, reason });
|
|
933
|
+
if (status === "done") {
|
|
934
|
+
const clearedCurrent = this.clearActivePlansForPlanWithConn(planId);
|
|
935
|
+
if (clearedCurrent) {
|
|
936
|
+
changes.active_plans_cleared.push({ plan_id: planId, reason: "plan marked done" });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return changes;
|
|
941
|
+
}
|
|
942
|
+
refreshStepStatus(stepId) {
|
|
943
|
+
const goals = this.db.prepare("SELECT * FROM goals WHERE step_id = ? ORDER BY id ASC").all(stepId);
|
|
944
|
+
if (!goals.length) return createEmptyStatusChanges();
|
|
945
|
+
const doneCount = goals.filter((goal) => goal.status === "done").length;
|
|
946
|
+
const total = goals.length;
|
|
947
|
+
const status = doneCount === total ? "done" : "todo";
|
|
948
|
+
const step = this.getStep(stepId);
|
|
949
|
+
const changes = createEmptyStatusChanges();
|
|
950
|
+
if (step.status !== status) {
|
|
951
|
+
const now = Date.now();
|
|
952
|
+
const reason = doneCount === total ? `all goals are done (${doneCount}/${total})` : `goals done ${doneCount}/${total}`;
|
|
953
|
+
this.db.prepare("UPDATE steps SET status = ?, updated_at = ? WHERE id = ?").run(status, now, stepId);
|
|
954
|
+
changes.steps.push({ step_id: stepId, from: step.status, to: status, reason });
|
|
955
|
+
}
|
|
956
|
+
mergeStatusChanges(changes, this.refreshPlanStatus(step.plan_id));
|
|
957
|
+
return changes;
|
|
958
|
+
}
|
|
959
|
+
nextGoalForStep(stepId) {
|
|
960
|
+
const row = this.db.prepare("SELECT * FROM goals WHERE step_id = ? AND status = ? ORDER BY id ASC LIMIT 1").get(stepId, "todo");
|
|
961
|
+
return row ?? null;
|
|
962
|
+
}
|
|
963
|
+
setAllGoalsDoneForStep(stepId) {
|
|
964
|
+
this.getStep(stepId);
|
|
965
|
+
const goals = this.goalsForStep(stepId);
|
|
966
|
+
if (!goals.length) return createEmptyStatusChanges();
|
|
967
|
+
const ids = goals.map((goal) => goal.id);
|
|
968
|
+
return this.setGoalsStatus(ids, "done").changes;
|
|
969
|
+
}
|
|
970
|
+
touchPlan(planId) {
|
|
971
|
+
const now = Date.now();
|
|
972
|
+
if (this.cwd) {
|
|
973
|
+
const result = this.db.prepare("UPDATE plans SET last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?").run(this.sessionId, this.cwd, now, planId);
|
|
974
|
+
if (result.changes === 0) {
|
|
975
|
+
throw notFound(`plan id ${planId}`);
|
|
976
|
+
}
|
|
977
|
+
} else {
|
|
978
|
+
const result = this.db.prepare("UPDATE plans SET last_session_id = ?, updated_at = ? WHERE id = ?").run(this.sessionId, now, planId);
|
|
979
|
+
if (result.changes === 0) {
|
|
980
|
+
throw notFound(`plan id ${planId}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
touchPlans(planIds) {
|
|
985
|
+
planIds.forEach((planId) => this.touchPlan(planId));
|
|
986
|
+
}
|
|
987
|
+
normalizeStepsForPlan(planId) {
|
|
988
|
+
const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
|
|
989
|
+
this.normalizeStepsInPlace(steps);
|
|
990
|
+
}
|
|
991
|
+
normalizeStepsInPlace(steps) {
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
steps.forEach((step, idx) => {
|
|
994
|
+
const desired = idx + 1;
|
|
995
|
+
if (step.sort_order !== desired) {
|
|
996
|
+
this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(desired, now, step.id);
|
|
997
|
+
step.sort_order = desired;
|
|
998
|
+
step.updated_at = now;
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
clearActivePlansForPlanWithConn(planId) {
|
|
1003
|
+
const existing = this.db.prepare("SELECT * FROM active_plan WHERE plan_id = ?").all(planId);
|
|
1004
|
+
const clearedCurrent = existing.some((row) => row.session_id === this.sessionId);
|
|
1005
|
+
this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(planId);
|
|
1006
|
+
return clearedCurrent;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// src/lib/config.ts
|
|
1011
|
+
import fs2 from "fs";
|
|
1012
|
+
import path2 from "path";
|
|
1013
|
+
|
|
1014
|
+
// src/lib/db.ts
|
|
1015
|
+
import fs from "fs";
|
|
1016
|
+
import path from "path";
|
|
1017
|
+
import os from "os";
|
|
1018
|
+
import { Database } from "bun:sqlite";
|
|
1019
|
+
import { xdgConfig } from "xdg-basedir";
|
|
1020
|
+
var cachedDb = null;
|
|
1021
|
+
function resolveConfigRoot() {
|
|
1022
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
1023
|
+
return process.env.OPENCODE_CONFIG_DIR;
|
|
1024
|
+
}
|
|
1025
|
+
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
1026
|
+
return path.join(base, "opencode");
|
|
1027
|
+
}
|
|
1028
|
+
function resolvePlanpilotDir() {
|
|
1029
|
+
const override = process.env.OPENCODE_PLANPILOT_DIR || process.env.OPENCODE_PLANPILOT_HOME;
|
|
1030
|
+
if (override && override.trim()) return override;
|
|
1031
|
+
return path.join(resolveConfigRoot(), ".planpilot");
|
|
1032
|
+
}
|
|
1033
|
+
function resolveDbPath() {
|
|
1034
|
+
return path.join(resolvePlanpilotDir(), "planpilot.db");
|
|
1035
|
+
}
|
|
1036
|
+
function ensureParentDir(filePath) {
|
|
1037
|
+
const dir = path.dirname(filePath);
|
|
1038
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1039
|
+
}
|
|
1040
|
+
function openDatabase() {
|
|
1041
|
+
if (cachedDb) return cachedDb;
|
|
1042
|
+
const dbPath = resolveDbPath();
|
|
1043
|
+
ensureParentDir(dbPath);
|
|
1044
|
+
const db = new Database(dbPath);
|
|
1045
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
1046
|
+
ensureSchema(db);
|
|
1047
|
+
cachedDb = db;
|
|
1048
|
+
return db;
|
|
1049
|
+
}
|
|
1050
|
+
function ensureSchema(db) {
|
|
1051
|
+
db.exec(`
|
|
1052
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
1053
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1054
|
+
title TEXT NOT NULL,
|
|
1055
|
+
content TEXT NOT NULL,
|
|
1056
|
+
status TEXT NOT NULL,
|
|
1057
|
+
comment TEXT,
|
|
1058
|
+
last_session_id TEXT,
|
|
1059
|
+
last_cwd TEXT,
|
|
1060
|
+
created_at INTEGER NOT NULL,
|
|
1061
|
+
updated_at INTEGER NOT NULL
|
|
1062
|
+
);
|
|
1063
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
1064
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1065
|
+
plan_id INTEGER NOT NULL,
|
|
1066
|
+
content TEXT NOT NULL,
|
|
1067
|
+
status TEXT NOT NULL,
|
|
1068
|
+
executor TEXT NOT NULL,
|
|
1069
|
+
sort_order INTEGER NOT NULL,
|
|
1070
|
+
comment TEXT,
|
|
1071
|
+
created_at INTEGER NOT NULL,
|
|
1072
|
+
updated_at INTEGER NOT NULL,
|
|
1073
|
+
FOREIGN KEY(plan_id) REFERENCES plans(id) ON DELETE CASCADE
|
|
1074
|
+
);
|
|
1075
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
1076
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1077
|
+
step_id INTEGER NOT NULL,
|
|
1078
|
+
content TEXT NOT NULL,
|
|
1079
|
+
status TEXT NOT NULL,
|
|
1080
|
+
comment TEXT,
|
|
1081
|
+
created_at INTEGER NOT NULL,
|
|
1082
|
+
updated_at INTEGER NOT NULL,
|
|
1083
|
+
FOREIGN KEY(step_id) REFERENCES steps(id) ON DELETE CASCADE
|
|
1084
|
+
);
|
|
1085
|
+
CREATE TABLE IF NOT EXISTS active_plan (
|
|
1086
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1087
|
+
session_id TEXT NOT NULL,
|
|
1088
|
+
plan_id INTEGER NOT NULL,
|
|
1089
|
+
updated_at INTEGER NOT NULL,
|
|
1090
|
+
UNIQUE(session_id),
|
|
1091
|
+
UNIQUE(plan_id),
|
|
1092
|
+
FOREIGN KEY(plan_id) REFERENCES plans(id) ON DELETE CASCADE
|
|
1093
|
+
);
|
|
1094
|
+
CREATE INDEX IF NOT EXISTS idx_steps_plan_order ON steps(plan_id, sort_order);
|
|
1095
|
+
CREATE INDEX IF NOT EXISTS idx_goals_step ON goals(step_id);
|
|
1096
|
+
`);
|
|
1097
|
+
const columns = db.prepare("PRAGMA table_info(plans)").all().map((row) => row.name);
|
|
1098
|
+
if (!columns.includes("last_cwd")) {
|
|
1099
|
+
db.exec("ALTER TABLE plans ADD COLUMN last_cwd TEXT");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/lib/config.ts
|
|
1104
|
+
var DEFAULT_KEYWORDS = {
|
|
1105
|
+
any: [],
|
|
1106
|
+
all: [],
|
|
1107
|
+
none: [],
|
|
1108
|
+
matchCase: false
|
|
1109
|
+
};
|
|
1110
|
+
var DEFAULT_EVENT_RULE = {
|
|
1111
|
+
enabled: false,
|
|
1112
|
+
force: false,
|
|
1113
|
+
keywords: DEFAULT_KEYWORDS
|
|
1114
|
+
};
|
|
1115
|
+
var DEFAULT_SESSION_ERROR_RULE = {
|
|
1116
|
+
enabled: false,
|
|
1117
|
+
force: true,
|
|
1118
|
+
keywords: DEFAULT_KEYWORDS,
|
|
1119
|
+
errorNames: [],
|
|
1120
|
+
statusCodes: [],
|
|
1121
|
+
retryableOnly: false
|
|
1122
|
+
};
|
|
1123
|
+
var DEFAULT_SESSION_RETRY_RULE = {
|
|
1124
|
+
enabled: false,
|
|
1125
|
+
force: false,
|
|
1126
|
+
keywords: DEFAULT_KEYWORDS,
|
|
1127
|
+
attemptAtLeast: 1
|
|
1128
|
+
};
|
|
1129
|
+
var DEFAULT_SEND_RETRY = {
|
|
1130
|
+
enabled: true,
|
|
1131
|
+
maxAttempts: 3,
|
|
1132
|
+
delaysMs: [1500, 5e3, 15e3]
|
|
1133
|
+
};
|
|
1134
|
+
var DEFAULT_PLANPILOT_CONFIG = {
|
|
1135
|
+
autoContinue: {
|
|
1136
|
+
sendRetry: DEFAULT_SEND_RETRY,
|
|
1137
|
+
onSessionError: DEFAULT_SESSION_ERROR_RULE,
|
|
1138
|
+
onSessionRetry: DEFAULT_SESSION_RETRY_RULE,
|
|
1139
|
+
onPermissionAsked: DEFAULT_EVENT_RULE,
|
|
1140
|
+
onPermissionRejected: {
|
|
1141
|
+
...DEFAULT_EVENT_RULE,
|
|
1142
|
+
force: true
|
|
1143
|
+
},
|
|
1144
|
+
onQuestionAsked: DEFAULT_EVENT_RULE,
|
|
1145
|
+
onQuestionRejected: {
|
|
1146
|
+
...DEFAULT_EVENT_RULE,
|
|
1147
|
+
force: true
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
runtime: {
|
|
1151
|
+
paused: false
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
function resolvePlanpilotConfigPath() {
|
|
1155
|
+
const override = process.env.OPENCODE_PLANPILOT_CONFIG;
|
|
1156
|
+
if (override && override.trim()) {
|
|
1157
|
+
const value = override.trim();
|
|
1158
|
+
return path2.isAbsolute(value) ? value : path2.resolve(value);
|
|
1159
|
+
}
|
|
1160
|
+
return path2.join(resolvePlanpilotDir(), "config.json");
|
|
1161
|
+
}
|
|
1162
|
+
function cloneDefaultConfig() {
|
|
1163
|
+
return {
|
|
1164
|
+
autoContinue: {
|
|
1165
|
+
sendRetry: {
|
|
1166
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.enabled,
|
|
1167
|
+
maxAttempts: DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.maxAttempts,
|
|
1168
|
+
delaysMs: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.delaysMs]
|
|
1169
|
+
},
|
|
1170
|
+
onSessionError: {
|
|
1171
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.enabled,
|
|
1172
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.force,
|
|
1173
|
+
keywords: {
|
|
1174
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.any],
|
|
1175
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.all],
|
|
1176
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.none],
|
|
1177
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.matchCase
|
|
1178
|
+
},
|
|
1179
|
+
errorNames: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.errorNames],
|
|
1180
|
+
statusCodes: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.statusCodes],
|
|
1181
|
+
retryableOnly: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.retryableOnly
|
|
1182
|
+
},
|
|
1183
|
+
onSessionRetry: {
|
|
1184
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.enabled,
|
|
1185
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.force,
|
|
1186
|
+
keywords: {
|
|
1187
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.any],
|
|
1188
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.all],
|
|
1189
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.none],
|
|
1190
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.matchCase
|
|
1191
|
+
},
|
|
1192
|
+
attemptAtLeast: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.attemptAtLeast
|
|
1193
|
+
},
|
|
1194
|
+
onPermissionAsked: {
|
|
1195
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.enabled,
|
|
1196
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.force,
|
|
1197
|
+
keywords: {
|
|
1198
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.any],
|
|
1199
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.all],
|
|
1200
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.none],
|
|
1201
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.matchCase
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
onPermissionRejected: {
|
|
1205
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.enabled,
|
|
1206
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.force,
|
|
1207
|
+
keywords: {
|
|
1208
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.any],
|
|
1209
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.all],
|
|
1210
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.none],
|
|
1211
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.matchCase
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
onQuestionAsked: {
|
|
1215
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.enabled,
|
|
1216
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.force,
|
|
1217
|
+
keywords: {
|
|
1218
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.any],
|
|
1219
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.all],
|
|
1220
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.none],
|
|
1221
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.matchCase
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
onQuestionRejected: {
|
|
1225
|
+
enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.enabled,
|
|
1226
|
+
force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.force,
|
|
1227
|
+
keywords: {
|
|
1228
|
+
any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.any],
|
|
1229
|
+
all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.all],
|
|
1230
|
+
none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.none],
|
|
1231
|
+
matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.matchCase
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
runtime: {
|
|
1236
|
+
paused: DEFAULT_PLANPILOT_CONFIG.runtime.paused
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function parseBoolean(value, fallback) {
|
|
1241
|
+
if (typeof value === "boolean") return value;
|
|
1242
|
+
return fallback;
|
|
1243
|
+
}
|
|
1244
|
+
function parseStringArray(value) {
|
|
1245
|
+
if (!Array.isArray(value)) return [];
|
|
1246
|
+
const parsed = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1247
|
+
return Array.from(new Set(parsed));
|
|
1248
|
+
}
|
|
1249
|
+
function parseNumberArray(value) {
|
|
1250
|
+
if (!Array.isArray(value)) return [];
|
|
1251
|
+
const parsed = value.map((item) => typeof item === "number" ? item : Number.NaN).filter((item) => Number.isFinite(item)).map((item) => Math.trunc(item));
|
|
1252
|
+
return Array.from(new Set(parsed));
|
|
1253
|
+
}
|
|
1254
|
+
function parsePositiveInt(value, fallback) {
|
|
1255
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
1256
|
+
const parsed = Math.trunc(value);
|
|
1257
|
+
return parsed > 0 ? parsed : fallback;
|
|
1258
|
+
}
|
|
1259
|
+
function parsePositiveNumberArray(value, fallback) {
|
|
1260
|
+
if (!Array.isArray(value)) return fallback;
|
|
1261
|
+
const parsed = value.map((item) => typeof item === "number" ? item : Number.NaN).filter((item) => Number.isFinite(item)).map((item) => Math.trunc(item)).filter((item) => item > 0);
|
|
1262
|
+
if (!parsed.length) return fallback;
|
|
1263
|
+
return Array.from(new Set(parsed));
|
|
1264
|
+
}
|
|
1265
|
+
function parseKeywordRule(value, fallback) {
|
|
1266
|
+
return {
|
|
1267
|
+
any: parseStringArray(value?.any),
|
|
1268
|
+
all: parseStringArray(value?.all),
|
|
1269
|
+
none: parseStringArray(value?.none),
|
|
1270
|
+
matchCase: parseBoolean(value?.matchCase, fallback.matchCase)
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function parseEventRule(value, fallback) {
|
|
1274
|
+
return {
|
|
1275
|
+
enabled: parseBoolean(value?.enabled, fallback.enabled),
|
|
1276
|
+
force: parseBoolean(value?.force, fallback.force),
|
|
1277
|
+
keywords: parseKeywordRule(value?.keywords, fallback.keywords)
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function parseSessionErrorRule(value, fallback) {
|
|
1281
|
+
const base = parseEventRule(value, fallback);
|
|
1282
|
+
return {
|
|
1283
|
+
...base,
|
|
1284
|
+
errorNames: parseStringArray(value?.errorNames),
|
|
1285
|
+
statusCodes: parseNumberArray(value?.statusCodes),
|
|
1286
|
+
retryableOnly: parseBoolean(value?.retryableOnly, fallback.retryableOnly)
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function parseSessionRetryRule(value, fallback) {
|
|
1290
|
+
const base = parseEventRule(value, fallback);
|
|
1291
|
+
const rawAttempt = typeof value?.attemptAtLeast === "number" ? Math.trunc(value.attemptAtLeast) : fallback.attemptAtLeast;
|
|
1292
|
+
return {
|
|
1293
|
+
...base,
|
|
1294
|
+
attemptAtLeast: rawAttempt > 0 ? rawAttempt : fallback.attemptAtLeast
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function parseSendRetryConfig(value, fallback) {
|
|
1298
|
+
return {
|
|
1299
|
+
enabled: parseBoolean(value?.enabled, fallback.enabled),
|
|
1300
|
+
maxAttempts: parsePositiveInt(value?.maxAttempts, fallback.maxAttempts),
|
|
1301
|
+
delaysMs: parsePositiveNumberArray(value?.delaysMs, fallback.delaysMs)
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
function parseConfig(raw) {
|
|
1305
|
+
return {
|
|
1306
|
+
autoContinue: {
|
|
1307
|
+
sendRetry: parseSendRetryConfig(raw.autoContinue?.sendRetry, DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry),
|
|
1308
|
+
onSessionError: parseSessionErrorRule(
|
|
1309
|
+
raw.autoContinue?.onSessionError,
|
|
1310
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError
|
|
1311
|
+
),
|
|
1312
|
+
onSessionRetry: parseSessionRetryRule(
|
|
1313
|
+
raw.autoContinue?.onSessionRetry,
|
|
1314
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry
|
|
1315
|
+
),
|
|
1316
|
+
onPermissionAsked: parseEventRule(
|
|
1317
|
+
raw.autoContinue?.onPermissionAsked,
|
|
1318
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked
|
|
1319
|
+
),
|
|
1320
|
+
onPermissionRejected: parseEventRule(
|
|
1321
|
+
raw.autoContinue?.onPermissionRejected,
|
|
1322
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected
|
|
1323
|
+
),
|
|
1324
|
+
onQuestionAsked: parseEventRule(
|
|
1325
|
+
raw.autoContinue?.onQuestionAsked,
|
|
1326
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked
|
|
1327
|
+
),
|
|
1328
|
+
onQuestionRejected: parseEventRule(
|
|
1329
|
+
raw.autoContinue?.onQuestionRejected,
|
|
1330
|
+
DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected
|
|
1331
|
+
)
|
|
1332
|
+
},
|
|
1333
|
+
runtime: {
|
|
1334
|
+
paused: parseBoolean(raw.runtime?.paused, DEFAULT_PLANPILOT_CONFIG.runtime.paused)
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function normalizePlanpilotConfig(raw) {
|
|
1339
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1340
|
+
return cloneDefaultConfig();
|
|
1341
|
+
}
|
|
1342
|
+
return parseConfig(raw);
|
|
1343
|
+
}
|
|
1344
|
+
function savePlanpilotConfig(config) {
|
|
1345
|
+
const filePath = resolvePlanpilotConfigPath();
|
|
1346
|
+
const normalized = normalizePlanpilotConfig(config);
|
|
1347
|
+
const parentDir = path2.dirname(filePath);
|
|
1348
|
+
fs2.mkdirSync(parentDir, { recursive: true });
|
|
1349
|
+
fs2.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}
|
|
1350
|
+
`, "utf8");
|
|
1351
|
+
return {
|
|
1352
|
+
path: filePath,
|
|
1353
|
+
loadedFromFile: true,
|
|
1354
|
+
config: normalized
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function loadPlanpilotConfig() {
|
|
1358
|
+
const filePath = resolvePlanpilotConfigPath();
|
|
1359
|
+
try {
|
|
1360
|
+
if (!fs2.existsSync(filePath)) {
|
|
1361
|
+
return {
|
|
1362
|
+
path: filePath,
|
|
1363
|
+
loadedFromFile: false,
|
|
1364
|
+
config: cloneDefaultConfig()
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const text = fs2.readFileSync(filePath, "utf8");
|
|
1368
|
+
const parsed = JSON.parse(text);
|
|
1369
|
+
return {
|
|
1370
|
+
path: filePath,
|
|
1371
|
+
loadedFromFile: true,
|
|
1372
|
+
config: normalizePlanpilotConfig(parsed)
|
|
1373
|
+
};
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
const loadError = error instanceof Error ? error.message : String(error);
|
|
1376
|
+
return {
|
|
1377
|
+
path: filePath,
|
|
1378
|
+
loadedFromFile: false,
|
|
1379
|
+
config: cloneDefaultConfig(),
|
|
1380
|
+
loadError
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/studio/bridge.ts
|
|
1386
|
+
var DEFAULT_SESSION_ID = "studio";
|
|
1387
|
+
var PLAN_UPDATE_ALLOWED = /* @__PURE__ */ new Set(["title", "content", "status", "comment"]);
|
|
1388
|
+
var STEP_UPDATE_ALLOWED = /* @__PURE__ */ new Set(["content", "status", "executor", "comment"]);
|
|
1389
|
+
var GOAL_UPDATE_ALLOWED = /* @__PURE__ */ new Set(["content", "status", "comment"]);
|
|
1390
|
+
async function main() {
|
|
1391
|
+
const response = await runBridge();
|
|
1392
|
+
process.stdout.write(JSON.stringify(response));
|
|
1393
|
+
}
|
|
1394
|
+
async function runBridge() {
|
|
1395
|
+
try {
|
|
1396
|
+
const raw = await readStdinOnce();
|
|
1397
|
+
const request = parseRequest(raw);
|
|
1398
|
+
const context = resolveRequestContext(request);
|
|
1399
|
+
const data = dispatch(request.action, request.payload, context);
|
|
1400
|
+
return ok(data);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
return fail(error);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function parseRequest(raw) {
|
|
1406
|
+
if (!raw.trim()) {
|
|
1407
|
+
throw invalidInput("bridge request is empty");
|
|
1408
|
+
}
|
|
1409
|
+
let parsed;
|
|
1410
|
+
try {
|
|
1411
|
+
parsed = JSON.parse(raw);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1414
|
+
throw invalidInput(`bridge request is not valid JSON: ${message}`);
|
|
1415
|
+
}
|
|
1416
|
+
const root = asObject(parsed, "bridge request");
|
|
1417
|
+
const action = expectString(root.action, "action");
|
|
1418
|
+
return {
|
|
1419
|
+
action,
|
|
1420
|
+
payload: root.payload,
|
|
1421
|
+
context: root.context,
|
|
1422
|
+
plugin: root.plugin
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function resolveRequestContext(request) {
|
|
1426
|
+
const context = asObjectOptional(request.context);
|
|
1427
|
+
const plugin = asObjectOptional(request.plugin);
|
|
1428
|
+
const sessionId = readNonEmptyString(context?.sessionId) ?? readNonEmptyString(context?.sessionID) ?? DEFAULT_SESSION_ID;
|
|
1429
|
+
const cwd = readNonEmptyString(context?.cwd) ?? readNonEmptyString(context?.directory) ?? readNonEmptyString(plugin?.rootPath) ?? void 0;
|
|
1430
|
+
return { sessionId, cwd };
|
|
1431
|
+
}
|
|
1432
|
+
function createApp(context) {
|
|
1433
|
+
return new PlanpilotApp(openDatabase(), context.sessionId, context.cwd);
|
|
1434
|
+
}
|
|
1435
|
+
function dispatch(action, payload, context) {
|
|
1436
|
+
const handler = ACTIONS[action];
|
|
1437
|
+
if (!handler) {
|
|
1438
|
+
throw invalidInput(`unknown action: ${action}`);
|
|
1439
|
+
}
|
|
1440
|
+
return handler(payload, context);
|
|
1441
|
+
}
|
|
1442
|
+
var ACTIONS = {
|
|
1443
|
+
"runtime.snapshot": actionRuntimeSnapshot,
|
|
1444
|
+
"runtime.next": actionRuntimeNext,
|
|
1445
|
+
"runtime.pause": actionRuntimePause,
|
|
1446
|
+
"runtime.resume": actionRuntimeResume,
|
|
1447
|
+
"events.poll": actionEventsPoll,
|
|
1448
|
+
"config.get": actionConfigGet,
|
|
1449
|
+
"config.set": actionConfigSet,
|
|
1450
|
+
"plan.list": actionPlanList,
|
|
1451
|
+
"plan.get": actionPlanGet,
|
|
1452
|
+
"plan.createTree": actionPlanAddTree,
|
|
1453
|
+
"plan.addTree": actionPlanAddTree,
|
|
1454
|
+
"plan.update": actionPlanUpdate,
|
|
1455
|
+
"plan.done": actionPlanDone,
|
|
1456
|
+
"plan.remove": actionPlanRemove,
|
|
1457
|
+
"plan.activate": actionPlanActivate,
|
|
1458
|
+
"plan.deactivate": actionPlanDeactivate,
|
|
1459
|
+
"plan.active": actionPlanActive,
|
|
1460
|
+
"step.list": actionStepList,
|
|
1461
|
+
"step.get": actionStepGet,
|
|
1462
|
+
"step.add": actionStepAdd,
|
|
1463
|
+
"step.addTree": actionStepAddTree,
|
|
1464
|
+
"step.update": actionStepUpdate,
|
|
1465
|
+
"step.done": actionStepDone,
|
|
1466
|
+
"step.remove": actionStepRemove,
|
|
1467
|
+
"step.move": actionStepMove,
|
|
1468
|
+
"step.wait": actionStepWait,
|
|
1469
|
+
"goal.list": actionGoalList,
|
|
1470
|
+
"goal.get": actionGoalGet,
|
|
1471
|
+
"goal.add": actionGoalAdd,
|
|
1472
|
+
"goal.update": actionGoalUpdate,
|
|
1473
|
+
"goal.done": actionGoalDone,
|
|
1474
|
+
"goal.remove": actionGoalRemove
|
|
1475
|
+
};
|
|
1476
|
+
function actionRuntimeSnapshot(_payload, context) {
|
|
1477
|
+
const app = createApp(context);
|
|
1478
|
+
return buildRuntimeSnapshot(app, loadPlanpilotConfig().config);
|
|
1479
|
+
}
|
|
1480
|
+
function actionRuntimeNext(_payload, context) {
|
|
1481
|
+
const app = createApp(context);
|
|
1482
|
+
const active = app.getActivePlan();
|
|
1483
|
+
if (!active) {
|
|
1484
|
+
return {
|
|
1485
|
+
activePlan: null,
|
|
1486
|
+
nextStep: null,
|
|
1487
|
+
cursor: buildRuntimeCursor(app, loadPlanpilotConfig().config)
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const nextStep = app.nextStep(active.plan_id);
|
|
1491
|
+
return {
|
|
1492
|
+
activePlan: active,
|
|
1493
|
+
nextStep: nextStep ? serializeStepDetail(app.getStepDetail(nextStep.id)) : null,
|
|
1494
|
+
cursor: buildRuntimeCursor(app, loadPlanpilotConfig().config)
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function actionRuntimePause(_payload, context) {
|
|
1498
|
+
const loaded = loadPlanpilotConfig();
|
|
1499
|
+
const config = normalizePlanpilotConfig(loaded.config);
|
|
1500
|
+
config.runtime.paused = true;
|
|
1501
|
+
savePlanpilotConfig(config);
|
|
1502
|
+
const app = createApp(context);
|
|
1503
|
+
return buildRuntimeSnapshot(app, config);
|
|
1504
|
+
}
|
|
1505
|
+
function actionRuntimeResume(_payload, context) {
|
|
1506
|
+
const loaded = loadPlanpilotConfig();
|
|
1507
|
+
const config = normalizePlanpilotConfig(loaded.config);
|
|
1508
|
+
config.runtime.paused = false;
|
|
1509
|
+
savePlanpilotConfig(config);
|
|
1510
|
+
const app = createApp(context);
|
|
1511
|
+
return buildRuntimeSnapshot(app, config);
|
|
1512
|
+
}
|
|
1513
|
+
function actionEventsPoll(payload, context) {
|
|
1514
|
+
const app = createApp(context);
|
|
1515
|
+
const config = loadPlanpilotConfig().config;
|
|
1516
|
+
const currentCursor = buildRuntimeCursor(app, config);
|
|
1517
|
+
const input = asObjectOptional(payload);
|
|
1518
|
+
const previousCursor = readNonEmptyString(input?.cursor) ?? "";
|
|
1519
|
+
if (previousCursor === currentCursor) {
|
|
1520
|
+
return {
|
|
1521
|
+
cursor: currentCursor,
|
|
1522
|
+
events: []
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
return {
|
|
1526
|
+
cursor: currentCursor,
|
|
1527
|
+
events: [
|
|
1528
|
+
{
|
|
1529
|
+
event: "planpilot.runtime.changed",
|
|
1530
|
+
id: currentCursor,
|
|
1531
|
+
data: buildRuntimeSnapshot(app, config)
|
|
1532
|
+
}
|
|
1533
|
+
]
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function actionConfigGet(_payload, _context) {
|
|
1537
|
+
const loaded = loadPlanpilotConfig();
|
|
1538
|
+
return loaded.config;
|
|
1539
|
+
}
|
|
1540
|
+
function actionConfigSet(payload, _context) {
|
|
1541
|
+
const root = asObject(payload, "config.set payload");
|
|
1542
|
+
const raw = "config" in root ? root.config : payload;
|
|
1543
|
+
const normalized = normalizePlanpilotConfig(raw);
|
|
1544
|
+
const saved = savePlanpilotConfig(normalized);
|
|
1545
|
+
return {
|
|
1546
|
+
path: saved.path,
|
|
1547
|
+
config: saved.config
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function actionPlanList(payload, context) {
|
|
1551
|
+
const app = createApp(context);
|
|
1552
|
+
const input = asObjectOptional(payload);
|
|
1553
|
+
const order = parsePlanOrderOptional(input?.order);
|
|
1554
|
+
const desc = readBoolean(input?.desc) ?? true;
|
|
1555
|
+
const plans = app.listPlans(order, desc);
|
|
1556
|
+
return plans;
|
|
1557
|
+
}
|
|
1558
|
+
function actionPlanGet(payload, context) {
|
|
1559
|
+
const app = createApp(context);
|
|
1560
|
+
const input = asObject(payload, "plan.get payload");
|
|
1561
|
+
const id = expectInt(input.id, "id");
|
|
1562
|
+
return serializePlanDetail(app.getPlanDetail(id));
|
|
1563
|
+
}
|
|
1564
|
+
function actionPlanAddTree(payload, context) {
|
|
1565
|
+
const app = createApp(context);
|
|
1566
|
+
const input = asObject(payload, "plan.createTree payload");
|
|
1567
|
+
const title = expectString(input.title, "title");
|
|
1568
|
+
const content = expectString(input.content, "content");
|
|
1569
|
+
const stepsInput = expectArray(input.steps, "steps");
|
|
1570
|
+
const steps = stepsInput.map((item, index) => {
|
|
1571
|
+
const step = asObject(item, `steps[${index}]`);
|
|
1572
|
+
return {
|
|
1573
|
+
content: expectString(step.content, `steps[${index}].content`),
|
|
1574
|
+
executor: parseExecutorOptional(step.executor) ?? "ai",
|
|
1575
|
+
goals: readStringArray(step.goals)
|
|
1576
|
+
};
|
|
1577
|
+
});
|
|
1578
|
+
const result = app.addPlanTree({ title, content }, steps);
|
|
1579
|
+
return {
|
|
1580
|
+
plan: result.plan,
|
|
1581
|
+
stepCount: result.stepCount,
|
|
1582
|
+
goalCount: result.goalCount,
|
|
1583
|
+
detail: serializePlanDetail(app.getPlanDetail(result.plan.id))
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function actionPlanUpdate(payload, context) {
|
|
1587
|
+
const app = createApp(context);
|
|
1588
|
+
const input = asObject(payload, "plan.update payload");
|
|
1589
|
+
const id = expectInt(input.id, "id");
|
|
1590
|
+
assertAllowedKeys(input, PLAN_UPDATE_ALLOWED, "plan.update");
|
|
1591
|
+
const result = app.updatePlanWithActiveClear(id, {
|
|
1592
|
+
title: readString(input.title),
|
|
1593
|
+
content: readString(input.content),
|
|
1594
|
+
status: parsePlanStatusOptional(input.status),
|
|
1595
|
+
comment: readNullableString(input.comment)
|
|
1596
|
+
});
|
|
1597
|
+
return {
|
|
1598
|
+
plan: result.plan,
|
|
1599
|
+
cleared: result.cleared
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function actionPlanDone(payload, context) {
|
|
1603
|
+
const app = createApp(context);
|
|
1604
|
+
const input = asObject(payload, "plan.done payload");
|
|
1605
|
+
const id = expectInt(input.id, "id");
|
|
1606
|
+
return app.updatePlanWithActiveClear(id, { status: "done" });
|
|
1607
|
+
}
|
|
1608
|
+
function actionPlanRemove(payload, context) {
|
|
1609
|
+
const app = createApp(context);
|
|
1610
|
+
const input = asObject(payload, "plan.remove payload");
|
|
1611
|
+
const id = expectInt(input.id, "id");
|
|
1612
|
+
app.deletePlan(id);
|
|
1613
|
+
return { removed: id };
|
|
1614
|
+
}
|
|
1615
|
+
function actionPlanActivate(payload, context) {
|
|
1616
|
+
const app = createApp(context);
|
|
1617
|
+
const input = asObject(payload, "plan.activate payload");
|
|
1618
|
+
const id = expectInt(input.id, "id");
|
|
1619
|
+
const force = readBoolean(input.force) ?? false;
|
|
1620
|
+
const plan = app.getPlan(id);
|
|
1621
|
+
if (plan.status === "done") {
|
|
1622
|
+
throw invalidInput("cannot activate plan; plan is done");
|
|
1623
|
+
}
|
|
1624
|
+
return app.setActivePlan(id, force);
|
|
1625
|
+
}
|
|
1626
|
+
function actionPlanDeactivate(_payload, context) {
|
|
1627
|
+
const app = createApp(context);
|
|
1628
|
+
const active = app.getActivePlan();
|
|
1629
|
+
app.clearActivePlan();
|
|
1630
|
+
return {
|
|
1631
|
+
activePlan: active,
|
|
1632
|
+
deactivated: true
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function actionPlanActive(_payload, context) {
|
|
1636
|
+
const app = createApp(context);
|
|
1637
|
+
const active = app.getActivePlan();
|
|
1638
|
+
if (!active) {
|
|
1639
|
+
return { activePlan: null, detail: null };
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
activePlan: active,
|
|
1643
|
+
detail: serializePlanDetail(app.getPlanDetail(active.plan_id))
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function actionStepList(payload, context) {
|
|
1647
|
+
const app = createApp(context);
|
|
1648
|
+
const input = asObject(payload, "step.list payload");
|
|
1649
|
+
const planId = expectInt(input.planId, "planId");
|
|
1650
|
+
const query = {
|
|
1651
|
+
status: parseStepStatusOptional(input.status),
|
|
1652
|
+
executor: parseExecutorOptional(input.executor),
|
|
1653
|
+
limit: parseIntOptional(input.limit),
|
|
1654
|
+
offset: parseIntOptional(input.offset),
|
|
1655
|
+
order: parseStepOrderOptional(input.order),
|
|
1656
|
+
desc: readBoolean(input.desc)
|
|
1657
|
+
};
|
|
1658
|
+
const steps = app.listStepsFiltered(planId, query);
|
|
1659
|
+
return steps;
|
|
1660
|
+
}
|
|
1661
|
+
function actionStepGet(payload, context) {
|
|
1662
|
+
const app = createApp(context);
|
|
1663
|
+
const input = asObject(payload, "step.get payload");
|
|
1664
|
+
const id = expectInt(input.id, "id");
|
|
1665
|
+
return serializeStepDetail(app.getStepDetail(id));
|
|
1666
|
+
}
|
|
1667
|
+
function actionStepAdd(payload, context) {
|
|
1668
|
+
const app = createApp(context);
|
|
1669
|
+
const input = asObject(payload, "step.add payload");
|
|
1670
|
+
const planId = expectInt(input.planId, "planId");
|
|
1671
|
+
const contents = resolveContents(input, "content", "contents");
|
|
1672
|
+
const executor = parseExecutorOptional(input.executor) ?? "ai";
|
|
1673
|
+
const at = parseIntOptional(input.at);
|
|
1674
|
+
const result = app.addStepsBatch(planId, contents, "todo", executor, at);
|
|
1675
|
+
return {
|
|
1676
|
+
steps: result.steps,
|
|
1677
|
+
changes: result.changes
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function actionStepAddTree(payload, context) {
|
|
1681
|
+
const app = createApp(context);
|
|
1682
|
+
const input = asObject(payload, "step.addTree payload");
|
|
1683
|
+
const planId = expectInt(input.planId, "planId");
|
|
1684
|
+
const content = expectString(input.content, "content");
|
|
1685
|
+
const executor = parseExecutorOptional(input.executor) ?? "ai";
|
|
1686
|
+
const goals = readStringArray(input.goals);
|
|
1687
|
+
return app.addStepTree(planId, content, executor, goals);
|
|
1688
|
+
}
|
|
1689
|
+
function actionStepUpdate(payload, context) {
|
|
1690
|
+
const app = createApp(context);
|
|
1691
|
+
const input = asObject(payload, "step.update payload");
|
|
1692
|
+
const id = expectInt(input.id, "id");
|
|
1693
|
+
assertAllowedKeys(input, STEP_UPDATE_ALLOWED, "step.update");
|
|
1694
|
+
return app.updateStep(id, {
|
|
1695
|
+
content: readString(input.content),
|
|
1696
|
+
status: parseStepStatusOptional(input.status),
|
|
1697
|
+
executor: parseExecutorOptional(input.executor),
|
|
1698
|
+
comment: readNullableString(input.comment)
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function actionStepDone(payload, context) {
|
|
1702
|
+
const app = createApp(context);
|
|
1703
|
+
const input = asObject(payload, "step.done payload");
|
|
1704
|
+
const id = expectInt(input.id, "id");
|
|
1705
|
+
const allGoals = readBoolean(input.allGoals) ?? false;
|
|
1706
|
+
return app.setStepDoneWithGoals(id, allGoals);
|
|
1707
|
+
}
|
|
1708
|
+
function actionStepRemove(payload, context) {
|
|
1709
|
+
const app = createApp(context);
|
|
1710
|
+
const input = asObject(payload, "step.remove payload");
|
|
1711
|
+
const ids = resolveIds(input);
|
|
1712
|
+
return app.deleteSteps(ids);
|
|
1713
|
+
}
|
|
1714
|
+
function actionStepMove(payload, context) {
|
|
1715
|
+
const app = createApp(context);
|
|
1716
|
+
const input = asObject(payload, "step.move payload");
|
|
1717
|
+
const id = expectInt(input.id, "id");
|
|
1718
|
+
const to = expectInt(input.to, "to");
|
|
1719
|
+
return app.moveStep(id, to);
|
|
1720
|
+
}
|
|
1721
|
+
function actionStepWait(payload, context) {
|
|
1722
|
+
const app = createApp(context);
|
|
1723
|
+
const input = asObject(payload, "step.wait payload");
|
|
1724
|
+
const id = expectInt(input.id, "id");
|
|
1725
|
+
const clear = readBoolean(input.clear) ?? false;
|
|
1726
|
+
if (clear) {
|
|
1727
|
+
return app.clearStepWait(id);
|
|
1728
|
+
}
|
|
1729
|
+
const delayMs = expectInt(input.delayMs, "delayMs");
|
|
1730
|
+
const reason = readString(input.reason);
|
|
1731
|
+
return app.setStepWait(id, delayMs, reason);
|
|
1732
|
+
}
|
|
1733
|
+
function actionGoalList(payload, context) {
|
|
1734
|
+
const app = createApp(context);
|
|
1735
|
+
const input = asObject(payload, "goal.list payload");
|
|
1736
|
+
const stepId = expectInt(input.stepId, "stepId");
|
|
1737
|
+
const query = {
|
|
1738
|
+
status: parseGoalStatusOptional(input.status),
|
|
1739
|
+
limit: parseIntOptional(input.limit),
|
|
1740
|
+
offset: parseIntOptional(input.offset)
|
|
1741
|
+
};
|
|
1742
|
+
return app.listGoalsFiltered(stepId, query);
|
|
1743
|
+
}
|
|
1744
|
+
function actionGoalGet(payload, context) {
|
|
1745
|
+
const app = createApp(context);
|
|
1746
|
+
const input = asObject(payload, "goal.get payload");
|
|
1747
|
+
const id = expectInt(input.id, "id");
|
|
1748
|
+
return app.getGoalDetail(id);
|
|
1749
|
+
}
|
|
1750
|
+
function actionGoalAdd(payload, context) {
|
|
1751
|
+
const app = createApp(context);
|
|
1752
|
+
const input = asObject(payload, "goal.add payload");
|
|
1753
|
+
const stepId = expectInt(input.stepId, "stepId");
|
|
1754
|
+
const contents = resolveContents(input, "content", "contents");
|
|
1755
|
+
return app.addGoalsBatch(stepId, contents, "todo");
|
|
1756
|
+
}
|
|
1757
|
+
function actionGoalUpdate(payload, context) {
|
|
1758
|
+
const app = createApp(context);
|
|
1759
|
+
const input = asObject(payload, "goal.update payload");
|
|
1760
|
+
const id = expectInt(input.id, "id");
|
|
1761
|
+
assertAllowedKeys(input, GOAL_UPDATE_ALLOWED, "goal.update");
|
|
1762
|
+
return app.updateGoal(id, {
|
|
1763
|
+
content: readString(input.content),
|
|
1764
|
+
status: parseGoalStatusOptional(input.status),
|
|
1765
|
+
comment: readNullableString(input.comment)
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
function actionGoalDone(payload, context) {
|
|
1769
|
+
const app = createApp(context);
|
|
1770
|
+
const input = asObject(payload, "goal.done payload");
|
|
1771
|
+
const ids = resolveIds(input);
|
|
1772
|
+
if (ids.length === 1) {
|
|
1773
|
+
return app.setGoalStatus(ids[0], "done");
|
|
1774
|
+
}
|
|
1775
|
+
return app.setGoalsStatus(ids, "done");
|
|
1776
|
+
}
|
|
1777
|
+
function actionGoalRemove(payload, context) {
|
|
1778
|
+
const app = createApp(context);
|
|
1779
|
+
const input = asObject(payload, "goal.remove payload");
|
|
1780
|
+
const ids = resolveIds(input);
|
|
1781
|
+
return app.deleteGoals(ids);
|
|
1782
|
+
}
|
|
1783
|
+
function resolveContents(input, contentKey, contentsKey) {
|
|
1784
|
+
const content = readString(input[contentKey]);
|
|
1785
|
+
const contents = readStringArray(input[contentsKey]);
|
|
1786
|
+
const out = content ? [content, ...contents] : contents;
|
|
1787
|
+
if (!out.length) {
|
|
1788
|
+
throw invalidInput(`expected ${contentKey} or ${contentsKey}`);
|
|
1789
|
+
}
|
|
1790
|
+
return out;
|
|
1791
|
+
}
|
|
1792
|
+
function resolveIds(input) {
|
|
1793
|
+
const fromSingle = parseIntOptional(input.id);
|
|
1794
|
+
const fromMany = readIntArray(input.ids);
|
|
1795
|
+
const ids = fromSingle !== void 0 ? [fromSingle, ...fromMany] : fromMany;
|
|
1796
|
+
if (!ids.length) {
|
|
1797
|
+
throw invalidInput("expected id or ids");
|
|
1798
|
+
}
|
|
1799
|
+
return Array.from(new Set(ids));
|
|
1800
|
+
}
|
|
1801
|
+
function parsePlanOrderOptional(value) {
|
|
1802
|
+
const raw = readString(value);
|
|
1803
|
+
if (!raw) return void 0;
|
|
1804
|
+
if (raw === "id" || raw === "title" || raw === "created" || raw === "updated") return raw;
|
|
1805
|
+
throw invalidInput(`invalid plan order '${raw}', expected id|title|created|updated`);
|
|
1806
|
+
}
|
|
1807
|
+
function parseStepOrderOptional(value) {
|
|
1808
|
+
const raw = readString(value);
|
|
1809
|
+
if (!raw) return void 0;
|
|
1810
|
+
if (raw === "order" || raw === "id" || raw === "created" || raw === "updated") return raw;
|
|
1811
|
+
throw invalidInput(`invalid step order '${raw}', expected order|id|created|updated`);
|
|
1812
|
+
}
|
|
1813
|
+
function parsePlanStatusOptional(value) {
|
|
1814
|
+
const raw = readString(value);
|
|
1815
|
+
if (!raw) return void 0;
|
|
1816
|
+
if (raw === "todo" || raw === "done") return raw;
|
|
1817
|
+
throw invalidInput(`invalid plan status '${raw}', expected todo|done`);
|
|
1818
|
+
}
|
|
1819
|
+
function parseStepStatusOptional(value) {
|
|
1820
|
+
const raw = parsePlanStatusOptional(value);
|
|
1821
|
+
return raw;
|
|
1822
|
+
}
|
|
1823
|
+
function parseGoalStatusOptional(value) {
|
|
1824
|
+
const raw = parsePlanStatusOptional(value);
|
|
1825
|
+
return raw;
|
|
1826
|
+
}
|
|
1827
|
+
function parseExecutorOptional(value) {
|
|
1828
|
+
const raw = readString(value);
|
|
1829
|
+
if (!raw) return void 0;
|
|
1830
|
+
if (raw === "ai" || raw === "human") return raw;
|
|
1831
|
+
throw invalidInput(`invalid executor '${raw}', expected ai|human`);
|
|
1832
|
+
}
|
|
1833
|
+
function assertAllowedKeys(input, allowed, action) {
|
|
1834
|
+
for (const key of Object.keys(input)) {
|
|
1835
|
+
if (key === "id") continue;
|
|
1836
|
+
if (!allowed.has(key)) {
|
|
1837
|
+
throw invalidInput(`${action} does not support '${key}'`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function buildRuntimeSnapshot(app, config) {
|
|
1842
|
+
const active = app.getActivePlan();
|
|
1843
|
+
const next = active ? app.nextStep(active.plan_id) : null;
|
|
1844
|
+
return {
|
|
1845
|
+
paused: config.runtime.paused,
|
|
1846
|
+
activePlan: active,
|
|
1847
|
+
nextStep: next ? serializeStepDetail(app.getStepDetail(next.id)) : null,
|
|
1848
|
+
cursor: buildRuntimeCursor(app, config)
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function buildRuntimeCursor(app, config) {
|
|
1852
|
+
const plans = app.listPlans("updated", true);
|
|
1853
|
+
const latestPlanUpdated = plans.length ? plans[0].updated_at : 0;
|
|
1854
|
+
const active = app.getActivePlan();
|
|
1855
|
+
const activeUpdated = active?.updated_at ?? 0;
|
|
1856
|
+
const activePlanId = active?.plan_id ?? 0;
|
|
1857
|
+
const next = active ? app.nextStep(active.plan_id) : null;
|
|
1858
|
+
const nextStepId = next?.id ?? 0;
|
|
1859
|
+
const paused = config.runtime.paused ? 1 : 0;
|
|
1860
|
+
return [paused, latestPlanUpdated, activeUpdated, activePlanId, nextStepId].join(":");
|
|
1861
|
+
}
|
|
1862
|
+
function serializePlanDetail(detail) {
|
|
1863
|
+
const goals = detail.steps.map((step) => ({
|
|
1864
|
+
stepId: step.id,
|
|
1865
|
+
goals: detail.goals.get(step.id) ?? []
|
|
1866
|
+
}));
|
|
1867
|
+
return {
|
|
1868
|
+
plan: detail.plan,
|
|
1869
|
+
steps: detail.steps,
|
|
1870
|
+
goals
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function serializeStepDetail(detail) {
|
|
1874
|
+
const wait = parseWaitFromComment(detail.step.comment);
|
|
1875
|
+
return {
|
|
1876
|
+
step: detail.step,
|
|
1877
|
+
goals: detail.goals,
|
|
1878
|
+
wait
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function ok(data) {
|
|
1882
|
+
return { ok: true, data };
|
|
1883
|
+
}
|
|
1884
|
+
function fail(error) {
|
|
1885
|
+
if (error instanceof AppError) {
|
|
1886
|
+
const code = error.kind === "InvalidInput" ? "invalid_input" : error.kind === "NotFound" ? "not_found" : error.kind === "Db" ? "db_error" : error.kind === "Io" ? "io_error" : "json_error";
|
|
1887
|
+
return {
|
|
1888
|
+
ok: false,
|
|
1889
|
+
error: {
|
|
1890
|
+
code,
|
|
1891
|
+
message: error.toDisplayString(),
|
|
1892
|
+
details: null
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1897
|
+
return {
|
|
1898
|
+
ok: false,
|
|
1899
|
+
error: {
|
|
1900
|
+
code: "internal_error",
|
|
1901
|
+
message,
|
|
1902
|
+
details: null
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function asObject(value, label) {
|
|
1907
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1908
|
+
throw invalidInput(`${label} must be an object`);
|
|
1909
|
+
}
|
|
1910
|
+
return value;
|
|
1911
|
+
}
|
|
1912
|
+
function asObjectOptional(value) {
|
|
1913
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1914
|
+
return void 0;
|
|
1915
|
+
}
|
|
1916
|
+
return value;
|
|
1917
|
+
}
|
|
1918
|
+
function expectString(value, label) {
|
|
1919
|
+
const parsed = readString(value);
|
|
1920
|
+
if (!parsed) {
|
|
1921
|
+
throw invalidInput(`${label} is required`);
|
|
1922
|
+
}
|
|
1923
|
+
return parsed;
|
|
1924
|
+
}
|
|
1925
|
+
function readString(value) {
|
|
1926
|
+
if (typeof value !== "string") return void 0;
|
|
1927
|
+
const trimmed = value.trim();
|
|
1928
|
+
return trimmed.length ? trimmed : void 0;
|
|
1929
|
+
}
|
|
1930
|
+
function readNullableString(value) {
|
|
1931
|
+
if (value === null || value === void 0) return void 0;
|
|
1932
|
+
return readString(value);
|
|
1933
|
+
}
|
|
1934
|
+
function readNonEmptyString(value) {
|
|
1935
|
+
return readString(value);
|
|
1936
|
+
}
|
|
1937
|
+
function readBoolean(value) {
|
|
1938
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1939
|
+
}
|
|
1940
|
+
function expectInt(value, label) {
|
|
1941
|
+
const parsed = parseIntOptional(value);
|
|
1942
|
+
if (parsed === void 0) {
|
|
1943
|
+
throw invalidInput(`${label} must be an integer`);
|
|
1944
|
+
}
|
|
1945
|
+
return parsed;
|
|
1946
|
+
}
|
|
1947
|
+
function parseIntOptional(value) {
|
|
1948
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1949
|
+
return void 0;
|
|
1950
|
+
}
|
|
1951
|
+
return value;
|
|
1952
|
+
}
|
|
1953
|
+
function readStringArray(value) {
|
|
1954
|
+
if (!Array.isArray(value)) return [];
|
|
1955
|
+
const out = [];
|
|
1956
|
+
for (const item of value) {
|
|
1957
|
+
const text = readString(item);
|
|
1958
|
+
if (text) out.push(text);
|
|
1959
|
+
}
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1962
|
+
function readIntArray(value) {
|
|
1963
|
+
if (!Array.isArray(value)) return [];
|
|
1964
|
+
const out = [];
|
|
1965
|
+
for (const item of value) {
|
|
1966
|
+
const id = parseIntOptional(item);
|
|
1967
|
+
if (id !== void 0) out.push(id);
|
|
1968
|
+
}
|
|
1969
|
+
return out;
|
|
1970
|
+
}
|
|
1971
|
+
function expectArray(value, label) {
|
|
1972
|
+
if (!Array.isArray(value)) {
|
|
1973
|
+
throw invalidInput(`${label} must be an array`);
|
|
1974
|
+
}
|
|
1975
|
+
return value;
|
|
1976
|
+
}
|
|
1977
|
+
async function readStdinOnce() {
|
|
1978
|
+
const chunks = [];
|
|
1979
|
+
for await (const chunk of process.stdin) {
|
|
1980
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1981
|
+
}
|
|
1982
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1983
|
+
}
|
|
1984
|
+
void main();
|
|
1985
|
+
//# sourceMappingURL=studio-bridge.js.map
|