pi-quests 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.0] - 2026-04-11
6
+
7
+ - feat: add dynamic quest usage nudges via context hook
8
+ - feat: inject quest management reminders into system prompt
9
+ - feat: render targeted quest results and add parameter descriptions
10
+ - chore: add npm release flow
11
+ - chore: refactor codebase for improved maintainability
12
+
5
13
  ## [0.1.0] - 2026-04-10
6
14
 
7
15
  - feat: add session-scoped quest log with tools, commands, and TUI widgets
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-quests
2
2
 
3
- [![version 0.1.0](https://img.shields.io/badge/version-0.1.0-blue)](CHANGELOG.md)
3
+ [![version 0.2.0](https://img.shields.io/badge/version-0.2.0-blue)](CHANGELOG.md)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
5
5
  [![pi extension](https://img.shields.io/badge/pi-extension-purple)](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
6
6
 
@@ -10,34 +10,51 @@ A quest-log for your [pi](https://github.com/badlogic/pi-mono/tree/main/packages
10
10
 
11
11
  Long agent sessions drift. Goals get lost in tool calls, side quests multiply, and the original task is forgotten under a pile of yak hair.
12
12
 
13
+ ![Quests log in action](docs/assets/quests_log.png)
14
+
13
15
  pi-quests gives your agent a persistent quest log — a living TODO list for the current session. Each quest is a checkpoint the agent can create, complete, and optionally roll back to if things go sideways.
14
16
 
15
17
  - **Stay focused** — the quest log keeps the original goal visible no matter how deep the rabbit hole goes
16
- - **Checkpoint progress** — mark milestones as quests and capture lightweight session snapshots
17
- - **Rollback safety** — revert to a previous quest snapshot when exploration goes off track
18
+ - **Checkpoint progress** — mark milestones as quests and capture lightweight session snapshots (WIP)
19
+ - **Rollback safety** — revert to a previous quest snapshot when exploration goes off track (WIP)
18
20
  - **Session-scoped** — quests live with the session, vanish when you start fresh
19
21
 
20
22
  ## Install
21
23
 
22
- TBD
24
+ ```bash
25
+ pi install npm:pi-quests
26
+ ```
23
27
 
24
28
  ## Quick start
25
29
 
26
- **See the extension version:**
27
- ```bash
28
- /quests version
29
30
  ```
30
-
31
- **View the changelog:**
32
- ```bash
33
- /quests changelog
31
+ /quests help
32
+
33
+ Available /quests subcommands:
34
+ add <description> - Add a new quest
35
+ list - List all quests
36
+ toggle <id> - Toggle quest completion
37
+ delete <id> - Delete a quest
38
+ update <id> <desc> - Update a quest description
39
+ revert - Revert the last quest change
40
+ clear - Clear all quests
41
+ version - Show version
42
+ changelog - Show changelog
43
+ h, help - Show this help message
34
44
  ```
35
45
 
46
+ ![Quests toggle](docs/assets/quests_toggle.png)
47
+ ![Quests view](docs/assets/quests_view.png)
48
+
49
+
36
50
  ## Documentation
37
51
 
38
52
  | Doc | Description |
39
53
  |-----|-------------|
40
- | [Roadmap](docs/roadmap.md) | Feature backlog and progress tracker |
54
+ | [Pattern](docs/pattern.md) | "How do I...?" recipes for common workflows |
55
+ | [Quests](docs/quests.md) | What are quests? |
56
+ | [Reference](docs/reference.md) | Complete tool and command schemas, types |
57
+ | [Architecture](docs/architecture.md) | Module map, data flow diagrams, design decisions |
41
58
  | [Changelog](CHANGELOG.md) | Version history |
42
59
 
43
60
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quests",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A quest-log for your pi. Keep your agent on track, one quest at a time.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reverse changelog sections so the newest release appears first.
3
+ */
4
+ export function reverseChangelog(content: string): string {
5
+ const parts = content.split(/^## \[/m);
6
+ const preamble = parts[0];
7
+ const sections = parts.slice(1).map((s) => `## [${s}`);
8
+ return [preamble, ...sections.reverse()].join("");
9
+ }
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
3
+ import { logger } from "../logger.js";
4
+ import type { QuestAction, QuestLog } from "../quest/dataplane.js";
5
+ import { QUEST_ACTIONS } from "../quest/types.js";
6
+ import { QuestListWidget } from "../renderers/commands.js";
7
+ import { CHANGELOG_PATH, getVersion } from "../version.js";
8
+ import { reverseChangelog } from "./changelog.js";
9
+ import { type ParsedArgs, parseQuestArgs } from "./parse-args.js";
10
+
11
+ type MutatingCommand = Extract<
12
+ ParsedArgs,
13
+ {
14
+ action:
15
+ | typeof QUEST_ACTIONS.add
16
+ | typeof QUEST_ACTIONS.toggle
17
+ | typeof QUEST_ACTIONS.update
18
+ | typeof QUEST_ACTIONS.delete
19
+ | typeof QUEST_ACTIONS.clear
20
+ | typeof QUEST_ACTIONS.revert;
21
+ }
22
+ >["action"];
23
+
24
+ const commandActionBuilders: {
25
+ [K in MutatingCommand]: (parsed: Extract<ParsedArgs, { action: K }>) => QuestAction;
26
+ } = {
27
+ [QUEST_ACTIONS.add]: (p) => ({ type: QUEST_ACTIONS.add, descriptions: p.descriptions }),
28
+ [QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle, id: p.id }),
29
+ [QUEST_ACTIONS.update]: (p) => ({
30
+ type: QUEST_ACTIONS.update,
31
+ id: p.id,
32
+ description: p.description,
33
+ }),
34
+ [QUEST_ACTIONS.delete]: (p) => ({ type: QUEST_ACTIONS.delete, id: p.id }),
35
+ [QUEST_ACTIONS.clear]: () => ({ type: QUEST_ACTIONS.clear }),
36
+ [QUEST_ACTIONS.revert]: () => ({ type: QUEST_ACTIONS.revert }),
37
+ };
38
+
39
+ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
40
+ return async function handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
41
+ logger.debug("quests:cmd", "handler", { args, hasUI: ctx.hasUI });
42
+
43
+ const parsed = parseQuestArgs(args);
44
+ if ("error" in parsed) {
45
+ logger.debug("quests:cmd", "handler-error", { error: parsed.error });
46
+ ctx.ui.notify(parsed.error, "error");
47
+ return;
48
+ }
49
+
50
+ switch (parsed.action) {
51
+ case "version": {
52
+ const version = getVersion();
53
+ logger.debug("quests:cmd", "version", { version });
54
+ ctx.ui.notify(`pi-quests v${version}`, "info");
55
+ return;
56
+ }
57
+ case "changelog": {
58
+ logger.debug("quests:cmd", "changelog", { changelogPath: CHANGELOG_PATH });
59
+
60
+ try {
61
+ const content = readFileSync(CHANGELOG_PATH, "utf-8");
62
+ logger.debug("quests:cmd", "changelog-read", { contentLength: content.length });
63
+
64
+ const reversedContent = reverseChangelog(content);
65
+ logger.debug("quests:cmd", "changelog-reversed");
66
+
67
+ pi.sendMessage({
68
+ customType: "quest-changelog",
69
+ content: "",
70
+ display: true,
71
+ details: { content: reversedContent },
72
+ });
73
+
74
+ logger.debug("quests:cmd", "changelog-sent");
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : String(error);
77
+
78
+ logger.debug("quests:cmd", "changelog-error", { error: errorMessage });
79
+ ctx.ui.notify(`Failed to read changelog: ${errorMessage}`, "error");
80
+ }
81
+ return;
82
+ }
83
+ case QUEST_ACTIONS.list: {
84
+ if (!ctx.hasUI) {
85
+ logger.debug("quests:cmd", "list-no-ui");
86
+ ctx.ui.notify("Interactive mode required", "error");
87
+ return;
88
+ }
89
+
90
+ logger.debug("quests:cmd", "list-open-widget", { count: questLog.getAll().length });
91
+ await ctx.ui.custom(
92
+ (_, theme, __, done) =>
93
+ new QuestListWidget(questLog.getAll(), theme, () => done(undefined)),
94
+ );
95
+
96
+ logger.debug("quests:cmd", "list-widget-closed");
97
+ return;
98
+ }
99
+ case QUEST_ACTIONS.add:
100
+ case QUEST_ACTIONS.toggle:
101
+ case QUEST_ACTIONS.update:
102
+ case QUEST_ACTIONS.delete:
103
+ case QUEST_ACTIONS.clear:
104
+ case QUEST_ACTIONS.revert: {
105
+ const builder = commandActionBuilders[parsed.action];
106
+ const action = builder(parsed as never);
107
+ const result = questLog.execute(action);
108
+
109
+ logger.debug("quests:cmd", parsed.action, { success: result.success });
110
+ ctx.ui.notify(result.message, result.success ? "info" : "error");
111
+ return;
112
+ }
113
+ case "help": {
114
+ logger.debug("quests:cmd", "help");
115
+
116
+ const lines = ["Available /quests subcommands:"];
117
+ lines.push(" add <description> - Add a new quest");
118
+ lines.push(" list - List all quests");
119
+ lines.push(" toggle <id> - Toggle quest completion");
120
+ lines.push(" delete <id> - Delete a quest");
121
+ lines.push(" update <id> <desc> - Update a quest description");
122
+ lines.push(" revert - Revert the last quest change");
123
+ lines.push(" clear - Clear all quests");
124
+ lines.push(" version - Show version");
125
+ lines.push(" changelog - Show changelog");
126
+ lines.push(" h, help - Show this help message");
127
+
128
+ ctx.ui.notify(lines.join("\n"), "info");
129
+ logger.debug("quests:cmd", "help-complete");
130
+ return;
131
+ }
132
+ }
133
+ };
134
+ }
@@ -0,0 +1,78 @@
1
+ import { logger } from "../logger.js";
2
+ import { QUEST_ACTIONS } from "../quest/types.js";
3
+
4
+ export type ParsedArgs =
5
+ | { action: typeof QUEST_ACTIONS.add; descriptions: string[] }
6
+ | { action: typeof QUEST_ACTIONS.list }
7
+ | { action: typeof QUEST_ACTIONS.toggle; id: number }
8
+ | { action: typeof QUEST_ACTIONS.update; id: number; description: string }
9
+ | { action: typeof QUEST_ACTIONS.delete; id: number }
10
+ | { action: typeof QUEST_ACTIONS.clear }
11
+ | { action: typeof QUEST_ACTIONS.revert }
12
+ | { action: "help" }
13
+ | { action: "version" }
14
+ | { action: "changelog" }
15
+ | { error: string };
16
+
17
+ /**
18
+ * Parse user input from the /quests command into structured arguments.
19
+ */
20
+ export function parseQuestArgs(args: string): ParsedArgs {
21
+ logger.debug("quests:cmd", "parse-args", { args });
22
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
23
+
24
+ // Empty input defaults to listing quests.
25
+ if (tokens.length === 0) {
26
+ logger.debug("quests:cmd", "parse-args-empty", { action: QUEST_ACTIONS.list });
27
+ return { action: QUEST_ACTIONS.list };
28
+ }
29
+
30
+ const [command, ...rest] = tokens;
31
+
32
+ // Meta commands
33
+ if (command === "version") return { action: "version" };
34
+ if (command === "changelog") return { action: "changelog" };
35
+ if (command === "help" || command === "h") return { action: "help" };
36
+
37
+ // Quest actions without arguments
38
+ if (command === QUEST_ACTIONS.list) return { action: QUEST_ACTIONS.list };
39
+ if (command === QUEST_ACTIONS.clear) return { action: QUEST_ACTIONS.clear };
40
+ if (command === QUEST_ACTIONS.revert) return { action: QUEST_ACTIONS.revert };
41
+
42
+ // Quest actions with arguments
43
+ if (command === QUEST_ACTIONS.add) return parseAddArgs(rest);
44
+ if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest);
45
+ if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest);
46
+ if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest);
47
+
48
+ return { error: `Unknown subcommand: ${command}. Use /quests help to see available commands.` };
49
+ }
50
+
51
+ function parseAddArgs(tokens: string[]): ParsedArgs {
52
+ const description = tokens.join(" ").trim();
53
+ if (!description) return { error: "Usage: /quests add <description>" };
54
+
55
+ return { action: QUEST_ACTIONS.add, descriptions: [description] };
56
+ }
57
+
58
+ function parseIdAction(
59
+ action: typeof QUEST_ACTIONS.toggle | typeof QUEST_ACTIONS.delete,
60
+ tokens: string[],
61
+ ): ParsedArgs {
62
+ const idStr = tokens[0];
63
+ const id = idStr ? Number(idStr) : NaN;
64
+ if (Number.isNaN(id)) return { error: `Usage: /quests ${action} <id>` };
65
+
66
+ return { action, id };
67
+ }
68
+
69
+ function parseUpdateArgs(tokens: string[]): ParsedArgs {
70
+ const idStr = tokens[0];
71
+ const id = idStr ? Number(idStr) : NaN;
72
+ if (Number.isNaN(id)) return { error: "Usage: /quests update <id> <description>" };
73
+
74
+ const description = tokens.slice(1).join(" ").trim();
75
+ if (!description) return { error: "Usage: /quests update <id> <description>" };
76
+
77
+ return { action: QUEST_ACTIONS.update, id, description };
78
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { createQuestsHandler } from "./commands/quests.js";
3
- import { QuestLog } from "./quests.js";
2
+ import { createQuestsHandler } from "./commands/handler.js";
3
+ import { QuestLog } from "./quest/dataplane.js";
4
+ import { QuestUsageTracker } from "./quest/tracker.js";
4
5
  import { questChangelogRenderer } from "./renderers/changelog.js";
5
- import { registerQuestTool } from "./tools/quest.js";
6
+ import { registerQuestTool } from "./tools/handler.js";
6
7
 
7
8
  /**
8
9
  * pi-quests
@@ -16,10 +17,54 @@ import { registerQuestTool } from "./tools/quest.js";
16
17
  */
17
18
  export default function (pi: ExtensionAPI): void {
18
19
  const questLog = new QuestLog();
20
+ const tracker = new QuestUsageTracker();
19
21
 
20
22
  pi.on("session_start", async (_event, ctx) => questLog.reconstructFromSession(ctx));
21
23
  pi.on("session_tree", async (_event, ctx) => questLog.reconstructFromSession(ctx));
22
24
 
25
+ pi.on("turn_start", async () => tracker.clearTurnNudge());
26
+
27
+ pi.on("tool_execution_end", async (event) => {
28
+ tracker.onToolExecution(event.toolName);
29
+ });
30
+
31
+ pi.on("context", async (event) => {
32
+ const latestPrompt = event.messages
33
+ .filter((m) => m.role === "user")
34
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
35
+ .join("\n");
36
+
37
+ const activeQuestCount = questLog.getAll().filter((q) => !q.done).length;
38
+ const nudge = tracker.getNudge(activeQuestCount, latestPrompt);
39
+ if (!nudge) return undefined;
40
+
41
+ const reminder: import("@mariozechner/pi-ai").UserMessage = {
42
+ role: "user",
43
+ content: nudge,
44
+ timestamp: Date.now(),
45
+ };
46
+ return { messages: [...event.messages, reminder] };
47
+ });
48
+
49
+ pi.on("before_agent_start", async (event) => {
50
+ const quests = questLog.getAll();
51
+ const gate =
52
+ "# Quest Management\nBefore reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests. ALWAYS break broad requests into concrete steps.\n\n";
53
+
54
+ let reminder =
55
+ "## Quest Management\nUse the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation. It is critical that you toggle quests to done as soon as you complete them. NEVER batch up multiple tasks before marking them completed.\n\nNEVER create a single vague quest for broad requests. Analyze the user's intent and break it into specific, independent, actionable quests that each represent a concrete step.";
56
+ if (quests.length > 0) {
57
+ const remaining = quests.filter((q) => !q.done).length;
58
+ const list = quests
59
+ .map((q) => `#${q.id} [${q.done ? "x" : " "}] ${q.description}`)
60
+ .join("\n");
61
+
62
+ reminder += `\n\nActive quests (${remaining}/${quests.length}):\n${list}`;
63
+ }
64
+
65
+ return { systemPrompt: `${gate}${event.systemPrompt}\n\n${reminder}` };
66
+ });
67
+
23
68
  registerQuestTool(pi, questLog);
24
69
 
25
70
  // Register custom message renderers
package/src/logger.ts CHANGED
@@ -3,13 +3,22 @@ import { dirname } from "node:path";
3
3
 
4
4
  export const LOG_FILE = "/tmp/logs/pi-quests/debug.log";
5
5
 
6
- mkdirSync(dirname(LOG_FILE), { recursive: true });
6
+ let stream: ReturnType<typeof createWriteStream> | undefined;
7
+
8
+ function getStream() {
9
+ if (!stream) {
10
+ mkdirSync(dirname(LOG_FILE), { recursive: true });
11
+ stream = createWriteStream(LOG_FILE, { flags: "a" });
12
+ stream.on("error", () => {});
13
+ }
14
+ return stream;
15
+ }
7
16
 
8
17
  export const logger = {
9
18
  debug: (namespace: string, event: string, meta?: Record<string, unknown>) => {
10
19
  const timestamp = new Date().toISOString();
11
20
  const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
12
21
  const line = `[${timestamp}] [${namespace}] ${event}${metaStr}\n`;
13
- createWriteStream(LOG_FILE, { flags: "a" }).write(line);
22
+ getStream().write(line);
14
23
  },
15
24
  };