@luxass/pi-personal 0.0.0 → 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.
package/README.md CHANGED
@@ -10,6 +10,8 @@ pi install npm:@luxass/pi-personal
10
10
 
11
11
  ## What it does
12
12
 
13
- The package registers a `session_start` hook. When a session starts, Pi shows a notification confirming the extension loaded.
13
+ The custom header pairs an ASCII terminal-robot mascot with the project, model, and current thinking level. A rounded frame, vertical padding, and a separate shortcut row give it room on wide terminals. Narrow panes use a compact layout and drop shortcut hints before truncating them. The thinking shortcut follows your Pi keybindings.
14
14
 
15
- That is the whole of it today. The package is a home for defaults that suit one workflow, and each addition is meant to be edited in place as that workflow changes rather than configured through settings.
15
+ Run `/resources` to inspect detected context files, skill commands, and extensions. This list is inferred from files and registered commands/tools, not a complete inventory of loaded resources.
16
+
17
+ The package also includes Crow provider support, compaction continuation, Git and package-manager interceptors, and a worker configuration guard. These defaults suit one workflow and are meant to be edited in place rather than configured through settings.
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@luxass/pi-personal",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "Personal Pi extensions collected in one package, tuned to a single workflow.",
5
5
  "keywords": [
6
6
  "personal",
7
7
  "pi",
8
- "pi-extension",
9
8
  "pi-package"
10
9
  ],
11
10
  "license": "MIT",
@@ -31,7 +30,12 @@
31
30
  "pi": {
32
31
  "image": "https://raw.githubusercontent.com/luxass/pi-extensions/main/packages/personal/docs/cover.png",
33
32
  "extensions": [
34
- "./src/index.ts"
33
+ "./src/continue-after-compaction.ts",
34
+ "./src/git-interceptor.ts",
35
+ "./src/package-manager-interceptor.ts",
36
+ "./src/worker-configuration-guard.ts",
37
+ "./src/custom-header.ts",
38
+ "./src/crow/index.ts"
35
39
  ]
36
40
  },
37
41
  "scripts": {
@@ -0,0 +1,59 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ const buildContinuationPrompt = (
4
+ sessionFile: string | undefined,
5
+ compactionEntryId: string,
6
+ ): string => {
7
+ const sessionSource =
8
+ sessionFile === undefined
9
+ ? "This session is ephemeral, so no persisted session file is available."
10
+ : [
11
+ `The persisted session JSONL is ${JSON.stringify(sessionFile)}.`,
12
+ "Inspect it directly with the read and bash tools.",
13
+ "Do not launch a nested Pi process or open the session with `pi --session`.",
14
+ ].join(" ");
15
+
16
+ return `Compaction has just completed. Resume the existing task rather than waiting for another user prompt.
17
+
18
+ ${sessionSource}
19
+ The new compaction entry ID is ${JSON.stringify(compactionEntryId)}.
20
+
21
+ Before continuing:
22
+
23
+ 1. Review the active session branch leading to the compaction entry. Focus first on messages and tool calls immediately before compaction, searching earlier history only as needed. Remember that JSONL append order can include abandoned branches, so follow parentId links rather than blindly treating every entry as active.
24
+ 2. Reconstruct the original goal, user constraints, decisions made, files changed, commands and tests run, unresolved issues, and intended next action.
25
+ 3. Reconcile the recovered history with the compaction summary and current repository state. Treat the current worktree as authoritative for file state and the original session history as authoritative for user intent.
26
+ 4. Briefly state the context you recovered.
27
+ 5. Immediately perform the next unfinished step. Do not stop after the recap and do not ask the user to repeat prior context unless the session data is genuinely unavailable or ambiguous.`;
28
+ };
29
+
30
+ /**
31
+ * Automatically resumes work after every successful Pi compaction.
32
+ *
33
+ * The continuation is deferred by one event-loop turn so manual compaction can
34
+ * finish reconnecting the agent runtime before a new prompt begins. During an
35
+ * active automatic-compaction recovery, it is delivered as steering so the
36
+ * continuation is read before the retried agent performs more work.
37
+ */
38
+ export default function continueAfterCompaction(pi: ExtensionAPI): void {
39
+ const pendingTimers = new Set<ReturnType<typeof setTimeout>>();
40
+
41
+ pi.on("session_compact", (event, ctx) => {
42
+ const sessionFile = ctx.sessionManager.getSessionFile();
43
+ const prompt = buildContinuationPrompt(sessionFile, event.compactionEntry.id);
44
+
45
+ const timer = setTimeout(() => {
46
+ pendingTimers.delete(timer);
47
+ pi.sendUserMessage(prompt, { deliverAs: "steer" });
48
+ }, 0);
49
+
50
+ pendingTimers.add(timer);
51
+ });
52
+
53
+ pi.on("session_shutdown", () => {
54
+ for (const timer of pendingTimers) {
55
+ clearTimeout(timer);
56
+ }
57
+ pendingTimers.clear();
58
+ });
59
+ }
@@ -0,0 +1,318 @@
1
+ import {
2
+ type Api,
3
+ type AssistantMessage,
4
+ createAssistantMessageEventStream,
5
+ type Model,
6
+ } from "@earendil-works/pi-ai";
7
+ import { type ExtensionAPI, SettingsManager } from "@earendil-works/pi-coding-agent";
8
+
9
+ import type { CliproxyCodexStreamSimple, CloseCodexWebSocketSessions } from "./codex-stream.ts";
10
+
11
+ export const PROACTIVE_COMPACTION_ERROR_PREFIX =
12
+ "context_length_exceeded: proactive compaction threshold reached";
13
+
14
+ export interface ProactiveCompactionSettings {
15
+ enabled: boolean;
16
+ reserveTokens: number;
17
+ }
18
+
19
+ export type { CloseCodexWebSocketSessions };
20
+
21
+ const DEFAULT_COMPACTION_ENABLED = true;
22
+ const DEFAULT_COMPACTION_RESERVE_TOKENS = 16384;
23
+
24
+ interface CompatibleSettingsManager {
25
+ reload?: () => Promise<void> | void;
26
+ reloadFromDisk?: () => Promise<void> | void;
27
+ getCompactionSettings?: () => { enabled?: unknown; reserveTokens?: unknown };
28
+ get?: (key: string) => unknown;
29
+ }
30
+
31
+ function isCompatibleSettingsManager(value: unknown): value is CompatibleSettingsManager {
32
+ return typeof value === "object" && value !== null;
33
+ }
34
+
35
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
36
+ return (
37
+ ((typeof value === "object" && value !== null) || typeof value === "function") &&
38
+ typeof (value as { then?: unknown }).then === "function"
39
+ );
40
+ }
41
+
42
+ function normalizeCompactionSettings(
43
+ enabled: unknown,
44
+ reserveTokens: unknown,
45
+ ): ProactiveCompactionSettings {
46
+ return {
47
+ enabled: typeof enabled === "boolean" ? enabled : DEFAULT_COMPACTION_ENABLED,
48
+ reserveTokens:
49
+ typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0
50
+ ? reserveTokens
51
+ : DEFAULT_COMPACTION_RESERVE_TOKENS,
52
+ };
53
+ }
54
+
55
+ function readCompactionSettings(
56
+ manager: CompatibleSettingsManager,
57
+ ): ProactiveCompactionSettings | undefined {
58
+ if (typeof manager.getCompactionSettings === "function") {
59
+ try {
60
+ const settings = manager.getCompactionSettings();
61
+ return normalizeCompactionSettings(settings?.enabled, settings?.reserveTokens);
62
+ } catch {
63
+ // Fall through to OMP's key-based settings API when available.
64
+ }
65
+ }
66
+ if (typeof manager.get === "function") {
67
+ try {
68
+ return normalizeCompactionSettings(
69
+ manager.get("compaction.enabled"),
70
+ manager.get("compaction.reserveTokens"),
71
+ );
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ async function reloadSettings(manager: CompatibleSettingsManager): Promise<void> {
80
+ if (typeof manager.reload === "function") {
81
+ try {
82
+ await manager.reload();
83
+ } catch {
84
+ // Keep the last successfully read settings when disk reload fails.
85
+ }
86
+ return;
87
+ }
88
+ if (typeof manager.reloadFromDisk === "function") {
89
+ try {
90
+ await manager.reloadFromDisk();
91
+ } catch {
92
+ // Keep the last successfully read settings when disk reload fails.
93
+ }
94
+ }
95
+ }
96
+
97
+ /** Session id used to key pi-ai's reused Codex WebSocket. */
98
+ export function resolveCompactionSessionId(source?: {
99
+ sessionId?: unknown;
100
+ sessionManager?: { getSessionId?: () => unknown };
101
+ }): string | undefined {
102
+ const fromManager = source?.sessionManager?.getSessionId?.();
103
+ if (typeof fromManager === "string" && fromManager.trim()) {
104
+ return fromManager;
105
+ }
106
+ if (typeof source?.sessionId === "string" && source.sessionId.trim()) {
107
+ return source.sessionId;
108
+ }
109
+ return undefined;
110
+ }
111
+
112
+ export function shouldScheduleProactiveCompaction(
113
+ message: AssistantMessage,
114
+ contextTokens: number,
115
+ contextWindow: number,
116
+ settings: ProactiveCompactionSettings,
117
+ ): boolean {
118
+ if (!settings.enabled || message.stopReason !== "toolUse") {
119
+ return false;
120
+ }
121
+ if (!message.content.some((block) => block.type === "toolCall")) {
122
+ return false;
123
+ }
124
+ if (!Number.isFinite(contextTokens) || contextTokens <= 0) {
125
+ return false;
126
+ }
127
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
128
+ return false;
129
+ }
130
+ if (!Number.isFinite(settings.reserveTokens) || settings.reserveTokens < 0) {
131
+ return false;
132
+ }
133
+
134
+ return contextTokens > contextWindow - settings.reserveTokens;
135
+ }
136
+
137
+ export function createProactiveCompactionStream(
138
+ model: Model<Api>,
139
+ contextTokens: number,
140
+ threshold: number,
141
+ ) {
142
+ const stream = createAssistantMessageEventStream();
143
+ const output: AssistantMessage = {
144
+ role: "assistant",
145
+ content: [],
146
+ api: model.api,
147
+ provider: model.provider,
148
+ model: model.id,
149
+ usage: {
150
+ input: 0,
151
+ output: 0,
152
+ cacheRead: 0,
153
+ cacheWrite: 0,
154
+ totalTokens: 0,
155
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
156
+ },
157
+ stopReason: "error",
158
+ errorMessage: `${PROACTIVE_COMPACTION_ERROR_PREFIX} (${contextTokens} > ${threshold})`,
159
+ timestamp: Date.now(),
160
+ };
161
+
162
+ queueMicrotask(() => {
163
+ stream.push({ type: "start", partial: output });
164
+ stream.push({ type: "error", reason: "error", error: output });
165
+ stream.end();
166
+ });
167
+
168
+ return stream;
169
+ }
170
+
171
+ export class ProactiveCompactionController {
172
+ private settingsManager: CompatibleSettingsManager | undefined;
173
+ private cachedCompactionSettings: ProactiveCompactionSettings | undefined;
174
+ private pending: { modelKey: string; contextTokens: number; threshold: number } | undefined;
175
+ private closeWebSocketSessions: CloseCodexWebSocketSessions | undefined;
176
+
177
+ constructor(
178
+ private readonly agentDir: string,
179
+ private readonly providerId: string,
180
+ closeWebSocketSessions?: CloseCodexWebSocketSessions,
181
+ ) {
182
+ this.closeWebSocketSessions = closeWebSocketSessions;
183
+ }
184
+
185
+ setCloseWebSocketSessions(closeWebSocketSessions: CloseCodexWebSocketSessions): void {
186
+ this.closeWebSocketSessions = closeWebSocketSessions;
187
+ }
188
+
189
+ register(pi: ExtensionAPI): void {
190
+ pi.on("session_start", async (_event, ctx) => {
191
+ this.cachedCompactionSettings = undefined;
192
+ try {
193
+ const created: unknown = SettingsManager.create(ctx.cwd, this.agentDir, {
194
+ projectTrusted: ctx.isProjectTrusted(),
195
+ });
196
+ const manager = isPromiseLike(created) ? await created : created;
197
+ this.settingsManager = isCompatibleSettingsManager(manager) ? manager : undefined;
198
+ this.cachedCompactionSettings = this.settingsManager
199
+ ? readCompactionSettings(this.settingsManager)
200
+ : undefined;
201
+ } catch {
202
+ this.settingsManager = undefined;
203
+ }
204
+ this.pending = undefined;
205
+ });
206
+
207
+ pi.on("session_shutdown", () => {
208
+ this.settingsManager = undefined;
209
+ this.cachedCompactionSettings = undefined;
210
+ this.pending = undefined;
211
+ });
212
+
213
+ pi.on("session_before_compact", () => {
214
+ // Compaction summaries use the same provider stream. Clear the synthetic
215
+ // overflow before pi invokes the summarizer so it cannot abort compaction.
216
+ this.pending = undefined;
217
+ });
218
+
219
+ pi.on("session_compact", (_event, ctx) => {
220
+ this.pending = undefined;
221
+ // CLIProxyAPI binds server-side Codex context to the WebSocket. Compaction
222
+ // only rewrites the client message list, so reuse would keep cacheRead high
223
+ // and retrigger proactive compaction on a now-small session.
224
+ this.resetWebSocketSession(resolveCompactionSessionId(ctx));
225
+ });
226
+
227
+ pi.on("agent_settled", () => {
228
+ this.pending = undefined;
229
+ });
230
+
231
+ pi.on("turn_end", async (event, ctx) => {
232
+ const message = event.message;
233
+ if (message.role !== "assistant" || message.provider !== this.providerId) {
234
+ return;
235
+ }
236
+ if (!ctx.model || ctx.model.provider !== this.providerId || ctx.model.id !== message.model) {
237
+ return;
238
+ }
239
+
240
+ const settingsManager = this.settingsManager;
241
+ if (!settingsManager) {
242
+ return;
243
+ }
244
+ await reloadSettings(settingsManager);
245
+ const refreshedSettings = readCompactionSettings(settingsManager);
246
+ if (refreshedSettings) {
247
+ this.cachedCompactionSettings = refreshedSettings;
248
+ }
249
+ const settings = refreshedSettings ?? this.cachedCompactionSettings;
250
+ if (!settings) {
251
+ return;
252
+ }
253
+ const contextTokens = ctx.getContextUsage()?.tokens;
254
+ if (contextTokens === null || contextTokens === undefined) {
255
+ return;
256
+ }
257
+ if (
258
+ !shouldScheduleProactiveCompaction(
259
+ message,
260
+ contextTokens,
261
+ ctx.model.contextWindow,
262
+ settings,
263
+ )
264
+ ) {
265
+ return;
266
+ }
267
+
268
+ this.pending = {
269
+ modelKey: this.modelKey(ctx.model),
270
+ contextTokens,
271
+ threshold: ctx.model.contextWindow - settings.reserveTokens,
272
+ };
273
+ });
274
+ }
275
+
276
+ getCompactionSettings(): ProactiveCompactionSettings | undefined {
277
+ const current = this.settingsManager ? readCompactionSettings(this.settingsManager) : undefined;
278
+ if (current) {
279
+ this.cachedCompactionSettings = current;
280
+ }
281
+ return current ?? this.cachedCompactionSettings;
282
+ }
283
+
284
+ wrapStreamSimple(streamSimple: CliproxyCodexStreamSimple): CliproxyCodexStreamSimple {
285
+ return (model, context, options) => {
286
+ const pending = this.pending;
287
+ if (!pending || pending.modelKey !== this.modelKey(model)) {
288
+ return streamSimple(model, context, options);
289
+ }
290
+
291
+ this.pending = undefined;
292
+ this.resetWebSocketSession(options?.sessionId);
293
+ return createProactiveCompactionStream(model, pending.contextTokens, pending.threshold);
294
+ };
295
+ }
296
+
297
+ private resetWebSocketSession(sessionId?: string): void {
298
+ if (!sessionId || !this.closeWebSocketSessions) {
299
+ return;
300
+ }
301
+ try {
302
+ this.closeWebSocketSessions(sessionId);
303
+ } catch (error) {
304
+ const message = error instanceof Error ? error.message : String(error);
305
+ process.stderr.write(
306
+ `${JSON.stringify({
307
+ source: "pi-cliproxyapi-provider",
308
+ level: "warning",
309
+ message: `Failed to close Codex WebSocket for session ${sessionId}: ${message}`,
310
+ })}\n`,
311
+ );
312
+ }
313
+ }
314
+
315
+ private modelKey(model: Pick<Model<Api>, "provider" | "id">): string {
316
+ return `${model.provider}/${model.id}`;
317
+ }
318
+ }