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 +8 -0
- package/README.md +29 -12
- package/package.json +1 -1
- package/src/commands/changelog.ts +9 -0
- package/src/commands/handler.ts +134 -0
- package/src/commands/parse-args.ts +78 -0
- package/src/index.ts +48 -3
- package/src/logger.ts +11 -2
- package/src/quest/dataplane.ts +337 -0
- package/src/quest/formatters.ts +34 -0
- package/src/quest/tracker.ts +87 -0
- package/src/quest/types.ts +33 -0
- package/src/renderers/commands.ts +9 -2
- package/src/renderers/tools.ts +17 -22
- package/src/tools/handler.ts +100 -0
- package/src/tools/params.ts +24 -0
- package/src/version.ts +9 -2
- package/src/commands/quests.ts +0 -220
- package/src/quests.ts +0 -222
- package/src/tools/quest.ts +0 -136
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
|
-
[](CHANGELOG.md)
|
|
4
4
|
[](LICENSE.md)
|
|
5
5
|
[](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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+

|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
|
|
36
50
|
## Documentation
|
|
37
51
|
|
|
38
52
|
| Doc | Description |
|
|
39
53
|
|-----|-------------|
|
|
40
|
-
| [
|
|
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
|
@@ -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/
|
|
3
|
-
import { QuestLog } from "./
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
getStream().write(line);
|
|
14
23
|
},
|
|
15
24
|
};
|