@rol1n/pi-kit 0.0.0-stage → 0.1.1

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.
Files changed (120) hide show
  1. package/README.md +73 -2
  2. package/ask-u-questions/README.md +26 -0
  3. package/ask-u-questions/dialog-ui.ts +80 -0
  4. package/ask-u-questions/index.ts +92 -0
  5. package/ask-u-questions/multi-choice.ts +258 -0
  6. package/ask-u-questions/single-choice.ts +180 -0
  7. package/ask-u-questions/tool-renderers.ts +67 -0
  8. package/ask-u-questions/types.ts +192 -0
  9. package/continue-signal/README.md +20 -0
  10. package/continue-signal/index.ts +69 -0
  11. package/custom-providers/README.md +20 -0
  12. package/custom-providers/asr/handlers/dashscope.ts +154 -0
  13. package/custom-providers/asr/index.ts +37 -0
  14. package/custom-providers/asr/models.ts +18 -0
  15. package/custom-providers/asr/types.ts +20 -0
  16. package/custom-providers/chat/index.ts +33 -0
  17. package/custom-providers/chat/magpie.ts +258 -0
  18. package/custom-providers/chat/models.ts +111 -0
  19. package/custom-providers/classifier/index.ts +6 -0
  20. package/custom-providers/classifier/models.ts +1 -0
  21. package/custom-providers/image/handlers/dashscope.ts +240 -0
  22. package/custom-providers/image/handlers/openrouter.ts +185 -0
  23. package/custom-providers/image/index.ts +96 -0
  24. package/custom-providers/image/models.ts +71 -0
  25. package/custom-providers/image/types.ts +22 -0
  26. package/custom-providers/index.ts +64 -0
  27. package/custom-providers/types.ts +17 -0
  28. package/image-generate/README.md +25 -0
  29. package/image-generate/files.ts +49 -0
  30. package/image-generate/index.ts +220 -0
  31. package/image-generate/model-selector.ts +150 -0
  32. package/memory-log/README.md +54 -0
  33. package/memory-log/agent/consolidator/prompt.ts +47 -0
  34. package/memory-log/agent/consolidator/tools.ts +160 -0
  35. package/memory-log/agent/cost.ts +32 -0
  36. package/memory-log/agent/index.ts +42 -0
  37. package/memory-log/agent/observer/prompt.ts +73 -0
  38. package/memory-log/agent/observer/tool.ts +73 -0
  39. package/memory-log/commands/compact.ts +35 -0
  40. package/memory-log/commands/consolidate.ts +47 -0
  41. package/memory-log/commands/status.ts +50 -0
  42. package/memory-log/config.ts +253 -0
  43. package/memory-log/debug-log.ts +76 -0
  44. package/memory-log/hooks/compaction-hook.ts +161 -0
  45. package/memory-log/hooks/compaction-trigger.ts +160 -0
  46. package/memory-log/hooks/consolidator-trigger.ts +143 -0
  47. package/memory-log/hooks/observer-trigger.ts +176 -0
  48. package/memory-log/ids.ts +78 -0
  49. package/memory-log/index.ts +149 -0
  50. package/memory-log/ledger/fold.ts +77 -0
  51. package/memory-log/ledger/index.ts +7 -0
  52. package/memory-log/ledger/pool.ts +38 -0
  53. package/memory-log/ledger/progress.ts +181 -0
  54. package/memory-log/ledger/projection.ts +115 -0
  55. package/memory-log/ledger/render.ts +38 -0
  56. package/memory-log/ledger/serialize.ts +161 -0
  57. package/memory-log/ledger/types.ts +201 -0
  58. package/memory-log/memory/index-render.ts +45 -0
  59. package/memory-log/memory/paths.ts +120 -0
  60. package/memory-log/memory/session.ts +64 -0
  61. package/memory-log/runtime.ts +127 -0
  62. package/memory-log/spawn/launch.ts +133 -0
  63. package/memory-log/spawn/runs.ts +83 -0
  64. package/memory-log/tokens.ts +66 -0
  65. package/memory-log/ui/status-controller.ts +225 -0
  66. package/memory-log/ui/timeline.ts +103 -0
  67. package/model-vision/README.md +22 -0
  68. package/multi-harness/README.md +63 -0
  69. package/multi-harness/adapters.mjs +72 -0
  70. package/multi-harness/harness-done.mjs +14 -0
  71. package/multi-harness/index.ts +375 -0
  72. package/multi-harness/ipc.mjs +37 -0
  73. package/multi-harness/opencode-plugin.mjs +99 -0
  74. package/multi-harness/process-runner.mjs +115 -0
  75. package/multi-harness/turn-hook.mjs +29 -0
  76. package/multi-harness/worker-bridge.ts +52 -0
  77. package/oai-subquota/README.md +22 -0
  78. package/oai-subquota/index.ts +286 -0
  79. package/oai-subquota/snapshot.ts +293 -0
  80. package/oai-subquota/ui.ts +183 -0
  81. package/package.json +92 -4
  82. package/prompt-snipaste/README.md +27 -0
  83. package/prompt-snipaste/index.ts +143 -0
  84. package/prompt-snipaste/menu.ts +159 -0
  85. package/prompt-snipaste/prompts/ask-questions.md +8 -0
  86. package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
  87. package/prompt-snipaste/prompts/diagnose-report.md +7 -0
  88. package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
  89. package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
  90. package/prompt-snipaste/prompts/session-kickoff.md +7 -0
  91. package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
  92. package/prompt-snipaste/renderers.ts +87 -0
  93. package/prompt-snipaste/snippets.ts +206 -0
  94. package/prompt-snipaste/types.ts +18 -0
  95. package/screenshot/README.md +14 -0
  96. package/srp-theme/README.md +18 -0
  97. package/srp-theme/footer.ts +193 -0
  98. package/srp-theme/index.ts +172 -0
  99. package/srp-theme/tps-meter.ts +441 -0
  100. package/subagent/README.md +35 -0
  101. package/subagent/bridge.ts +171 -0
  102. package/subagent/index.ts +590 -0
  103. package/subagent/mux.ts +621 -0
  104. package/subagent/session.ts +166 -0
  105. package/subagent/ui.ts +100 -0
  106. package/tui-asr/README.md +21 -0
  107. package/tui-asr/dictation.ts +272 -0
  108. package/tui-asr/index.ts +181 -0
  109. package/tui-asr/input.ts +87 -0
  110. package/tui-asr/model-selector.ts +163 -0
  111. package/tui-asr/status.ts +79 -0
  112. package/web-reach/README.md +19 -0
  113. package/web-reach/html.ts +83 -0
  114. package/web-reach/index.ts +82 -0
  115. package/web-reach/page.ts +214 -0
  116. package/web-reach/rsc.ts +161 -0
  117. package/web-reach/search.ts +85 -0
  118. package/web-reach/transport.ts +35 -0
  119. package/windows-wheel-fix/README.md +12 -0
  120. package/windows-wheel-fix/index.ts +44 -0
@@ -0,0 +1,17 @@
1
+ import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
2
+ import type { AsrModelDefinition } from "./asr/models.ts";
3
+
4
+ export interface ModalityProviderConfig {
5
+ name?: string;
6
+ baseUrl?: string;
7
+ api?: string;
8
+ apiKey?: ProviderConfig["apiKey"];
9
+ headers?: Record<string, string>;
10
+ models?: Record<string, unknown>[];
11
+ refreshModels?: ProviderConfig["refreshModels"];
12
+ images?: ProviderConfig["images"];
13
+ classifiers?: ProviderConfig["classifiers"];
14
+ asr?: AsrModelDefinition[];
15
+ }
16
+
17
+ export type ModalityProvidersRecord = Record<string, ModalityProviderConfig>;
@@ -0,0 +1,25 @@
1
+ # image-generate
2
+
3
+ Generate images through OpenRouter or DashScope and save them to `.pi/generated-images/` in the current project. Registers the `image_generate` tool and `/image-generate` model selector.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/image-generate
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. Installing this package automatically installs `@rol1n/custom-providers`.
12
+
13
+ ## Use
14
+
15
+ Run `/image-generate model` to open the interactive model picker, or `/image-generate model qwen-image-3.0-pro` to switch directly.
16
+
17
+ ```json
18
+ {
19
+ "prompt": "A clean technical diagram of a terminal workflow",
20
+ "aspectRatio": "16:9",
21
+ "model": "google/gemini-3.1-flash-image"
22
+ }
23
+ ```
24
+
25
+ Authenticate via `/login openrouter` or `/login dashscope` (or `OPENROUTER_API_KEY` / `DASHSCOPE_API_KEY`).
@@ -0,0 +1,49 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, open, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+
5
+ const DEFAULT_OUTPUT_DIR = ".pi/generated-images";
6
+
7
+ function detectExtension(mimeType: string): string {
8
+ switch (mimeType) {
9
+ case "image/jpeg":
10
+ case "image/jpg":
11
+ return ".jpg";
12
+ case "image/webp":
13
+ return ".webp";
14
+ case "image/gif":
15
+ return ".gif";
16
+ case "image/bmp":
17
+ return ".bmp";
18
+ default:
19
+ return ".png";
20
+ }
21
+ }
22
+
23
+ export async function saveImageAtomically(cwd: string, buffer: Buffer, mimeType: string): Promise<string> {
24
+ const outputDir = resolve(cwd, DEFAULT_OUTPUT_DIR);
25
+ if (!existsSync(outputDir)) {
26
+ await mkdir(outputDir, { recursive: true });
27
+ }
28
+
29
+ const date = new Date().toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
30
+ const ext = detectExtension(mimeType);
31
+
32
+ for (let i = 1; i <= 999; i++) {
33
+ const filename = `${date}-${String(i).padStart(3, "0")}${ext}`;
34
+ const targetPath = join(outputDir, filename);
35
+ try {
36
+ const handle = await open(targetPath, "wx");
37
+ await handle.writeFile(buffer);
38
+ await handle.close();
39
+ return targetPath;
40
+ } catch (err: any) {
41
+ if (err?.code === "EEXIST") continue;
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ const fallback = join(outputDir, `${date}-${Date.now()}${ext}`);
47
+ await writeFile(fallback, buffer);
48
+ return fallback;
49
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * image-generate — Image generation tool and model selector.
3
+ *
4
+ * Provides:
5
+ * 1. Tool `image_generate`: Tool call for generating images with local disk persistence.
6
+ * Delegates generation to adapters in custom-providers/image (DashScope and OpenRouter).
7
+ * 2. Command `/image-generate model`: Interactive model picker matching Pi's native model selector UI.
8
+ */
9
+
10
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
+ import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
12
+ import { Type } from "typebox";
13
+
14
+ import {
15
+ ensureCustomProviders,
16
+ fetchAllAvailableImageModels,
17
+ generateImage,
18
+ resolveImageModel,
19
+ type ImageModelDefinition,
20
+ } from "../custom-providers/index.ts";
21
+ import { saveImageAtomically } from "./files.ts";
22
+ /** Image catalog lookup and provider selection. */
23
+
24
+ export const DEFAULT_MODEL = "google/gemini-3.1-flash-image";
25
+
26
+ /** Image models known to Pi: OpenRouter's built-in catalog plus our DashScope registration. */
27
+ export function registryImageModels(ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } }) {
28
+ return ctx.modelRegistry.getModelsOfType("image") as readonly { id: string; name?: string; provider: string }[];
29
+ }
30
+
31
+ /** Resolve which provider owns a model id, preferring the Pi registry over heuristics. */
32
+ export function resolveProviderId(
33
+ ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } },
34
+ modelId: string,
35
+ ): string {
36
+ const lower = modelId.toLowerCase();
37
+ const known = registryImageModels(ctx).find((m) => m.id.toLowerCase() === lower);
38
+ if (known) return known.provider;
39
+ return lower.startsWith("qwen-image") ? "dashscope" : "openrouter";
40
+ }
41
+ import { SearchableImageModelSelectorComponent } from "./model-selector.ts";
42
+
43
+ const DEFAULT_TIMEOUT_MS = 120_000;
44
+
45
+ let currentModel = DEFAULT_MODEL;
46
+
47
+ // ============================ Extension Entrypoint ============================
48
+
49
+ export default function imageGenerate(pi: ExtensionAPI): void {
50
+ ensureCustomProviders(pi);
51
+ // 1. Slash command: /image-generate model [name]
52
+ pi.registerCommand("image-generate", {
53
+ description: "Switch default image generation model (/image-generate model [name])",
54
+ getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
55
+ const trimmed = prefix.trimStart();
56
+ if (!trimmed || "model".startsWith(trimmed)) {
57
+ return [{ value: "model", label: "model", description: "Select image generation model in TUI" }];
58
+ }
59
+ return null;
60
+ },
61
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
62
+ const parts = args.trim().split(/\s+/).filter(Boolean);
63
+ const sub = (parts[0] || "").toLowerCase();
64
+
65
+ const models = await fetchAllAvailableImageModels(registryImageModels(ctx), ctx.signal);
66
+
67
+ // Case: direct model slug provided
68
+ if (sub === "model" && parts[1]) {
69
+ const resolved = resolveImageModel(parts.slice(1).join(" "), models);
70
+ currentModel = resolved;
71
+ ctx.ui.notify(`Default image model set to: ${resolved}`, "info");
72
+ return;
73
+ }
74
+
75
+ // Case: bare command or /image-generate model -> open interactive selector
76
+ if (sub === "model" || parts.length === 0) {
77
+ // Detect login status for each provider
78
+ const loginStatusMap = new Map<string, boolean>();
79
+ for (const m of models) {
80
+ if (!loginStatusMap.has(m.provider)) {
81
+ const key = await ctx.modelRegistry.getApiKeyForProvider(m.provider);
82
+ loginStatusMap.set(
83
+ m.provider,
84
+ Boolean(key) || Boolean(process.env[`${m.provider.toUpperCase()}_API_KEY`]),
85
+ );
86
+ }
87
+ }
88
+
89
+ const chosenModel = await ctx.ui.custom<ImageModelDefinition | undefined>((tui, theme, _kb, done) => {
90
+ return new SearchableImageModelSelectorComponent(
91
+ tui,
92
+ theme,
93
+ "Select Image Generation Model",
94
+ models,
95
+ currentModel,
96
+ (model) => done(model),
97
+ () => done(undefined),
98
+ (provider) => loginStatusMap.get(provider) ?? false,
99
+ );
100
+ });
101
+
102
+ if (!chosenModel) return;
103
+
104
+ currentModel = chosenModel.id;
105
+ const isLogged = loginStatusMap.get(chosenModel.provider);
106
+ const extra = isLogged ? "" : ` (No key detected: please run /login ${chosenModel.provider})`;
107
+ ctx.ui.notify(`Default image model set to: ${chosenModel.name} (${chosenModel.id})${extra}`, "info");
108
+ return;
109
+ }
110
+
111
+ // Unknown arguments: report usage instead of silently doing nothing
112
+ ctx.ui.notify(
113
+ `Unknown argument: ${parts.join(" ")}. Usage: /image-generate [model] [name]`,
114
+ "warning",
115
+ );
116
+ },
117
+ });
118
+
119
+ // 2. Tool: image_generate (English Schema)
120
+ pi.registerTool({
121
+ name: "image_generate",
122
+ description:
123
+ "Generate an image from a text prompt using OpenRouter or DashScope. Saves the generated image to local disk and returns the file path.",
124
+ parameters: Type.Object({
125
+ prompt: Type.String({
126
+ description: "The text prompt describing the image to generate.",
127
+ }),
128
+ model: Type.Optional(
129
+ Type.String({
130
+ description:
131
+ "Optional model slug (e.g. 'google/gemini-3.1-flash-image', 'qwen-image-3.0-pro', 'black-forest-labs/flux.2-pro'). Defaults to current default model.",
132
+ }),
133
+ ),
134
+ aspectRatio: Type.Optional(
135
+ Type.String({
136
+ description: "Optional aspect ratio for the image (e.g., '1:1', '16:9', '9:16', '4:3', '3:4').",
137
+ }),
138
+ ),
139
+ image: Type.Optional(
140
+ Type.String({
141
+ description: "Optional local file path or remote image URL for image-to-image reference.",
142
+ }),
143
+ ),
144
+ }),
145
+ execute: async (_toolCallId, params, signal, ctx) => {
146
+ const targetModel = params.model
147
+ ? resolveImageModel(params.model, registryImageModels(ctx))
148
+ : currentModel;
149
+ const timeoutSig = AbortSignal.any([AbortSignal.timeout(DEFAULT_TIMEOUT_MS), ...(signal ? [signal] : [])]);
150
+
151
+ const providerId = resolveProviderId(ctx, targetModel);
152
+ const apiKey =
153
+ (await ctx.modelRegistry.getApiKeyForProvider(providerId)) ||
154
+ process.env[`${providerId.toUpperCase()}_API_KEY`];
155
+
156
+ if (!apiKey) {
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: `Error: No ${providerId} API key found. Please run '/login ${providerId}' or set ${providerId.toUpperCase()}_API_KEY environment variable.`,
162
+ },
163
+ ],
164
+ isError: true,
165
+ };
166
+ }
167
+
168
+ try {
169
+ const res = await generateImage(
170
+ {
171
+ prompt: params.prompt,
172
+ model: targetModel,
173
+ aspectRatio: params.aspectRatio,
174
+ image: params.image,
175
+ cwd: ctx.cwd,
176
+ },
177
+ { apiKey, provider: providerId, signal: timeoutSig, cwd: ctx.cwd },
178
+ );
179
+
180
+ const savedPath = await saveImageAtomically(ctx.cwd, res.buffer, res.mimeType);
181
+
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: `Image successfully generated using '${res.model}'.\nSaved to: ${savedPath}`,
187
+ },
188
+ ],
189
+ details: {
190
+ model: res.model,
191
+ provider: res.provider,
192
+ savedPath,
193
+ prompt: params.prompt,
194
+ revisedPrompt: res.revisedPrompt,
195
+ },
196
+ };
197
+ } catch (err: any) {
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: `Image generation failed: ${err?.message ?? String(err)}`,
203
+ },
204
+ ],
205
+ isError: true,
206
+ };
207
+ }
208
+ },
209
+ renderCall(args, theme) {
210
+ const { prompt, model } = args as { prompt?: string; model?: string };
211
+ const displayModel = model || currentModel;
212
+ const preview = prompt ? (prompt.length > 50 ? prompt.slice(0, 47) + "..." : prompt) : "";
213
+ return new Text(
214
+ theme.fg("toolTitle", theme.bold("generate_image ")) +
215
+ theme.fg("dim", `[${displayModel}] `) +
216
+ theme.fg("accent", `"${preview}"`),
217
+ );
218
+ },
219
+ });
220
+ }
@@ -0,0 +1,150 @@
1
+ import { Container, Input, Spacer, Text, fuzzyFilter, getKeybindings } from "@earendil-works/pi-tui";
2
+ import type { ImageModelDefinition } from "../custom-providers/index.ts";
3
+
4
+ export class SearchableImageModelSelectorComponent extends Container {
5
+ private searchInput: Input;
6
+ private listContainer: Container;
7
+ private selectedIndex = 0;
8
+ private filteredModels: ImageModelDefinition[] = [];
9
+ private _focused = true;
10
+
11
+ get focused() {
12
+ return this._focused;
13
+ }
14
+ set focused(value: boolean) {
15
+ this._focused = value;
16
+ this.searchInput.focused = value;
17
+ }
18
+
19
+ constructor(
20
+ private tui: any,
21
+ private theme: any,
22
+ private title: string,
23
+ private models: ImageModelDefinition[],
24
+ private currentModelId: string,
25
+ private onSelect: (model: ImageModelDefinition) => void,
26
+ private onCancel: () => void,
27
+ private getLoginStatus?: (provider: string) => boolean,
28
+ ) {
29
+ super();
30
+ this.filteredModels = [...models];
31
+ const curIdx = models.findIndex((m) => m.id === currentModelId);
32
+ this.selectedIndex = curIdx >= 0 ? curIdx : 0;
33
+
34
+ this.searchInput = new Input();
35
+ this.searchInput.focused = true;
36
+ this.listContainer = new Container();
37
+
38
+ this.updateList();
39
+ }
40
+
41
+ filterModels(query: string) {
42
+ const q = query.trim().toLowerCase();
43
+ if (q) {
44
+ this.filteredModels = fuzzyFilter(
45
+ this.models,
46
+ q,
47
+ (m) => `${m.id} ${m.name} ${m.provider} ${m.description}`,
48
+ );
49
+ } else {
50
+ this.filteredModels = [...this.models];
51
+ }
52
+ this.selectedIndex = q ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
53
+ this.updateList();
54
+ this.tui.requestRender?.();
55
+ }
56
+
57
+ updateList() {
58
+ this.listContainer.clear();
59
+ const maxVisible = 6;
60
+ const total = this.filteredModels.length;
61
+
62
+ if (total === 0) {
63
+ this.listContainer.addChild(new Text(this.theme.fg("muted", " No matching models"), 0, 0));
64
+ return;
65
+ }
66
+
67
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
68
+ const endIndex = Math.min(startIndex + maxVisible, total);
69
+
70
+ for (let i = startIndex; i < endIndex; i++) {
71
+ const item = this.filteredModels[i];
72
+ const isSelected = i === this.selectedIndex;
73
+ const isCurrent = item.id === this.currentModelId;
74
+
75
+ const cursor = isSelected ? this.theme.fg("accent", "→ ") : " ";
76
+ const check = isCurrent ? this.theme.fg("accent", "✓ ") : " ";
77
+ const modelText = isSelected
78
+ ? this.theme.fg("accent", this.theme.bold ? this.theme.bold(item.id) : item.id)
79
+ : this.theme.fg("text", item.id);
80
+ const providerBadge = this.theme.fg("muted", `[${item.provider}]`);
81
+
82
+ let authBadge = "";
83
+ if (this.getLoginStatus) {
84
+ const loggedIn = this.getLoginStatus(item.provider);
85
+ authBadge = loggedIn
86
+ ? this.theme.fg("dim", " · logged in")
87
+ : this.theme.fg("warning", ` · not logged in: /login ${item.provider}`);
88
+ }
89
+
90
+ const line = `${cursor}${check}${modelText} ${providerBadge}${authBadge}`;
91
+ this.listContainer.addChild(new Text(line, 0, 0));
92
+ }
93
+
94
+ if (startIndex > 0 || endIndex < total) {
95
+ const scrollInfo = this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`);
96
+ this.listContainer.addChild(new Text(scrollInfo, 0, 0));
97
+ }
98
+
99
+ const selected = this.filteredModels[this.selectedIndex];
100
+ if (selected) {
101
+ this.listContainer.addChild(new Spacer(1));
102
+ this.listContainer.addChild(
103
+ new Text(this.theme.fg("muted", ` Model: ${selected.name} — ${selected.description}`), 0, 0),
104
+ );
105
+ }
106
+ }
107
+
108
+ render(width: number): string[] {
109
+ this.clear();
110
+ const border = this.theme.fg("accent", "─".repeat(Math.max(10, width)));
111
+
112
+ this.addChild(new Text(border, 0, 0));
113
+ this.addChild(new Spacer(1));
114
+ this.addChild(new Text(this.theme.fg("accent", this.theme.bold ? this.theme.bold(` ${this.title}`) : ` ${this.title}`), 0, 0));
115
+ this.addChild(new Spacer(1));
116
+ this.addChild(this.searchInput);
117
+ this.addChild(new Spacer(1));
118
+ this.addChild(this.listContainer);
119
+ this.addChild(new Spacer(1));
120
+ this.addChild(new Text(this.theme.fg("dim", " Enter to select · Escape to cancel"), 0, 0));
121
+ this.addChild(new Text(border, 0, 0));
122
+
123
+ return super.render(width);
124
+ }
125
+
126
+ handleInput(keyData: string) {
127
+ const kb = getKeybindings();
128
+ if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
129
+ if (this.filteredModels.length === 0) return;
130
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
131
+ this.updateList();
132
+ this.tui.requestRender?.();
133
+ } else if (kb.matches(keyData, "tui.select.down") || keyData === "j") {
134
+ if (this.filteredModels.length === 0) return;
135
+ this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
136
+ this.updateList();
137
+ this.tui.requestRender?.();
138
+ } else if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n" || keyData === "\r") {
139
+ const selected = this.filteredModels[this.selectedIndex];
140
+ if (selected) {
141
+ this.onSelect(selected);
142
+ }
143
+ } else if (kb.matches(keyData, "tui.select.cancel") || keyData === "\x1b" || keyData === "\x03") {
144
+ this.onCancel();
145
+ } else {
146
+ this.searchInput.handleInput(keyData);
147
+ this.filterModels(this.searchInput.getValue());
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,54 @@
1
+ # memory-log
2
+
3
+ Observational memory for long Pi sessions: background observers distill conversation chunks, a ledger records observations, and a consolidator writes durable project memory. Compaction rebuilds context from the recorded memory and recent conversation.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/memory-log
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. The extension loads with new-session memory disabled by default.
12
+
13
+ ## Commands
14
+
15
+ | Command | Action |
16
+ |---|---|
17
+ | `/mem-log on` / `/mem-log off` | Set the current session's memory gate |
18
+ | `/mem-log status` or `/mem-log:status` | Inspect observation progress, workers, memory, and costs |
19
+ | `/mem-log compact` or `/mem-log:compact` | Request compaction |
20
+ | `/mem-log consolidate` or `/mem-log:consolidate` | Consolidate the observation pool |
21
+
22
+ ## Settings
23
+
24
+ Add `memoryLog` to personal or project Pi settings. Project values override personal values:
25
+
26
+ ```json
27
+ {
28
+ "memoryLog": {
29
+ "defaultEnabled": false,
30
+ "chunkTokens": 10000,
31
+ "poolTargetTokens": 10000,
32
+ "consolidateAtPoolTokens": 15000,
33
+ "compactAtContextTokens": 150000,
34
+ "tailTokens": 20000,
35
+ "journeyTargetTokens": 1000,
36
+ "observerConcurrency": 4,
37
+ "resumeAfterMidRunCompaction": true,
38
+ "passive": false,
39
+ "debugLog": false,
40
+ "models": {
41
+ "observer": { "thinking": "low" },
42
+ "consolidator": { "thinking": "medium" }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ Observer and consolidator model objects accept `provider`, `id`, and `thinking`. An explicitly configured model takes priority over the session model, followed by Pi's configured default model. `chunkOverlapTokens` defaults to zero.
49
+
50
+ Environment overrides are `PI_OM_PASSIVE`, `PI_OM_OBSERVER_PROVIDER`, `PI_OM_OBSERVER_MODEL`, `PI_OM_OBSERVER_THINKING`, and the corresponding `PI_OM_CONSOLIDATOR_*` values. Passive mode disables automatic triggers separately from the session gate.
51
+
52
+ ## Storage and requirements
53
+
54
+ Durable files live under `.memory/<session-id>/` in the working project, including topic files and `JOURNEY.md`. Gate changes and observation history are recorded in the Pi session ledger. Observer and consolidator workers require the `pi` executable and model credentials; they make additional model requests. Disabling memory aborts active background workers.
@@ -0,0 +1,47 @@
1
+ export const CONSOLIDATOR_SYSTEM = `You are the consolidation agent for a coding assistant's long-term memory.
2
+
3
+ Your job: take a batch of older observations (timestamped facts distilled from earlier conversation) and fold them into durable topic files under .memory/. These topic files are the assistant's permanent, cross-session memory of this project. The observations you are given are about to be deleted from the short-term buffer, so anything worth keeping that you fail to record here is forgotten forever.
4
+
5
+ You operate entirely on .memory/. You have scoped tools: read, write, edit, ls, grep — all confined to the .memory/ directory. You CANNOT touch anything outside .memory/. Do NOT create or edit INDEX.md; it is generated automatically from your topic files' front-matter — your job is the <topic>.md files plus JOURNEY.md (described below).
6
+
7
+ How you work:
8
+ 1. Run ls to see existing topic files, and read the ones relevant to the incoming observations.
9
+ 2. For each incoming observation, decide where it belongs: an existing topic file, or a new one.
10
+ 3. Write/edit topic files so each holds clean, current-state prose about its topic.
11
+ 4. Update JOURNEY.md (see below) with a short segment covering this batch.
12
+ 5. When every incoming observation has been folded in (or deliberately discarded as low-value/noise), emit a one-sentence confirmation and stop.
13
+
14
+ Topic routing (start conservative — prefer fewer, larger topics; split only when a file clearly covers two unrelated subjects):
15
+ - Create a topic when the observations introduce a genuinely new subject with no existing home.
16
+ - Merge into an existing topic when the observations extend or update it.
17
+ - Split a topic only when it has grown to cover clearly distinct subjects.
18
+
19
+ Writing topic files:
20
+ - Write current-state prose, not a changelog. If an observation supersedes an existing fact, REWRITE the file to reflect the new truth and delete the obsolete statement. Do not leave "was X, now Y" cruft or tombstones.
21
+ - Preserve distinguishing detail: file paths, identifiers, package/function names, error codes, exact numbers, the user's own terminology (quote unusual terms verbatim).
22
+ - Keep prose tight and skimmable. Headings and short paragraphs or bullet lists are fine. This is reference material the assistant will read later.
23
+ - Preserve the authoritative/assertion vs question distinction the observations carry. User assertions are authoritative.
24
+
25
+ JOURNEY.md — the running project history (orientation, not a topic file):
26
+ - Purpose: ONE brief, free-form narrative of how this project/work reached its current state, so a future reader can orient to the rough arc of how we got here. Its current contents are provided in your prompt; you rewrite the whole file with the write tool. It has NO front-matter and is not a topic file.
27
+ - STRICTLY DESCRIPTIVE. Write only what happened, in the past tense. Do NOT include recommendations, next steps, TODOs, plans, advice, warnings, predictions, open questions framed as tasks, or evaluative judgement. No "should", "needs to", "the goal is", "next we". If you catch yourself steering future behaviour, delete that sentence. It exists purely to orient, never to instruct.
28
+ - Keep it ROUGH and high-level: the shape of the journey, not a detailed log. Topic files already hold the details.
29
+ - APPEND-MOSTLY: add one short dated segment (2-5 sentences) describing the arc that THIS batch of observations represents, using a '## <date>' heading and the current time from your prompt. Leave existing recent segments intact — do not rewrite them.
30
+ - THIS BATCH IS NOT THE END OF THE SESSION. The session was still running when you were invoked — these observations are an early or mid-session slice; newer conversation exists beyond this batch that has not been consolidated yet. Never write as if this batch edge is the current moment. Forbidden phrases: "by session end", "at the end of the session", "the session concluded", "work remaining", or anything framed as the present state. Use past-arc language instead: "during this period", "by this point", "at this stage of the session".
31
+ - COMPRESS THE OLD TAIL ONLY WHEN OVER SIZE: if the file would exceed the token budget given in your prompt, condense the OLDEST segments into a tighter summary at the top, preserving the most recent segments in more detail. Recent history stays detailed; the distant past gets condensed. Never grow the file unbounded.
32
+ - Order chronologically, oldest first (a compressed early-history summary may lead).
33
+
34
+ Front-matter (REQUIRED at the top of every topic file you write):
35
+ ---
36
+ id: <stable-slug> # matches the filename without .md, e.g. "auth" for auth.md
37
+ title: <short human title>
38
+ summary: <one line, <= 140 chars; what this file covers — this is what the assistant sees in the index>
39
+ updated: <the current date/time provided in your prompt>
40
+ ---
41
+ Maintain these fields whenever you write a file. The summary is load-bearing: it is the ONLY thing the assistant sees about this file until it opens it, so make it specific.
42
+
43
+ Filenames: lowercase kebab-case slugs ending in .md (e.g. auth.md, deploy-pipeline.md, user-preferences.md). The id must equal the filename without .md.
44
+
45
+ Completion:
46
+ - When done, emit a one-sentence plain-text confirmation and stop. The run ends on its own.
47
+ - The whole incoming batch leaves the short-term buffer once you finish, whether you filed it or judged it not worth keeping — you do not report back. So make sure everything worth keeping has been written to a file before you stop. Discarding clear noise is fine and expected; dropping a genuine fact you meant to keep is the failure to avoid.`;