@nklisch/pi-enhanced 0.3.1 → 0.4.2

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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -0
  3. package/node_modules/@nklisch/pi-astral-pocket/LICENSE +3 -0
  4. package/node_modules/@nklisch/pi-astral-pocket/README.md +138 -0
  5. package/node_modules/@nklisch/pi-astral-pocket/package.json +54 -0
  6. package/node_modules/@nklisch/pi-astral-pocket/src/activation.ts +44 -0
  7. package/node_modules/@nklisch/pi-astral-pocket/src/config.ts +95 -0
  8. package/node_modules/@nklisch/pi-astral-pocket/src/controller.ts +78 -0
  9. package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +271 -0
  10. package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +66 -0
  11. package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +222 -0
  12. package/node_modules/@nklisch/pi-astral-pocket/src/provider.ts +128 -0
  13. package/node_modules/@nklisch/pi-astral-pocket/src/scope.ts +33 -0
  14. package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +237 -0
  15. package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +427 -0
  16. package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +142 -0
  17. package/node_modules/@nklisch/pi-clearance/native/clearance-core.darwin-arm64.node +0 -0
  18. package/node_modules/@nklisch/pi-clearance/native/clearance-core.darwin-x64.node +0 -0
  19. package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-arm64-gnu.node +0 -0
  20. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
  21. package/node_modules/@nklisch/pi-conveniences/extensions/agents-context.ts +4 -6
  22. package/node_modules/@nklisch/pi-conveniences/extensions/context-window-footer.ts +38 -22
  23. package/node_modules/@nklisch/pi-conveniences/package.json +1 -1
  24. package/node_modules/@nklisch/pi-plugins/README.md +12 -6
  25. package/node_modules/@nklisch/pi-plugins/dist/catalog.js +11 -0
  26. package/node_modules/@nklisch/pi-plugins/dist/catalog.js.map +1 -1
  27. package/node_modules/@nklisch/pi-plugins/dist/host.js +85 -29
  28. package/node_modules/@nklisch/pi-plugins/dist/host.js.map +1 -1
  29. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.d.ts +4 -0
  30. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.js +24 -0
  31. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.js.map +1 -0
  32. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +6 -0
  33. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +130 -47
  34. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
  35. package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.d.ts +9 -0
  36. package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.js +37 -0
  37. package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.js.map +1 -0
  38. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +2 -0
  39. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
  40. package/node_modules/@nklisch/pi-plugins/dist/types.d.ts +4 -1
  41. package/node_modules/@nklisch/pi-plugins/dist/types.js.map +1 -1
  42. package/node_modules/@nklisch/pi-plugins/package.json +1 -1
  43. package/package.json +4 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.1 — 2026-09-04
4
+
5
+ ### Added
6
+
7
+ - Include `@nklisch/pi-astral-pocket` 0.1.0, the Astra-only persistent note and recall extension.
8
+
9
+ ### Fixed
10
+
11
+ - Rebundle `@nklisch/pi-plugins` 0.8.2 and `@nklisch/pi-conveniences` 0.1.3 with accurate plugin versions, a framed manager, safer updates, and workspace-correct project instructions.
12
+
3
13
  ## v0.3.1
4
14
 
5
15
  ### Fixed
package/README.md CHANGED
@@ -25,6 +25,7 @@ install commands, or versions to coordinate.
25
25
  | pi-background-tasks | Background jobs with polling, status, cancellation, and wakeups. |
26
26
  | pi-model-modes | System-prompt adaptation per model and mode. |
27
27
  | pi-conveniences | `/exit`, `.agents/AGENTS.md` context loading, context-window footer, subagent model listing. |
28
+ | pi-astral-pocket | Persistent note pocket and past-session recall, active only for gpt-6-astra. |
28
29
  | pi-fff-compat | FFF-indexed file search through Pi-native find/grep semantics (no fuzzy fallback). |
29
30
 
30
31
  (`@nklisch/pi-zai-research` remains published separately if you prefer its
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nathan Klisch
@@ -0,0 +1,138 @@
1
+ # @nklisch/pi-astral-pocket
2
+
3
+ Astral Pocket keeps durable Markdown notes across Pi sessions. It activates only
4
+ when the current model is `openai-codex/gpt-6-astra`.
5
+
6
+ Notes normally belong to the current Git repository. Subdirectories and linked
7
+ worktrees share the same local repository identity. Explicit global notes can
8
+ carry a general preference or a conditional lesson between repositories.
9
+ Foreign repository memories are not injected into the current session.
10
+
11
+ ## Notes and digest
12
+
13
+ The extension adds two tools while Astra is active:
14
+
15
+ - `pocket_note` saves a durable note. Its default scope is `project`. The caller
16
+ may set `scope: "global"` only for an intentionally portable preference or
17
+ observation.
18
+ - `pocket_recall` searches current-project and global notes, plus past Astra
19
+ sessions from the current repository. `full: true` returns larger excerpts.
20
+ `scope: "all"` deliberately includes other repositories and labels their
21
+ results as cross-repository precedent. Recall returns at most 20 matches from
22
+ each selected source.
23
+
24
+ Distillation means extracting durable decisions, constraints, preferences, and
25
+ pitfalls from an idle session into a source-linked note. Automatic extraction
26
+ always stays project-scoped. A digest is then rebuilt from the actual notes for
27
+ the current repository. Explicit global notes have a separate, smaller digest.
28
+ The extension never builds a new digest by recursively summarizing an old one.
29
+
30
+ Memory is historical evidence, not an instruction source. The current user
31
+ request and current repository guidance take priority. Verify remembered facts
32
+ when their relevance or freshness is uncertain.
33
+
34
+ ## Commands
35
+
36
+ ```text
37
+ /pocket status
38
+ /pocket on
39
+ /pocket off
40
+ /pocket distiller on
41
+ /pocket distiller off
42
+ /pocket model openai-codex/gpt-6-astra
43
+ /pocket model reset
44
+ /pocket reasoning minimal
45
+ /pocket reasoning off|minimal|low|medium|high|xhigh|max
46
+ /pocket reasoning reset
47
+ /pocket distill
48
+ /pocket rebuild
49
+ ```
50
+
51
+ `/pocket status` shows the requested and resolved model, requested and effective
52
+ reasoning, whether distillation is enabled, and the last pass outcome. Pi may
53
+ map a requested reasoning level to a model-supported effort; status reports that
54
+ mapping.
55
+
56
+ `/pocket distill` retries changed or previously failed session work and stale
57
+ digests. `/pocket rebuild` also forces the current project and global digest
58
+ caches to be regenerated. Both commands require an active Astra session and an
59
+ enabled distiller.
60
+
61
+ Changing the model or reasoning setting cancels the current pass before starting
62
+ a replacement. Switching away from Astra, disabling the pocket, reloading, or
63
+ shutting down cancels the session-owned pass.
64
+
65
+ ## Configuration
66
+
67
+ Configuration lives in `config.json`. All fields are optional. These are the
68
+ defaults:
69
+
70
+ ```json
71
+ {
72
+ "enabled": true,
73
+ "distiller": {
74
+ "enabled": true,
75
+ "model": "openai-codex/gpt-6-astra",
76
+ "reasoning": "minimal",
77
+ "minIdleHours": 6,
78
+ "maxSessionsPerPass": 16,
79
+ "maxSessionAgeDays": 30
80
+ }
81
+ }
82
+ ```
83
+
84
+ The model must use the exact `provider/modelId` form. Astral Pocket asks Pi's
85
+ model registry for current authentication and headers on every request. It does
86
+ not silently select another provider when that model is unavailable. A malformed
87
+ configuration falls back to defaults without blocking note access.
88
+
89
+ ## Storage and recovery
90
+
91
+ Files are stored under `~/.pi/agent/astral-pocket/`, or under
92
+ `$PI_CODING_AGENT_DIR/astral-pocket/` when that variable is set:
93
+
94
+ ```text
95
+ config.json settings
96
+ notes/ canonical Markdown notes
97
+ digests/ rebuildable per-project and global digest caches
98
+ POCKET.md rebuildable note registry
99
+ SUMMARY.md legacy-compatible derived summary surface
100
+ distilled.json processed source revisions and digest fingerprints
101
+ ```
102
+
103
+ Canonical note files remain readable if a model call fails or a derived registry
104
+ or digest lags. Changed sessions replace their one stable generated note. If a
105
+ fresh extraction returns `NONE`, that generated note is removed rather than
106
+ leaving superseded knowledge behind. Legacy notes are not rewritten: notes with
107
+ project metadata remain project-scoped, while notes with unknown scope appear
108
+ only in an explicit all-project recall.
109
+
110
+ There is no daemon or global worker. Work runs only on Astra activation or an
111
+ explicit command. Pi's file mutation queue serializes writes inside one process,
112
+ and files are atomically replaced to avoid torn output. Two simultaneous Pi
113
+ processes can still duplicate model calls or publish competing derived snapshots;
114
+ the extension does not claim cross-process locking. A later rebuild recovers the
115
+ registry and digest from canonical notes.
116
+
117
+ ## Privacy
118
+
119
+ Session files can contain prompts, tool arguments, and tool results. Distillation
120
+ uses a bounded transcript that omits most tool output and instructs the model to
121
+ exclude credentials, personal data, quoted instructions, rejected proposals,
122
+ and facts already documented in the repository. A prompt cannot guarantee that
123
+ every sensitive value is removed.
124
+
125
+ Choosing a distiller model sends the bounded source material to that model's
126
+ configured provider. Astral Pocket never falls back to a different provider.
127
+ Use `pocket_recall` without `full` first; larger session excerpts can re-surface
128
+ sensitive text from past work. Use all-project recall only when you intentionally
129
+ want foreign repository precedent.
130
+
131
+ ## Install
132
+
133
+ ```sh
134
+ pi install npm:@nklisch/pi-enhanced
135
+ ```
136
+
137
+ Astral Pocket is currently distributed as part of Pi Enhanced rather than as a
138
+ standalone npm package.
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@nklisch/pi-astral-pocket",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "Model-gated note pocket and session recall for gpt-6-astra",
6
+ "type": "module",
7
+ "author": {
8
+ "name": "Nathan Klisch"
9
+ },
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/nklisch/pi-extensions.git",
14
+ "directory": "packages/pi-astral-pocket"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/nklisch/pi-extensions/issues"
18
+ },
19
+ "homepage": "https://github.com/nklisch/pi-extensions/tree/main/packages/pi-astral-pocket#readme",
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "keywords": [
25
+ "pi-package",
26
+ "pi-extension"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./src/index.ts"
31
+ ]
32
+ },
33
+ "files": [
34
+ "src",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*",
45
+ "typebox": "*"
46
+ },
47
+ "devDependencies": {
48
+ "@earendil-works/pi-ai": "0.82.0",
49
+ "@earendil-works/pi-coding-agent": "*",
50
+ "@types/node": "^24.0.0",
51
+ "typescript": ">=7.0.0 <8",
52
+ "vitest": "^4.0.0"
53
+ }
54
+ }
@@ -0,0 +1,44 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { PocketConfig } from "./config.js";
4
+
5
+ export const ASTRA_PROVIDER = "openai-codex";
6
+ export const ASTRA_MODEL_ID = "gpt-6-astra";
7
+ export const POCKET_TOOLS = ["pocket_note", "pocket_recall"] as const;
8
+
9
+ /** Mutable activation flag shared by tools, the /pocket command, and the
10
+ * before_agent_start injector. Module-level because the extension factory has
11
+ * no ctx — the first reliable model read is in session_start. */
12
+ export interface ActivationState {
13
+ active: boolean;
14
+ }
15
+
16
+ export function isAstraModel(model: { id?: string; provider?: string } | undefined | null): boolean {
17
+ return model?.provider === ASTRA_PROVIDER && model?.id === ASTRA_MODEL_ID;
18
+ }
19
+
20
+ export function isActive(state: ActivationState, ctx: ExtensionContext, config: PocketConfig): boolean {
21
+ return config.enabled && isAstraModel(ctx.model);
22
+ }
23
+
24
+ /** Recompute activation from the current model + config and sync pi's active
25
+ * tool set. Call from session_start and model_select handlers — never from
26
+ * the factory body (pi forbids active-set mutation during load). */
27
+ export function recomputeActivation(
28
+ pi: ExtensionAPI,
29
+ ctx: ExtensionContext,
30
+ state: ActivationState,
31
+ config: PocketConfig,
32
+ ): boolean {
33
+ const active = isActive(state, ctx, config);
34
+ const current = pi.getActiveTools();
35
+ const hasPocket = POCKET_TOOLS.every((t) => current.includes(t));
36
+ if (active && !hasPocket) {
37
+ pi.setActiveTools([...new Set([...current, ...POCKET_TOOLS])]);
38
+ } else if (!active && hasPocket) {
39
+ pi.setActiveTools(current.filter((t) => !(POCKET_TOOLS as readonly string[]).includes(t)));
40
+ }
41
+ const becameActive = active && !state.active;
42
+ state.active = active;
43
+ return becameActive;
44
+ }
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
5
+
6
+ export const DEFAULT_DISTILLER_MODEL = "openai-codex/gpt-6-astra";
7
+ export const DEFAULT_DISTILLER_REASONING: ModelThinkingLevel = "minimal";
8
+ export const REASONING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
9
+
10
+ export interface DistillerConfig {
11
+ /** Master switch for activation-time and explicit distillation. */
12
+ enabled: boolean;
13
+ /** Exact provider/modelId selection. No provider fallback is performed. */
14
+ model: string;
15
+ /** Requested Pi reasoning level; the model may map it to another effective effort. */
16
+ reasoning: ModelThinkingLevel;
17
+ /** Sessions younger than this are still in progress; leave them alone. */
18
+ minIdleHours: number;
19
+ /** Bound on extraction calls per pass. */
20
+ maxSessionsPerPass: number;
21
+ /** Sessions older than this are not automatically distilled. */
22
+ maxSessionAgeDays: number;
23
+ }
24
+
25
+ export interface PocketConfig {
26
+ enabled: boolean;
27
+ distiller: DistillerConfig;
28
+ }
29
+
30
+ export const DEFAULT_CONFIG: PocketConfig = {
31
+ enabled: true,
32
+ distiller: {
33
+ enabled: true,
34
+ model: DEFAULT_DISTILLER_MODEL,
35
+ reasoning: DEFAULT_DISTILLER_REASONING,
36
+ minIdleHours: 6,
37
+ maxSessionsPerPass: 16,
38
+ maxSessionAgeDays: 30,
39
+ },
40
+ };
41
+
42
+ export function configPath(root: string): string {
43
+ return join(root, "config.json");
44
+ }
45
+
46
+ function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
47
+ const n = typeof value === "number" && Number.isFinite(value) ? value : fallback;
48
+ return Math.min(max, Math.max(min, n));
49
+ }
50
+
51
+ export function isReasoningLevel(value: unknown): value is ModelThinkingLevel {
52
+ return typeof value === "string" && (REASONING_LEVELS as readonly string[]).includes(value);
53
+ }
54
+
55
+ export function isModelSpec(value: unknown): value is string {
56
+ if (typeof value !== "string") return false;
57
+ const slash = value.indexOf("/");
58
+ return slash > 0 && slash < value.length - 1;
59
+ }
60
+
61
+ /** A malformed configuration degrades to defaults so note access remains usable. */
62
+ export function loadConfig(root: string): PocketConfig {
63
+ let raw: Record<string, unknown> = {};
64
+ try {
65
+ raw = JSON.parse(readFileSync(configPath(root), "utf8")) as Record<string, unknown>;
66
+ } catch {
67
+ return structuredClone(DEFAULT_CONFIG);
68
+ }
69
+ const d = typeof raw.distiller === "object" && raw.distiller !== null
70
+ ? raw.distiller as Record<string, unknown>
71
+ : {};
72
+ return {
73
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_CONFIG.enabled,
74
+ distiller: {
75
+ enabled: typeof d.enabled === "boolean" ? d.enabled : DEFAULT_CONFIG.distiller.enabled,
76
+ // Legacy null meant "choose a cheap fallback". It now resets to the explicit Astra default.
77
+ model: isModelSpec(d.model) ? d.model : DEFAULT_DISTILLER_MODEL,
78
+ reasoning: isReasoningLevel(d.reasoning) ? d.reasoning : DEFAULT_DISTILLER_REASONING,
79
+ minIdleHours: clampNumber(d.minIdleHours, DEFAULT_CONFIG.distiller.minIdleHours, 1, 48),
80
+ maxSessionsPerPass: clampNumber(d.maxSessionsPerPass, DEFAULT_CONFIG.distiller.maxSessionsPerPass, 1, 128),
81
+ maxSessionAgeDays: clampNumber(d.maxSessionAgeDays, DEFAULT_CONFIG.distiller.maxSessionAgeDays, 0, 90),
82
+ },
83
+ };
84
+ }
85
+
86
+ function atomicWrite(path: string, contents: string): void {
87
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
88
+ writeFileSync(temporary, contents, "utf8");
89
+ renameSync(temporary, path);
90
+ }
91
+
92
+ export function saveConfig(root: string, config: PocketConfig): void {
93
+ mkdirSync(root, { recursive: true });
94
+ atomicWrite(configPath(root), `${JSON.stringify(config, null, 2)}\n`);
95
+ }
@@ -0,0 +1,78 @@
1
+ import type { DistillerResult } from "./distiller.js";
2
+
3
+ export type PassOutcome =
4
+ | { state: "idle" }
5
+ | { state: "running"; startedAt: string }
6
+ | { state: "completed"; finishedAt: string; result: DistillerResult }
7
+ | { state: "failed"; finishedAt: string; error: string }
8
+ | { state: "cancelled"; finishedAt: string };
9
+
10
+ export type PassReporter = (message: string, level: "info" | "warning" | "error") => void;
11
+
12
+ /** Own one serialized, revocable background pass for this extension instance. */
13
+ export class DistillerController {
14
+ private generation = 0;
15
+ private controller: AbortController | undefined;
16
+ private queue: Promise<void> = Promise.resolve();
17
+ private reporter: PassReporter | undefined;
18
+ private outcome: PassOutcome = { state: "idle" };
19
+
20
+ status(): PassOutcome {
21
+ return this.outcome;
22
+ }
23
+
24
+ stop(): void {
25
+ this.generation += 1;
26
+ this.reporter = undefined;
27
+ this.controller?.abort();
28
+ this.controller = undefined;
29
+ if (this.outcome.state === "running") {
30
+ this.outcome = { state: "cancelled", finishedAt: new Date().toISOString() };
31
+ }
32
+ }
33
+
34
+ start(
35
+ task: (signal: AbortSignal) => Promise<DistillerResult>,
36
+ reporter?: PassReporter,
37
+ ): Promise<void> {
38
+ this.stop();
39
+ const generation = this.generation;
40
+ const controller = new AbortController();
41
+ this.controller = controller;
42
+ this.reporter = reporter;
43
+ const run = async () => {
44
+ if (controller.signal.aborted || generation !== this.generation) return;
45
+ this.outcome = { state: "running", startedAt: new Date().toISOString() };
46
+ try {
47
+ const result = await task(controller.signal);
48
+ if (controller.signal.aborted || generation !== this.generation) return;
49
+ this.outcome = { state: "completed", finishedAt: new Date().toISOString(), result };
50
+ if (result.errors.length > 0) {
51
+ this.report(generation, `Astral Pocket distillation finished with ${result.errors.length} error(s); source notes remain available.`, "warning");
52
+ }
53
+ } catch (error) {
54
+ if (controller.signal.aborted || generation !== this.generation) return;
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ this.outcome = { state: "failed", finishedAt: new Date().toISOString(), error: message };
57
+ this.report(generation, `Astral Pocket distillation failed: ${message}. Source notes remain available.`, "warning");
58
+ } finally {
59
+ if (generation === this.generation) {
60
+ this.controller = undefined;
61
+ this.reporter = undefined;
62
+ }
63
+ }
64
+ };
65
+ this.queue = this.queue.catch(() => undefined).then(run);
66
+ return this.queue;
67
+ }
68
+
69
+ private report(generation: number, message: string, level: "info" | "warning" | "error"): void {
70
+ if (generation !== this.generation || !this.reporter) return;
71
+ try {
72
+ this.reporter(message, level);
73
+ } catch {
74
+ // Session replacement can revoke a UI sink between the generation check
75
+ // and notification. Reporting failure must never escape this boundary.
76
+ }
77
+ }
78
+ }