pi-quests 0.3.0 → 0.5.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/CHANGELOG.md +16 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +43 -11
- package/src/commands/parse-args.ts +51 -22
- package/src/config.ts +148 -0
- package/src/index.ts +55 -23
- package/src/prompts/skill.md +60 -0
- package/src/prompts.ts +16 -2
- package/src/quest/dataplane.ts +473 -104
- package/src/quest/formatters.ts +119 -16
- package/src/quest/tracker.ts +99 -39
- package/src/quest/types.ts +15 -2
- package/src/renderers/commands.ts +46 -24
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +80 -31
- package/src/tools/handler.ts +59 -16
- package/src/tools/params.ts +44 -30
package/src/quest/dataplane.ts
CHANGED
|
@@ -1,38 +1,83 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { DEFAULT_CONFIG, type ResolvedConfig } from "../config.js";
|
|
2
3
|
import { logger } from "../logger.js";
|
|
4
|
+
import { getQuestSkillDocument } from "../prompts.js";
|
|
3
5
|
import {
|
|
6
|
+
formatAddResult,
|
|
4
7
|
formatBatchAddResult,
|
|
8
|
+
formatBlockedBySteps,
|
|
5
9
|
formatDeleteResult,
|
|
10
|
+
formatDescriptionRequiredError,
|
|
11
|
+
formatEmptyDescriptionsError,
|
|
12
|
+
formatIdRequiredError,
|
|
13
|
+
formatMissingDescriptionsError,
|
|
6
14
|
formatNotFound,
|
|
15
|
+
formatNothingToRevertError,
|
|
16
|
+
formatParentDoneError,
|
|
17
|
+
formatParentNotFoundError,
|
|
7
18
|
formatQuestList,
|
|
19
|
+
formatReorderedQuestNotFoundError,
|
|
20
|
+
formatReorderNotFoundError,
|
|
21
|
+
formatReparentDemoteHasStepsError,
|
|
22
|
+
formatReparentResult,
|
|
23
|
+
formatReparentSelfParentError,
|
|
24
|
+
formatReparentTargetDoneError,
|
|
25
|
+
formatReparentTargetIsStepError,
|
|
26
|
+
formatReparentTargetNotFoundError,
|
|
27
|
+
formatStepCannotHaveSteps,
|
|
28
|
+
formatTargetIdRequiredError,
|
|
8
29
|
formatToggleResult,
|
|
30
|
+
formatUnknownActionError,
|
|
9
31
|
formatUpdateResult,
|
|
10
32
|
} from "./formatters.js";
|
|
11
|
-
import { QUEST_ACTIONS, type Quest } from "./types.js";
|
|
33
|
+
import { QUEST_ACTIONS, type Quest, type Step } from "./types.js";
|
|
12
34
|
|
|
13
35
|
export type HistoryEntry =
|
|
14
|
-
| { type: typeof QUEST_ACTIONS.add; id:
|
|
15
|
-
| { type: typeof QUEST_ACTIONS.toggle; id:
|
|
16
|
-
| { type: typeof QUEST_ACTIONS.update; id:
|
|
17
|
-
| {
|
|
36
|
+
| { type: typeof QUEST_ACTIONS.add; id: string; parentId?: string }
|
|
37
|
+
| { type: typeof QUEST_ACTIONS.toggle; id: string }
|
|
38
|
+
| { type: typeof QUEST_ACTIONS.update; id: string; previousDescription: string }
|
|
39
|
+
| {
|
|
40
|
+
type: typeof QUEST_ACTIONS.delete;
|
|
41
|
+
quest: Quest | Step;
|
|
42
|
+
index: number;
|
|
43
|
+
isStep?: boolean;
|
|
44
|
+
cascadeDeletedSteps?: Step[];
|
|
45
|
+
}
|
|
18
46
|
| {
|
|
19
47
|
type: typeof QUEST_ACTIONS.clear;
|
|
20
48
|
previousQuests: Quest[];
|
|
21
|
-
|
|
49
|
+
previousSteps?: Step[];
|
|
22
50
|
all: false;
|
|
23
51
|
}
|
|
24
|
-
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[];
|
|
25
|
-
| {
|
|
52
|
+
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; steps?: Step[]; all: true }
|
|
53
|
+
| {
|
|
54
|
+
type: typeof QUEST_ACTIONS.reorder;
|
|
55
|
+
quest: Quest;
|
|
56
|
+
oldIndex: number;
|
|
57
|
+
previousIds: string[];
|
|
58
|
+
targetId: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
type: typeof QUEST_ACTIONS.reparent;
|
|
62
|
+
id: string;
|
|
63
|
+
previousParentId?: string;
|
|
64
|
+
previousQuestIndex?: number;
|
|
65
|
+
};
|
|
26
66
|
|
|
27
67
|
export type QuestAction =
|
|
28
68
|
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
|
|
69
|
+
| { type: typeof QUEST_ACTIONS.split; id?: string; descriptions?: string[] }
|
|
70
|
+
| { type: typeof QUEST_ACTIONS.add_step; id?: string; descriptions?: string[] }
|
|
29
71
|
| { type: typeof QUEST_ACTIONS.list }
|
|
30
|
-
| { type: typeof QUEST_ACTIONS.toggle; id?:
|
|
31
|
-
| { type: typeof QUEST_ACTIONS.update; id?:
|
|
32
|
-
| { type: typeof QUEST_ACTIONS.delete; id?:
|
|
72
|
+
| { type: typeof QUEST_ACTIONS.toggle; id?: string }
|
|
73
|
+
| { type: typeof QUEST_ACTIONS.update; id?: string; description?: string }
|
|
74
|
+
| { type: typeof QUEST_ACTIONS.delete; id?: string }
|
|
33
75
|
| { type: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
34
|
-
| { type: typeof QUEST_ACTIONS.reorder; id?:
|
|
35
|
-
| { type: typeof QUEST_ACTIONS.revert }
|
|
76
|
+
| { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
|
|
77
|
+
| { type: typeof QUEST_ACTIONS.revert }
|
|
78
|
+
| { type: typeof QUEST_ACTIONS.reparent; id?: string; parentId?: string }
|
|
79
|
+
| { type: typeof QUEST_ACTIONS.rules }
|
|
80
|
+
| { type: typeof QUEST_ACTIONS.skill };
|
|
36
81
|
|
|
37
82
|
export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
|
|
38
83
|
|
|
@@ -44,8 +89,35 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
|
|
|
44
89
|
*/
|
|
45
90
|
export class QuestLog {
|
|
46
91
|
private quests: Quest[] = [];
|
|
47
|
-
private
|
|
92
|
+
private steps: Step[] = [];
|
|
93
|
+
private usedIds: Set<string> = new Set();
|
|
48
94
|
private history: HistoryEntry[] = [];
|
|
95
|
+
|
|
96
|
+
private readonly ID_LENGTH: number;
|
|
97
|
+
private readonly MAX_IDS: number;
|
|
98
|
+
|
|
99
|
+
constructor(config: ResolvedConfig = DEFAULT_CONFIG) {
|
|
100
|
+
this.ID_LENGTH = config.ids.length;
|
|
101
|
+
this.MAX_IDS = 16 ** this.ID_LENGTH;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private generateId(): string {
|
|
105
|
+
if (this.usedIds.size >= this.MAX_IDS) {
|
|
106
|
+
logger.debug("quests:state", "generate-id-exhausted", { usedIds: this.usedIds.size });
|
|
107
|
+
throw new Error("No available quest IDs. Clear done or all quests to free up IDs.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let id: string;
|
|
111
|
+
do {
|
|
112
|
+
id = Math.floor(Math.random() * this.MAX_IDS)
|
|
113
|
+
.toString(16)
|
|
114
|
+
.padStart(this.ID_LENGTH, "0")
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
} while (this.usedIds.has(id));
|
|
117
|
+
|
|
118
|
+
this.usedIds.add(id);
|
|
119
|
+
return id;
|
|
120
|
+
}
|
|
49
121
|
private undoHandlers: {
|
|
50
122
|
[K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
|
|
51
123
|
success: boolean;
|
|
@@ -54,51 +126,68 @@ export class QuestLog {
|
|
|
54
126
|
} = {
|
|
55
127
|
[QUEST_ACTIONS.add]: (entry) => {
|
|
56
128
|
this.quests = this.quests.filter((q) => q.id !== entry.id);
|
|
129
|
+
this.steps = this.steps.filter((q) => q.id !== entry.id);
|
|
130
|
+
this.usedIds.delete(entry.id);
|
|
57
131
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return { success: true, message: `Reverted add quest
|
|
132
|
+
logger.debug("quests:state", "revert-add", {
|
|
133
|
+
id: entry.id,
|
|
134
|
+
total: this.quests.length + this.steps.length,
|
|
135
|
+
});
|
|
136
|
+
return { success: true, message: `Reverted add quest [${entry.id}]` };
|
|
63
137
|
},
|
|
64
138
|
[QUEST_ACTIONS.toggle]: (entry) => {
|
|
65
|
-
const quest = this.
|
|
139
|
+
const quest = this.findById(entry.id);
|
|
66
140
|
|
|
67
141
|
if (quest) {
|
|
68
142
|
quest.done = !quest.done;
|
|
69
143
|
logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
|
|
70
|
-
return { success: true, message: `Reverted toggle for quest
|
|
144
|
+
return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
|
|
74
|
-
return { success: false, message:
|
|
148
|
+
return { success: false, message: formatNotFound(entry.id) };
|
|
75
149
|
},
|
|
76
150
|
[QUEST_ACTIONS.update]: (entry) => {
|
|
77
|
-
const quest = this.
|
|
151
|
+
const quest = this.findById(entry.id);
|
|
78
152
|
if (quest) {
|
|
79
153
|
quest.description = entry.previousDescription;
|
|
80
154
|
logger.debug("quests:state", "revert-update", { id: entry.id });
|
|
81
|
-
return { success: true, message: `Reverted update for quest
|
|
155
|
+
return { success: true, message: `Reverted update for quest [${entry.id}]` };
|
|
82
156
|
}
|
|
83
157
|
|
|
84
158
|
logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
|
|
85
|
-
return { success: false, message:
|
|
159
|
+
return { success: false, message: formatNotFound(entry.id) };
|
|
86
160
|
},
|
|
87
161
|
[QUEST_ACTIONS.delete]: (entry) => {
|
|
88
|
-
|
|
162
|
+
if (entry.isStep) {
|
|
163
|
+
this.steps.splice(entry.index, 0, entry.quest as Step);
|
|
164
|
+
} else {
|
|
165
|
+
this.quests.splice(entry.index, 0, entry.quest);
|
|
166
|
+
if (entry.cascadeDeletedSteps) {
|
|
167
|
+
for (const sub of entry.cascadeDeletedSteps) {
|
|
168
|
+
this.steps.push(sub);
|
|
169
|
+
this.usedIds.add(sub.id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.usedIds.add(entry.quest.id);
|
|
89
174
|
|
|
90
175
|
logger.debug("quests:state", "revert-delete", {
|
|
91
176
|
id: entry.quest.id,
|
|
92
177
|
index: entry.index,
|
|
93
|
-
total: this.quests.length,
|
|
178
|
+
total: this.quests.length + this.steps.length,
|
|
94
179
|
});
|
|
95
|
-
return { success: true, message: `Reverted delete for quest
|
|
180
|
+
return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
|
|
96
181
|
},
|
|
97
182
|
[QUEST_ACTIONS.clear]: (entry) => {
|
|
98
183
|
if ("previousQuests" in entry) {
|
|
99
|
-
const restoredCount =
|
|
184
|
+
const restoredCount =
|
|
185
|
+
entry.previousQuests.length +
|
|
186
|
+
(entry.previousSteps?.length ?? 0) -
|
|
187
|
+
(this.quests.length + this.steps.length);
|
|
100
188
|
this.quests = [...entry.previousQuests];
|
|
101
|
-
this.
|
|
189
|
+
this.steps = [...(entry.previousSteps ?? [])];
|
|
190
|
+
this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
|
|
102
191
|
|
|
103
192
|
return {
|
|
104
193
|
success: true,
|
|
@@ -107,13 +196,15 @@ export class QuestLog {
|
|
|
107
196
|
}
|
|
108
197
|
|
|
109
198
|
this.quests = [...entry.quests];
|
|
110
|
-
this.
|
|
199
|
+
this.steps = [...(entry.steps ?? [])];
|
|
200
|
+
this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
|
|
111
201
|
|
|
112
202
|
return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
|
|
113
203
|
},
|
|
114
204
|
[QUEST_ACTIONS.reorder]: (entry) => {
|
|
115
205
|
const currentIndex = this.quests.indexOf(entry.quest);
|
|
116
|
-
if (currentIndex === -1)
|
|
206
|
+
if (currentIndex === -1)
|
|
207
|
+
return { success: false, message: formatReorderedQuestNotFoundError() };
|
|
117
208
|
|
|
118
209
|
this.quests.splice(currentIndex, 1);
|
|
119
210
|
this.quests.splice(entry.oldIndex, 0, entry.quest);
|
|
@@ -121,24 +212,67 @@ export class QuestLog {
|
|
|
121
212
|
this.quests[i].id = entry.previousIds[i];
|
|
122
213
|
}
|
|
123
214
|
|
|
124
|
-
|
|
125
|
-
|
|
215
|
+
return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
|
|
216
|
+
},
|
|
217
|
+
[QUEST_ACTIONS.reparent]: (entry) => {
|
|
218
|
+
const quest = this.findById(entry.id);
|
|
219
|
+
if (!quest) return { success: false, message: formatNotFound(entry.id) };
|
|
220
|
+
if (entry.previousParentId !== undefined) {
|
|
221
|
+
this.quests = this.quests.filter((q) => q.id !== entry.id);
|
|
222
|
+
(quest as Step).parentId = entry.previousParentId;
|
|
223
|
+
if (!this.steps.some((s) => s.id === entry.id)) this.steps.push(quest as Step);
|
|
224
|
+
} else if (entry.previousQuestIndex !== undefined) {
|
|
225
|
+
this.steps = this.steps.filter((s) => s.id !== entry.id);
|
|
226
|
+
delete (quest as Partial<Step>).parentId;
|
|
227
|
+
this.quests.splice(entry.previousQuestIndex, 0, quest);
|
|
228
|
+
}
|
|
229
|
+
return { success: true, message: `Reverted reparent for quest [${entry.id}]` };
|
|
126
230
|
},
|
|
127
231
|
};
|
|
128
232
|
|
|
233
|
+
/*
|
|
234
|
+
* Returns all quests including steps, inserted in order
|
|
235
|
+
* steps are returned immediately after their parent quest
|
|
236
|
+
*/
|
|
129
237
|
getAll(): Quest[] {
|
|
238
|
+
const result: Quest[] = [];
|
|
239
|
+
for (const q of this.quests) {
|
|
240
|
+
result.push(q);
|
|
241
|
+
result.push(...this.steps.filter((step) => step.parentId === q.id));
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* Returns only top-level quests. Steps should be accessed via `getSteps()`.
|
|
248
|
+
*/
|
|
249
|
+
getQuests(): Quest[] {
|
|
130
250
|
return [...this.quests];
|
|
131
251
|
}
|
|
132
252
|
|
|
133
|
-
|
|
134
|
-
|
|
253
|
+
/*
|
|
254
|
+
* Returns steps for a given parent quest ID. Steps are not included in `getQuests()`.
|
|
255
|
+
*/
|
|
256
|
+
getSteps(parentId: string): Step[] {
|
|
257
|
+
return this.steps.filter((step) => step.parentId === parentId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getParent(step: Step): Quest | undefined {
|
|
261
|
+
return this.quests.find((q) => q.id === step.parentId);
|
|
135
262
|
}
|
|
136
263
|
|
|
137
|
-
|
|
264
|
+
private findById(id: string): Quest | Step | undefined {
|
|
265
|
+
return this.quests.find((q) => q.id === id) ?? this.steps.find((step) => step.id === id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getUsedIds(): string[] {
|
|
269
|
+
return Array.from(this.usedIds);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
add(description: string): Quest {
|
|
138
273
|
const quest: Quest = {
|
|
139
|
-
id: this.
|
|
274
|
+
id: this.generateId(),
|
|
140
275
|
description,
|
|
141
|
-
additionalContext,
|
|
142
276
|
done: false,
|
|
143
277
|
createdAt: Date.now(),
|
|
144
278
|
};
|
|
@@ -154,26 +288,77 @@ export class QuestLog {
|
|
|
154
288
|
return quest;
|
|
155
289
|
}
|
|
156
290
|
|
|
157
|
-
|
|
158
|
-
|
|
291
|
+
addStep(description: string, parentId: string): Step {
|
|
292
|
+
if (this.steps.some((step) => step.id === parentId)) {
|
|
293
|
+
throw new Error(formatStepCannotHaveSteps(parentId));
|
|
294
|
+
}
|
|
295
|
+
const parent = this.quests.find((q) => q.id === parentId);
|
|
296
|
+
if (!parent) {
|
|
297
|
+
throw new Error(formatParentNotFoundError(parentId));
|
|
298
|
+
}
|
|
299
|
+
if (parent.done) {
|
|
300
|
+
throw new Error(formatParentDoneError(parentId));
|
|
301
|
+
}
|
|
302
|
+
const step: Step = {
|
|
303
|
+
id: this.generateId(),
|
|
304
|
+
description,
|
|
305
|
+
done: false,
|
|
306
|
+
createdAt: Date.now(),
|
|
307
|
+
parentId,
|
|
308
|
+
};
|
|
309
|
+
this.steps.push(step);
|
|
310
|
+
this.history.push({ type: QUEST_ACTIONS.add, id: step.id, parentId });
|
|
311
|
+
logger.debug("quests:state", "add-step", {
|
|
312
|
+
id: step.id,
|
|
313
|
+
parentId,
|
|
314
|
+
description,
|
|
315
|
+
totalSteps: this.steps.length,
|
|
316
|
+
});
|
|
317
|
+
return step;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
split(id: string, descriptions: string[]): Step[] {
|
|
321
|
+
if (this.steps.some((step) => step.id === id)) {
|
|
322
|
+
throw new Error(formatStepCannotHaveSteps(id));
|
|
323
|
+
}
|
|
324
|
+
const parent = this.quests.find((q) => q.id === id);
|
|
325
|
+
if (!parent) throw new Error(formatNotFound(id));
|
|
326
|
+
if (parent.done) throw new Error(formatParentDoneError(id));
|
|
327
|
+
const created: Step[] = [];
|
|
328
|
+
for (const desc of descriptions) {
|
|
329
|
+
created.push(this.addStep(desc, id));
|
|
330
|
+
}
|
|
331
|
+
return created;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
toggle(id: string): Quest | Step | null | undefined {
|
|
335
|
+
const quest = this.findById(id);
|
|
159
336
|
if (!quest) {
|
|
160
337
|
logger.debug("quests:state", "toggle-not-found", { id });
|
|
161
338
|
return undefined;
|
|
162
339
|
}
|
|
163
340
|
|
|
341
|
+
if (!quest.done && !("parentId" in quest)) {
|
|
342
|
+
const steps = this.getSteps(id);
|
|
343
|
+
if (steps.some((q) => !q.done)) {
|
|
344
|
+
logger.debug("quests:state", "toggle-blocked-steps", { id });
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
164
349
|
quest.done = !quest.done;
|
|
165
350
|
this.history.push({ type: QUEST_ACTIONS.toggle, id });
|
|
166
351
|
|
|
167
352
|
logger.debug("quests:state", QUEST_ACTIONS.toggle, {
|
|
168
353
|
id,
|
|
169
354
|
done: quest.done,
|
|
170
|
-
total: this.quests.length,
|
|
355
|
+
total: this.quests.length + this.steps.length,
|
|
171
356
|
});
|
|
172
357
|
return quest;
|
|
173
358
|
}
|
|
174
359
|
|
|
175
|
-
update(id:
|
|
176
|
-
const quest = this.
|
|
360
|
+
update(id: string, description: string): Quest | Step | undefined {
|
|
361
|
+
const quest = this.findById(id);
|
|
177
362
|
if (!quest) {
|
|
178
363
|
logger.debug("quests:state", "update-not-found", { id });
|
|
179
364
|
return undefined;
|
|
@@ -185,77 +370,184 @@ export class QuestLog {
|
|
|
185
370
|
logger.debug("quests:state", QUEST_ACTIONS.update, {
|
|
186
371
|
id,
|
|
187
372
|
description,
|
|
188
|
-
total: this.quests.length,
|
|
373
|
+
total: this.quests.length + this.steps.length,
|
|
189
374
|
});
|
|
190
375
|
return quest;
|
|
191
376
|
}
|
|
192
377
|
|
|
193
|
-
delete(id:
|
|
378
|
+
delete(id: string): Quest | Step | null | undefined {
|
|
194
379
|
const index = this.quests.findIndex((q) => q.id === id);
|
|
195
|
-
if (index
|
|
196
|
-
|
|
197
|
-
|
|
380
|
+
if (index !== -1) {
|
|
381
|
+
const steps = this.getSteps(id);
|
|
382
|
+
if (steps.some((q) => !q.done)) {
|
|
383
|
+
logger.debug("quests:state", "delete-blocked-steps", { id });
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const [quest] = this.quests.splice(index, 1);
|
|
387
|
+
this.usedIds.delete(quest.id);
|
|
388
|
+
const cascadeDeletedSteps = this.steps.filter((step) => step.parentId === id);
|
|
389
|
+
this.history.push({ type: QUEST_ACTIONS.delete, quest, index, cascadeDeletedSteps });
|
|
390
|
+
this.steps = this.steps.filter((step) => {
|
|
391
|
+
if (step.parentId === id) {
|
|
392
|
+
this.usedIds.delete(step.id);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
398
|
+
id,
|
|
399
|
+
index,
|
|
400
|
+
total: this.quests.length + this.steps.length,
|
|
401
|
+
});
|
|
402
|
+
return quest;
|
|
198
403
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
404
|
+
const stepIndex = this.steps.findIndex((step) => step.id === id);
|
|
405
|
+
if (stepIndex !== -1) {
|
|
406
|
+
const [quest] = this.steps.splice(stepIndex, 1);
|
|
407
|
+
this.usedIds.delete(quest.id);
|
|
408
|
+
this.history.push({ type: QUEST_ACTIONS.delete, quest, index: stepIndex, isStep: true });
|
|
409
|
+
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
410
|
+
id,
|
|
411
|
+
index: stepIndex,
|
|
412
|
+
total: this.quests.length + this.steps.length,
|
|
413
|
+
});
|
|
414
|
+
return quest;
|
|
415
|
+
}
|
|
416
|
+
logger.debug("quests:state", "delete-not-found", { id });
|
|
417
|
+
return undefined;
|
|
205
418
|
}
|
|
206
419
|
|
|
207
420
|
clear(all = false): number {
|
|
208
421
|
if (!all) {
|
|
422
|
+
const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
|
|
209
423
|
const done = this.quests.filter((q) => q.done);
|
|
210
|
-
const
|
|
211
|
-
|
|
424
|
+
const removedSteps = this.steps.filter(
|
|
425
|
+
(step) => step.done || doneParentIds.has(step.parentId),
|
|
426
|
+
);
|
|
427
|
+
const keptSteps = this.steps.filter(
|
|
428
|
+
(step) => !step.done && !doneParentIds.has(step.parentId),
|
|
429
|
+
);
|
|
430
|
+
const previousQuests = [...this.quests];
|
|
431
|
+
const previousSteps = [...this.steps];
|
|
432
|
+
for (const q of done) this.usedIds.delete(q.id);
|
|
433
|
+
for (const q of removedSteps) this.usedIds.delete(q.id);
|
|
212
434
|
this.quests = this.quests.filter((q) => !q.done);
|
|
213
|
-
|
|
214
|
-
this.quests[i].id = i + 1;
|
|
215
|
-
}
|
|
216
|
-
this.nextId = this.quests.length + 1;
|
|
435
|
+
this.steps = keptSteps;
|
|
217
436
|
this.history.push({
|
|
218
437
|
type: QUEST_ACTIONS.clear,
|
|
219
438
|
previousQuests,
|
|
220
|
-
|
|
439
|
+
previousSteps,
|
|
221
440
|
all: false,
|
|
222
441
|
});
|
|
223
|
-
|
|
224
|
-
|
|
442
|
+
const count = done.length + removedSteps.length;
|
|
443
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
444
|
+
return count;
|
|
225
445
|
}
|
|
226
|
-
const count = this.quests.length;
|
|
446
|
+
const count = this.quests.length + this.steps.length;
|
|
227
447
|
this.history.push({
|
|
228
448
|
type: QUEST_ACTIONS.clear,
|
|
229
449
|
quests: [...this.quests],
|
|
230
|
-
|
|
450
|
+
steps: [...this.steps],
|
|
231
451
|
all: true,
|
|
232
452
|
});
|
|
233
453
|
this.quests = [];
|
|
234
|
-
this.
|
|
454
|
+
this.steps = [];
|
|
455
|
+
this.usedIds.clear();
|
|
235
456
|
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
236
457
|
return count;
|
|
237
458
|
}
|
|
238
459
|
|
|
239
|
-
reorder(id:
|
|
460
|
+
reorder(id: string, targetId: string): Quest | undefined {
|
|
461
|
+
if (this.steps.some((step) => step.id === id || step.id === targetId)) {
|
|
462
|
+
logger.debug("quests:state", "reorder-step-rejected", { id, targetId });
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
240
465
|
const idx = this.quests.findIndex((q) => q.id === id);
|
|
241
466
|
if (idx === -1) {
|
|
242
467
|
logger.debug("quests:state", "reorder-not-found", { id });
|
|
243
468
|
return undefined;
|
|
244
469
|
}
|
|
245
470
|
|
|
471
|
+
const targetIdx = this.quests.findIndex((q) => q.id === targetId);
|
|
472
|
+
if (targetIdx === -1) {
|
|
473
|
+
logger.debug("quests:state", "reorder-target-not-found", { id, targetId });
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (idx === targetIdx) {
|
|
478
|
+
return this.quests[idx];
|
|
479
|
+
}
|
|
480
|
+
|
|
246
481
|
const previousIds = this.quests.map((q) => q.id);
|
|
247
482
|
const [quest] = this.quests.splice(idx, 1);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
483
|
+
|
|
484
|
+
let insertIndex = targetIdx;
|
|
485
|
+
if (idx < targetIdx) {
|
|
486
|
+
insertIndex = targetIdx - 1;
|
|
251
487
|
}
|
|
252
488
|
|
|
253
|
-
this.
|
|
254
|
-
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds });
|
|
489
|
+
this.quests.splice(insertIndex, 0, quest);
|
|
490
|
+
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds, targetId });
|
|
255
491
|
logger.debug("quests:state", QUEST_ACTIONS.reorder, {
|
|
256
492
|
id: quest.id,
|
|
257
|
-
|
|
258
|
-
total: this.quests.length,
|
|
493
|
+
targetId,
|
|
494
|
+
total: this.quests.length + this.steps.length,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return quest;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
reparent(id: string, parentId?: string): Quest | Step | undefined {
|
|
501
|
+
const quest = this.findById(id);
|
|
502
|
+
if (!quest) return undefined;
|
|
503
|
+
|
|
504
|
+
if (parentId) {
|
|
505
|
+
if (id === parentId) {
|
|
506
|
+
throw new Error(formatReparentSelfParentError(id));
|
|
507
|
+
}
|
|
508
|
+
const target = this.findById(parentId);
|
|
509
|
+
if (!target) {
|
|
510
|
+
throw new Error(formatReparentTargetNotFoundError(parentId));
|
|
511
|
+
}
|
|
512
|
+
if ("parentId" in target) {
|
|
513
|
+
throw new Error(formatReparentTargetIsStepError(parentId));
|
|
514
|
+
}
|
|
515
|
+
if (target.done) {
|
|
516
|
+
throw new Error(formatReparentTargetDoneError(parentId));
|
|
517
|
+
}
|
|
518
|
+
if (!("parentId" in quest)) {
|
|
519
|
+
const steps = this.getSteps(id);
|
|
520
|
+
if (steps.length > 0) {
|
|
521
|
+
throw new Error(formatReparentDemoteHasStepsError(id));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const previousQuestIndex = this.quests.findIndex((q) => q.id === id);
|
|
525
|
+
const previousParentId = "parentId" in quest ? quest.parentId : undefined;
|
|
526
|
+
this.quests = this.quests.filter((q) => q.id !== id);
|
|
527
|
+
this.steps = this.steps.filter((s) => s.id !== id);
|
|
528
|
+
(quest as Step).parentId = parentId;
|
|
529
|
+
this.steps.push(quest as Step);
|
|
530
|
+
this.history.push({
|
|
531
|
+
type: QUEST_ACTIONS.reparent,
|
|
532
|
+
id,
|
|
533
|
+
previousParentId,
|
|
534
|
+
previousQuestIndex: previousQuestIndex === -1 ? undefined : previousQuestIndex,
|
|
535
|
+
});
|
|
536
|
+
} else {
|
|
537
|
+
if (!("parentId" in quest)) {
|
|
538
|
+
return quest;
|
|
539
|
+
}
|
|
540
|
+
const previousParentId = quest.parentId;
|
|
541
|
+
this.steps = this.steps.filter((s) => s.id !== id);
|
|
542
|
+
delete (quest as Partial<Step>).parentId;
|
|
543
|
+
this.quests.push(quest);
|
|
544
|
+
this.history.push({ type: QUEST_ACTIONS.reparent, id, previousParentId });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
logger.debug("quests:state", QUEST_ACTIONS.reparent, {
|
|
548
|
+
id,
|
|
549
|
+
parentId,
|
|
550
|
+
total: this.quests.length + this.steps.length,
|
|
259
551
|
});
|
|
260
552
|
|
|
261
553
|
return quest;
|
|
@@ -265,7 +557,7 @@ export class QuestLog {
|
|
|
265
557
|
const entry = this.history.pop();
|
|
266
558
|
if (!entry) {
|
|
267
559
|
logger.debug("quests:state", "revert-empty");
|
|
268
|
-
return { success: false, message:
|
|
560
|
+
return { success: false, message: formatNothingToRevertError() };
|
|
269
561
|
}
|
|
270
562
|
|
|
271
563
|
logger.debug("quests:state", "revert", { type: entry.type });
|
|
@@ -290,20 +582,40 @@ export class QuestLog {
|
|
|
290
582
|
if (action.descriptions.some((d) => !d || d.trim().length === 0)) {
|
|
291
583
|
return {
|
|
292
584
|
success: false,
|
|
293
|
-
message:
|
|
585
|
+
message: formatEmptyDescriptionsError(),
|
|
294
586
|
};
|
|
295
587
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
588
|
+
if (action.descriptions.length === 1) {
|
|
589
|
+
try {
|
|
590
|
+
const [firstDesc] = action.descriptions;
|
|
591
|
+
const q = this.add(firstDesc);
|
|
592
|
+
|
|
593
|
+
return { success: true, message: formatAddResult(q) };
|
|
594
|
+
} catch (err) {
|
|
595
|
+
return {
|
|
596
|
+
success: false,
|
|
597
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const added: { id: string; description: string }[] = [];
|
|
603
|
+
for (const desc of action.descriptions) {
|
|
604
|
+
const q = this.add(desc);
|
|
605
|
+
added.push({ id: q.id, description: q.description });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { success: true, message: formatBatchAddResult(added) };
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
613
|
+
};
|
|
300
614
|
}
|
|
301
|
-
|
|
302
|
-
return { success: true, message: formatBatchAddResult(added) };
|
|
303
615
|
}
|
|
304
616
|
return {
|
|
305
617
|
success: false,
|
|
306
|
-
message:
|
|
618
|
+
message: formatMissingDescriptionsError(),
|
|
307
619
|
};
|
|
308
620
|
}
|
|
309
621
|
case QUEST_ACTIONS.list: {
|
|
@@ -312,23 +624,26 @@ export class QuestLog {
|
|
|
312
624
|
}
|
|
313
625
|
case QUEST_ACTIONS.toggle: {
|
|
314
626
|
if (action.id === undefined) {
|
|
315
|
-
return { success: false, message: "
|
|
627
|
+
return { success: false, message: formatIdRequiredError("toggle") };
|
|
316
628
|
}
|
|
317
629
|
|
|
318
630
|
const q = this.toggle(action.id);
|
|
319
|
-
if (
|
|
631
|
+
if (q === undefined) {
|
|
320
632
|
return { success: false, message: formatNotFound(action.id) };
|
|
321
633
|
}
|
|
634
|
+
if (q === null) {
|
|
635
|
+
return { success: false, message: formatBlockedBySteps(action.id) };
|
|
636
|
+
}
|
|
322
637
|
|
|
323
638
|
return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
|
|
324
639
|
}
|
|
325
640
|
case QUEST_ACTIONS.update: {
|
|
326
641
|
if (action.id === undefined) {
|
|
327
|
-
return { success: false, message: "
|
|
642
|
+
return { success: false, message: formatIdRequiredError("update") };
|
|
328
643
|
}
|
|
329
644
|
|
|
330
|
-
if (!action.description) {
|
|
331
|
-
return { success: false, message:
|
|
645
|
+
if (!action.description || action.description.trim().length === 0) {
|
|
646
|
+
return { success: false, message: formatDescriptionRequiredError() };
|
|
332
647
|
}
|
|
333
648
|
|
|
334
649
|
const q = this.update(action.id, action.description);
|
|
@@ -340,13 +655,16 @@ export class QuestLog {
|
|
|
340
655
|
}
|
|
341
656
|
case QUEST_ACTIONS.delete: {
|
|
342
657
|
if (action.id === undefined) {
|
|
343
|
-
return { success: false, message: "
|
|
658
|
+
return { success: false, message: formatIdRequiredError("delete") };
|
|
344
659
|
}
|
|
345
660
|
|
|
346
661
|
const q = this.delete(action.id);
|
|
347
|
-
if (
|
|
662
|
+
if (q === undefined) {
|
|
348
663
|
return { success: false, message: formatNotFound(action.id) };
|
|
349
664
|
}
|
|
665
|
+
if (q === null) {
|
|
666
|
+
return { success: false, message: formatBlockedBySteps(action.id) };
|
|
667
|
+
}
|
|
350
668
|
|
|
351
669
|
return { success: true, message: formatDeleteResult(q), quest: q };
|
|
352
670
|
}
|
|
@@ -360,21 +678,66 @@ export class QuestLog {
|
|
|
360
678
|
}
|
|
361
679
|
case QUEST_ACTIONS.reorder: {
|
|
362
680
|
if (action.id === undefined)
|
|
363
|
-
return { success: false, message: "
|
|
681
|
+
return { success: false, message: formatIdRequiredError("reorder") };
|
|
364
682
|
|
|
365
|
-
if (action.
|
|
366
|
-
return { success: false, message:
|
|
683
|
+
if (action.targetId === undefined)
|
|
684
|
+
return { success: false, message: formatTargetIdRequiredError() };
|
|
367
685
|
|
|
368
|
-
const q = this.reorder(action.id, action.
|
|
369
|
-
if (!q) return { success: false, message:
|
|
686
|
+
const q = this.reorder(action.id, action.targetId);
|
|
687
|
+
if (!q) return { success: false, message: formatReorderNotFoundError() };
|
|
370
688
|
|
|
371
|
-
return { success: true, message: `Reordered quest
|
|
689
|
+
return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
|
|
690
|
+
}
|
|
691
|
+
case QUEST_ACTIONS.split:
|
|
692
|
+
case QUEST_ACTIONS.add_step: {
|
|
693
|
+
if (action.id === undefined) {
|
|
694
|
+
return { success: false, message: formatIdRequiredError("split") };
|
|
695
|
+
}
|
|
696
|
+
if (!action.descriptions || action.descriptions.length === 0) {
|
|
697
|
+
return { success: false, message: formatMissingDescriptionsError("split") };
|
|
698
|
+
}
|
|
699
|
+
if (action.descriptions.some((d) => !d || d.trim().length === 0)) {
|
|
700
|
+
return { success: false, message: formatEmptyDescriptionsError() };
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const steps = this.split(action.id, action.descriptions);
|
|
704
|
+
return {
|
|
705
|
+
success: true,
|
|
706
|
+
message: `Split quest [${action.id}] into ${steps.length} steps`,
|
|
707
|
+
};
|
|
708
|
+
} catch (err) {
|
|
709
|
+
return {
|
|
710
|
+
success: false,
|
|
711
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
case QUEST_ACTIONS.reparent: {
|
|
716
|
+
if (action.id === undefined)
|
|
717
|
+
return { success: false, message: formatIdRequiredError("reparent") };
|
|
718
|
+
try {
|
|
719
|
+
const q = this.reparent(action.id, action.parentId);
|
|
720
|
+
if (!q) return { success: false, message: formatNotFound(action.id) };
|
|
721
|
+
return { success: true, message: formatReparentResult(q, action.parentId), quest: q };
|
|
722
|
+
} catch (err) {
|
|
723
|
+
return {
|
|
724
|
+
success: false,
|
|
725
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
case QUEST_ACTIONS.rules:
|
|
730
|
+
case QUEST_ACTIONS.skill: {
|
|
731
|
+
return { success: true, message: getQuestSkillDocument() };
|
|
372
732
|
}
|
|
373
733
|
case QUEST_ACTIONS.revert: {
|
|
374
734
|
return this.revert();
|
|
375
735
|
}
|
|
376
736
|
default: {
|
|
377
|
-
return {
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
message: formatUnknownActionError((action as { type: string }).type),
|
|
740
|
+
};
|
|
378
741
|
}
|
|
379
742
|
}
|
|
380
743
|
}
|
|
@@ -398,15 +761,21 @@ export class QuestLog {
|
|
|
398
761
|
|
|
399
762
|
if (lastState) {
|
|
400
763
|
const quests = lastState.quests;
|
|
401
|
-
const nextId = lastState.nextId;
|
|
402
764
|
const questCount = Array.isArray(quests) ? quests.length : 0;
|
|
403
765
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
766
|
+
if (Array.isArray(quests)) {
|
|
767
|
+
this.quests = (quests as Quest[]).filter((q) => !("parentId" in q && q.parentId));
|
|
768
|
+
this.steps = (quests as Quest[]).filter((q) => "parentId" in q && q.parentId) as Step[];
|
|
769
|
+
} else {
|
|
770
|
+
this.quests = [];
|
|
771
|
+
this.steps = [];
|
|
772
|
+
}
|
|
773
|
+
this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
|
|
774
|
+
logger.debug("quests:state", "reconstruct", { toolResults, questCount });
|
|
407
775
|
} else {
|
|
408
776
|
this.quests = [];
|
|
409
|
-
this.
|
|
777
|
+
this.steps = [];
|
|
778
|
+
this.usedIds.clear();
|
|
410
779
|
logger.debug("quests:state", "reconstruct-empty", { toolResults });
|
|
411
780
|
}
|
|
412
781
|
|
|
@@ -423,7 +792,7 @@ export function makeToolResult(
|
|
|
423
792
|
content: [{ type: "text", text }],
|
|
424
793
|
details: {
|
|
425
794
|
quests: questLog.getAll(),
|
|
426
|
-
|
|
795
|
+
usedIds: questLog.getUsedIds(),
|
|
427
796
|
displayQuests,
|
|
428
797
|
},
|
|
429
798
|
};
|