@astrosheep/pi-context 0.25.2 → 0.26.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/README.md +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +613 -367
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +16 -12
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +298 -242
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/tool-output.d.ts +101 -0
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +309 -10
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +55 -29
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +8 -7
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension.d.ts +115 -0
- package/dist/test/helpers/extension.js +6 -6
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +111 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +22 -24
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +137 -7
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +19 -11
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +308 -244
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PiContextSettings } from "../settings.js";
|
|
2
|
+
export type DreamerSetting = {
|
|
3
|
+
pattern?: string;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* `pi-context.dreamer` is a non-empty model pattern. Anything else present is ignored
|
|
8
|
+
* with one warning; absent means no configured pattern, so the automatic model applies.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveDreamer(settings: PiContextSettings): DreamerSetting;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the configurable dreamer model from Pi settings for a CLI invocation: global
|
|
13
|
+
* `~/.pi/agent/settings.json` merged with the project's `.pi/settings.json`, project
|
|
14
|
+
* values winning per key. A settings read failure degrades to no pattern with one warning.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readDreamerSettings(cwd?: string): DreamerSetting;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SessionReader } from "../session-reader.js";
|
|
2
|
+
type HistoryItem = {
|
|
3
|
+
windowId: string;
|
|
4
|
+
itemId: string;
|
|
5
|
+
role: "user" | "assistant" | "tool_call" | "tool" | "system" | "developer";
|
|
6
|
+
content: string;
|
|
7
|
+
createdAt: string | undefined;
|
|
8
|
+
toolName?: string;
|
|
9
|
+
outputTruncated?: boolean;
|
|
10
|
+
fullOutputPath?: string;
|
|
11
|
+
toolError?: boolean;
|
|
12
|
+
};
|
|
13
|
+
type HistoryWindow = {
|
|
14
|
+
windowId: string;
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
items: HistoryItem[];
|
|
17
|
+
};
|
|
18
|
+
type HistoryFilter = {
|
|
19
|
+
window_id?: string | null;
|
|
20
|
+
role?: HistoryItem["role"] | null;
|
|
21
|
+
tool_name?: string | null;
|
|
22
|
+
recent_first?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function contentText(content: unknown): string;
|
|
25
|
+
/** Build durable, on-demand history directly from every entry on the current session branch. */
|
|
26
|
+
export declare function historyFromSession(ctx: SessionReader): HistoryWindow[];
|
|
27
|
+
export declare function visibleItem(item: HistoryItem, maxChars?: number): {
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
total_chars: number;
|
|
30
|
+
truncated_content: string;
|
|
31
|
+
tool_error?: boolean | undefined;
|
|
32
|
+
output_truncated?: boolean | undefined;
|
|
33
|
+
full_output_path?: string | null | undefined;
|
|
34
|
+
window_id: string;
|
|
35
|
+
item_id: string;
|
|
36
|
+
role: "user" | "assistant" | "tool_call" | "tool" | "system" | "developer";
|
|
37
|
+
tool_name: string | null;
|
|
38
|
+
};
|
|
39
|
+
export declare function allItems(ctx: SessionReader): HistoryItem[];
|
|
40
|
+
/**
|
|
41
|
+
* window_id must name a real window; anything else is a named error, not a silent empty page
|
|
42
|
+
* (a window that exists but has no matching items after the other filters stays a legal empty
|
|
43
|
+
* page). Returns the teaching message plus the known window ids so the error is self-healing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function unknownWindowId(ctx: SessionReader, params: HistoryFilter): {
|
|
46
|
+
message: string;
|
|
47
|
+
known: string[];
|
|
48
|
+
} | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* A role×tool_name combination is vacuous — provably empty from the taxonomy alone, before
|
|
51
|
+
* any data is read — when tool_name is given alongside a role that never carries one. Only
|
|
52
|
+
* "tool_call" and "tool" items have a tool name. Returns the teaching error message, or
|
|
53
|
+
* undefined when the combination can match.
|
|
54
|
+
*/
|
|
55
|
+
export declare function vacuousRoleToolCombo(params: HistoryFilter): string | undefined;
|
|
56
|
+
export declare function filteredItems(ctx: SessionReader, params: HistoryFilter): HistoryItem[];
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionFactory, type SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { deriveThresholds } from "./context/thresholds.js";
|
|
3
|
+
import { mergePiContextSettings } from "./settings.js";
|
|
4
|
+
import { assertVirtualPath } from "./notes/address.js";
|
|
5
|
+
export { historyFromSession } from "./history/history.js";
|
|
6
|
+
export { notesFromSession } from "./pi/notes/session-replay.js";
|
|
7
|
+
/**
|
|
8
|
+
* Create an extension factory bound to an SDK settings authority. The host must pass
|
|
9
|
+
* the same manager to createAgentSession and to this factory's resource loader.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPiContext(options?: {
|
|
12
|
+
settingsManager?: SettingsManager;
|
|
13
|
+
}): ExtensionFactory;
|
|
14
|
+
/** The Pi-discovered extension keeps the standard file-backed settings behavior. */
|
|
15
|
+
export default function piContext(pi: ExtensionAPI): void;
|
|
16
|
+
export declare const internal: {
|
|
17
|
+
MAX_NOTE_BYTES: number;
|
|
18
|
+
NOTE_TYPE: string;
|
|
19
|
+
BOOT_TYPE: string;
|
|
20
|
+
GUIDANCE_TYPE: string;
|
|
21
|
+
WARNING_TYPE: string;
|
|
22
|
+
CONTINUATION_TYPE: string;
|
|
23
|
+
WARNING_PROMPT: string;
|
|
24
|
+
WARNING_CONTENT: string;
|
|
25
|
+
WARNING_RUNWAY_TOKENS: number;
|
|
26
|
+
RESET_MARKER_TYPE: string;
|
|
27
|
+
CONTINUATION: string;
|
|
28
|
+
CONTEXT_WINDOW_OPEN_TAG: string;
|
|
29
|
+
CONTEXT_WINDOW_CLOSE_TAG: string;
|
|
30
|
+
CONTEXT_WINDOW_PROTOCOL_OPEN_TAG: string;
|
|
31
|
+
CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG: string;
|
|
32
|
+
GUIDANCE_OPEN_TAG: string;
|
|
33
|
+
PI_CONTEXT_SETTINGS_KEY: string;
|
|
34
|
+
DEFAULT_RESERVE_TOKENS: number;
|
|
35
|
+
DEFAULT_REMINDER_MARGIN_TOKENS: number;
|
|
36
|
+
deriveThresholds: typeof deriveThresholds;
|
|
37
|
+
mergePiContextSettings: typeof mergePiContextSettings;
|
|
38
|
+
assertVirtualPath: typeof assertVirtualPath;
|
|
39
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerHistoryTools } from "./history/history-tools.js";
|
|
3
|
-
import { registerNotesTools } from "./notes/tools.js";
|
|
3
|
+
import { registerNotesTools } from "./pi/notes/tools.js";
|
|
4
4
|
import { deriveThresholds } from "./context/thresholds.js";
|
|
5
5
|
import { registerContext } from "./context/runtime.js";
|
|
6
6
|
import { mergePiContextSettings } from "./settings.js";
|
|
7
|
-
import { NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, RESET_MARKER_TYPE, CONTINUATION_TYPE, MAX_NOTE_BYTES, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, WARNING_RUNWAY_TOKENS, CONTINUATION, WARNING_PROMPT } from "./protocol.js";
|
|
7
|
+
import { NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, RESET_MARKER_TYPE, CONTINUATION_TYPE, MAX_NOTE_BYTES, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, WARNING_RUNWAY_TOKENS, CONTINUATION, WARNING_PROMPT, WARNING_CONTENT } from "./protocol.js";
|
|
8
8
|
import { assertVirtualPath } from "./notes/address.js";
|
|
9
9
|
export { historyFromSession } from "./history/history.js";
|
|
10
|
-
export { notesFromSession } from "./notes/session-replay.js";
|
|
10
|
+
export { notesFromSession } from "./pi/notes/session-replay.js";
|
|
11
11
|
function registerPiContext(pi, settingsManager) {
|
|
12
12
|
const [major, minor] = VERSION.split(".").map(Number);
|
|
13
13
|
if (!(major > 0 || (major === 0 && minor >= 87))) {
|
|
@@ -28,4 +28,4 @@ export function createPiContext(options = {}) {
|
|
|
28
28
|
export default function piContext(pi) {
|
|
29
29
|
registerPiContext(pi);
|
|
30
30
|
}
|
|
31
|
-
export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
|
|
31
|
+
export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_CONTENT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NotesContext } from "./context.js";
|
|
2
|
+
import { type Scope } from "./paths.js";
|
|
3
|
+
export type NoteAddress = {
|
|
4
|
+
scope: Scope;
|
|
5
|
+
path: string;
|
|
6
|
+
who?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const ADDRESS_FORMS = "legal prefixes are @project/, @human/, @self/, and @model/; bare names are this session";
|
|
9
|
+
export declare function assertVirtualPath(value: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* Minimal glob over virtual note paths: `*` matches any run within a segment (never
|
|
12
|
+
* `/`), `**` matches any run across segments (a leading double-star followed by a
|
|
13
|
+
* slash also matches zero segments, so it covers the root too), `?` matches exactly
|
|
14
|
+
* one non-`/` character. Everything else is literal and the match is anchored to the
|
|
15
|
+
* whole path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
18
|
+
/** Glob patterns are not virtual paths (`*` is legal), so they get their own guard. */
|
|
19
|
+
export declare function assertGlobPattern(value: unknown): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Decode one public note address into its physical home and virtual path. @self and @model
|
|
22
|
+
* are relative; explicit agents/models addresses always name a canonical slug.
|
|
23
|
+
*/
|
|
24
|
+
export declare function assertAddress(value: unknown): NoteAddress;
|
|
25
|
+
/** Render a virtual path in its one unambiguous public address form. */
|
|
26
|
+
export declare function addressFor(context: NotesContext, scope: Scope, path: string, who?: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SLUG_PATTERN } from "./paths.js";
|
|
2
2
|
export const ADDRESS_FORMS = "legal prefixes are @project/, @human/, @self/, and @model/; bare names are this session";
|
|
3
3
|
export function assertVirtualPath(value) {
|
|
4
4
|
if (typeof value !== "string" || value.length === 0)
|
|
@@ -37,7 +37,7 @@ export function globToRegExp(pattern) {
|
|
|
37
37
|
}
|
|
38
38
|
return new RegExp(`${source}$`);
|
|
39
39
|
}
|
|
40
|
-
/** Glob patterns are not virtual paths (`*` is legal), so they get their own guard
|
|
40
|
+
/** Glob patterns are not virtual paths (`*` is legal), so they get their own guard. */
|
|
41
41
|
export function assertGlobPattern(value) {
|
|
42
42
|
if (value === undefined || value === null || value === "")
|
|
43
43
|
return undefined;
|
|
@@ -48,12 +48,8 @@ export function assertGlobPattern(value) {
|
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Decode
|
|
52
|
-
*
|
|
53
|
-
* The word after `@` is always a reserved home name; agent and model names live at the
|
|
54
|
-
* second level (@agents/faye/, never @faye/), so user-chosen names can never collide with
|
|
55
|
-
* the reserved set. `@self` and `@model` are relative — `who` stays undefined and the
|
|
56
|
-
* store resolves the current agent/model at call time.
|
|
51
|
+
* Decode one public note address into its physical home and virtual path. @self and @model
|
|
52
|
+
* are relative; explicit agents/models addresses always name a canonical slug.
|
|
57
53
|
*/
|
|
58
54
|
export function assertAddress(value) {
|
|
59
55
|
if (typeof value !== "string")
|
|
@@ -96,10 +92,8 @@ export function assertAddress(value) {
|
|
|
96
92
|
assertVirtualPath(path);
|
|
97
93
|
return { scope, path, who };
|
|
98
94
|
}
|
|
99
|
-
/** Render a virtual path in its one unambiguous public address form.
|
|
100
|
-
|
|
101
|
-
* name, so listings alone tell every home apart. */
|
|
102
|
-
export function addressFor(ctx, scope, path, who) {
|
|
95
|
+
/** Render a virtual path in its one unambiguous public address form. */
|
|
96
|
+
export function addressFor(context, scope, path, who) {
|
|
103
97
|
if (scope === "session")
|
|
104
98
|
return path;
|
|
105
99
|
if (scope === "project")
|
|
@@ -107,6 +101,6 @@ export function addressFor(ctx, scope, path, who) {
|
|
|
107
101
|
if (scope === "human")
|
|
108
102
|
return `@human/${path}`;
|
|
109
103
|
if (scope === "agent")
|
|
110
|
-
return `@agents/${who ??
|
|
111
|
-
return `@models/${who ??
|
|
104
|
+
return `@agents/${who ?? context.agent}/${path}`;
|
|
105
|
+
return `@models/${who ?? context.model}/${path}`;
|
|
112
106
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Explicit, host-neutral identity for one notes store. */
|
|
2
|
+
export type NotesContext = Readonly<{
|
|
3
|
+
home: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
projectKey: string;
|
|
6
|
+
agent: string;
|
|
7
|
+
model: string;
|
|
8
|
+
}>;
|
|
9
|
+
/** Validate and snapshot caller identity; no global or environment defaults are consulted. */
|
|
10
|
+
export declare function snapshotNotesContext(value: NotesContext): NotesContext;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { SLUG_PATTERN } from "./paths.js";
|
|
3
|
+
function requireString(field, value) {
|
|
4
|
+
if (typeof value !== "string" || value.length === 0)
|
|
5
|
+
throw new TypeError(`notes context ${field} must be a non-empty string`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function assertDirectoryComponent(field, value) {
|
|
9
|
+
if (value === "." || value === ".." || /[\\/\0:]/.test(value)) {
|
|
10
|
+
throw new TypeError(`notes context ${field} must be a safe single directory component`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Validate and snapshot caller identity; no global or environment defaults are consulted. */
|
|
14
|
+
export function snapshotNotesContext(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
16
|
+
throw new TypeError("notes context must be a plain object");
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
19
|
+
throw new TypeError("notes context must be a plain object");
|
|
20
|
+
const homeValue = requireString("home", value.home);
|
|
21
|
+
const sessionId = requireString("sessionId", value.sessionId);
|
|
22
|
+
const projectKey = requireString("projectKey", value.projectKey);
|
|
23
|
+
const agent = requireString("agent", value.agent);
|
|
24
|
+
const model = requireString("model", value.model);
|
|
25
|
+
assertDirectoryComponent("sessionId", sessionId);
|
|
26
|
+
assertDirectoryComponent("projectKey", projectKey);
|
|
27
|
+
if (!SLUG_PATTERN.test(agent))
|
|
28
|
+
throw new TypeError("notes context agent must be a canonical lowercase slug");
|
|
29
|
+
if (!SLUG_PATTERN.test(model))
|
|
30
|
+
throw new TypeError("notes context model must be a canonical lowercase slug");
|
|
31
|
+
const home = resolve(homeValue);
|
|
32
|
+
return Object.freeze({ home, sessionId, projectKey, agent, model });
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Scope } from "./paths.js";
|
|
2
|
+
export type NoteStatus = "active" | "superseded" | "pending" | "archived";
|
|
3
|
+
export type Origin = "user" | "self" | "external";
|
|
4
|
+
/**
|
|
5
|
+
* Harness-owned note metadata. The eight required keys are always written; the four optional
|
|
6
|
+
* sleep-shift keys survive this layer untouched when present. `[key: string]: unknown` carries
|
|
7
|
+
* any frontmatter key this layer does not know, preserved verbatim across rewrites.
|
|
8
|
+
*/
|
|
9
|
+
export type NoteMeta = {
|
|
10
|
+
scope: Scope;
|
|
11
|
+
origin: Origin;
|
|
12
|
+
status: NoteStatus;
|
|
13
|
+
stale: boolean;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
lastAccessed: number;
|
|
17
|
+
accessCount: number;
|
|
18
|
+
sourceWindow?: string;
|
|
19
|
+
supersedes?: string;
|
|
20
|
+
recurrenceCount?: number;
|
|
21
|
+
recurrenceWindows?: string[];
|
|
22
|
+
/** Project ownership on newly-created session notes; legacy/invalid values are preserved as-is. */
|
|
23
|
+
project?: unknown;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Format epoch milliseconds as an ISO 8601 string in the host's local time zone with an
|
|
28
|
+
* explicit numeric offset (e.g. 2026-09-15T17:31:45.392+08:00). A UTC host renders
|
|
29
|
+
* "+00:00"; the "Z" designator is never used, and Date.parse round-trips the value.
|
|
30
|
+
*/
|
|
31
|
+
export declare function localIso(epochMs: number): string;
|
|
32
|
+
export declare function isScope(value: unknown): value is Scope;
|
|
33
|
+
export declare function isOrigin(value: unknown): value is Origin;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a note file. Missing known keys take the Design defaults (status active, stale false,
|
|
36
|
+
* accessCount 0, timestamps now); unknown keys are carried through untouched. Known
|
|
37
|
+
* snake_case metadata is refused because it requires the explicit manual migration.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseNote(raw: string, now?: number): {
|
|
40
|
+
meta: NoteMeta;
|
|
41
|
+
body: string;
|
|
42
|
+
};
|
|
43
|
+
/** Serialize frontmatter + blank line + body. Known keys emit in Design order, extras after. */
|
|
44
|
+
export declare function serializeNote(meta: NoteMeta, body: string): string;
|
|
45
|
+
/** Strip a leading frontmatter block from user content, so a note body is pure content. */
|
|
46
|
+
export declare function stripLeadingFrontmatter(content: string): string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const SCOPES = ["session", "project", "human", "agent", "model"];
|
|
2
2
|
const ORIGINS = ["user", "self", "external"];
|
|
3
3
|
const STATUSES = ["active", "superseded", "pending", "archived"];
|
|
4
|
-
const TIMESTAMP_KEYS = ["
|
|
4
|
+
const TIMESTAMP_KEYS = ["createdAt", "updatedAt", "lastAccessed"];
|
|
5
|
+
const LEGACY_KNOWN_KEYS = ["created_at", "updated_at", "last_accessed", "access_count", "source_window", "recurrence_count", "recurrence_windows"];
|
|
5
6
|
/** Emission order, exactly the Design's key list. */
|
|
6
|
-
const KNOWN_KEYS = ["origin", "status", "stale", "
|
|
7
|
+
const KNOWN_KEYS = ["origin", "status", "stale", "createdAt", "updatedAt", "lastAccessed", "accessCount", "sourceWindow", "supersedes", "recurrenceCount", "recurrenceWindows"];
|
|
7
8
|
const pad2 = (value) => String(value).padStart(2, "0");
|
|
8
9
|
/**
|
|
9
10
|
* Format epoch milliseconds as an ISO 8601 string in the host's local time zone with an
|
|
@@ -69,7 +70,7 @@ function parseFrontmatter(raw) {
|
|
|
69
70
|
}
|
|
70
71
|
if (close === -1)
|
|
71
72
|
return { fields: {}, body: raw };
|
|
72
|
-
const fields =
|
|
73
|
+
const fields = Object.create(null);
|
|
73
74
|
for (let index = 1; index < close; index++) {
|
|
74
75
|
const line = lines[index];
|
|
75
76
|
const match = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/.exec(line);
|
|
@@ -97,10 +98,14 @@ function parseFrontmatter(raw) {
|
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
100
|
* Parse a note file. Missing known keys take the Design defaults (status active, stale false,
|
|
100
|
-
*
|
|
101
|
+
* accessCount 0, timestamps now); unknown keys are carried through untouched. Known
|
|
102
|
+
* snake_case metadata is refused because it requires the explicit manual migration.
|
|
101
103
|
*/
|
|
102
104
|
export function parseNote(raw, now = Date.now()) {
|
|
103
105
|
const { fields, body } = parseFrontmatter(raw);
|
|
106
|
+
const legacyKeys = LEGACY_KNOWN_KEYS.filter((key) => Object.hasOwn(fields, key));
|
|
107
|
+
if (legacyKeys.length > 0)
|
|
108
|
+
throw new Error(`legacy note metadata ${legacyKeys.join(", ")} requires manual migration to camelCase before this note can be used`);
|
|
104
109
|
const meta = { ...fields };
|
|
105
110
|
// scope is a legacy on-disk field: store callers derive it from the file's home and
|
|
106
111
|
// overwrite it after parsing, so an absent or outdated value just falls back.
|
|
@@ -110,7 +115,7 @@ export function parseNote(raw, now = Date.now()) {
|
|
|
110
115
|
meta.stale = meta.stale === true;
|
|
111
116
|
for (const key of TIMESTAMP_KEYS)
|
|
112
117
|
meta[key] = toEpoch(meta[key], now);
|
|
113
|
-
meta.
|
|
118
|
+
meta.accessCount = typeof meta.accessCount === "number" && Number.isFinite(meta.accessCount) ? meta.accessCount : 0;
|
|
114
119
|
return { meta: meta, body };
|
|
115
120
|
}
|
|
116
121
|
/** Emit a YAML scalar: bare for safe strings and JSON literals, JSON-quoted otherwise. */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createNotesStore, NoteError } from "./store.js";
|
|
2
|
+
export { projectKey, slugify } from "./paths.js";
|
|
3
|
+
export type { NotesContext } from "./context.js";
|
|
4
|
+
export type { EditOperation, EditOptions, NoteChange, NoteEditResult, NoteErrorCode, NoteMatch, NoteMeta, NoteReadResult, NoteRow, NoteSearchRow, NoteStatus, NoteWriteResult, NotesQuery, NotesStore, Origin, Scope, WriteOptions, } from "./store.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NotesContext } from "./context.js";
|
|
2
|
+
export type Scope = "session" | "project" | "human" | "agent" | "model";
|
|
3
|
+
/** The one legal home-name shape: lowercase [a-z0-9-] runs separated by single dashes. */
|
|
4
|
+
export declare const SLUG_PATTERN: RegExp;
|
|
5
|
+
/** Slugify a caller-declared identity; identity is never inferred from note content. */
|
|
6
|
+
export declare function slugify(value: string): string;
|
|
7
|
+
/** `<basename(absGitRoot)-sha1(absGitRoot)[:8]>`, or the same formula over cwd with no git root. */
|
|
8
|
+
export declare function projectKey(cwd: string): string;
|
|
9
|
+
/** Absolute directory holding the per-session note homes. */
|
|
10
|
+
export declare function sessionHomesRoot(home: string): string;
|
|
11
|
+
/** Absolute directory holding one scope's notes. The context's home is already resolved. */
|
|
12
|
+
export declare function scopeDir(scope: Scope, context: NotesContext, who?: string): string;
|
|
13
|
+
/** Every existing home directory of the agents/ or models/ namespace, as names. */
|
|
14
|
+
export declare function namespaceSlugs(namespace: "agents" | "models", home: string): Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Notes are markdown files: a virtual path without an `.md` suffix gains one, an explicit
|
|
17
|
+
* `.md` is kept as-is, so `a/b` and `a/b.md` name the same physical file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function noteFileName(vpath: string): string;
|
|
20
|
+
/** Absolute file path for a virtual path in a scope. Callers validate the vpath first. */
|
|
21
|
+
export declare function physicalPath(scope: Scope, vpath: string, context: NotesContext, who?: string): string;
|
package/dist/src/notes/paths.js
CHANGED
|
@@ -1,99 +1,91 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
4
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
/** The one legal home-name shape: lowercase [a-z0-9-] runs separated by single dashes. */
|
|
6
|
+
export const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
/** Slugify a caller-declared identity; identity is never inferred from note content. */
|
|
8
|
+
export function slugify(value) {
|
|
9
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10
|
+
return slug.length > 0 ? slug : "root";
|
|
9
11
|
}
|
|
10
|
-
/**
|
|
11
|
-
export function
|
|
12
|
-
|
|
12
|
+
/** `<basename(absGitRoot)-sha1(absGitRoot)[:8]>`, or the same formula over cwd with no git root. */
|
|
13
|
+
export function projectKey(cwd) {
|
|
14
|
+
const absolute = resolve(cwd);
|
|
15
|
+
const root = gitRoot(absolute) ?? absolute;
|
|
16
|
+
const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
|
|
17
|
+
return `${basename(root)}-${digest}`;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* Repository root behind one `.git` entry. A `.git` directory is the main checkout
|
|
21
|
+
* itself. A `.git` file is a worktree or submodule pointer: a linked worktree names
|
|
22
|
+
* `<main>/.git/worktrees/<name>` and resolves to `<main>`, so every worktree of one
|
|
23
|
+
* repository shares one project identity. Submodules, bare repositories, and separate
|
|
24
|
+
* git dirs keep the current directory.
|
|
17
25
|
*/
|
|
26
|
+
function repositoryRoot(dir) {
|
|
27
|
+
let stats;
|
|
28
|
+
try {
|
|
29
|
+
stats = statSync(join(dir, ".git"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
if (stats.isDirectory())
|
|
35
|
+
return dir;
|
|
36
|
+
let pointer;
|
|
37
|
+
try {
|
|
38
|
+
pointer = readFileSync(join(dir, ".git"), "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
const match = /^gitdir:\s*(.+)$/m.exec(pointer);
|
|
44
|
+
if (!match)
|
|
45
|
+
return dir;
|
|
46
|
+
const parts = resolve(dir, match[1].trim()).split(sep);
|
|
47
|
+
const worktrees = parts.lastIndexOf("worktrees");
|
|
48
|
+
if (worktrees <= 0 || worktrees !== parts.length - 2)
|
|
49
|
+
return dir;
|
|
50
|
+
const common = parts.slice(0, worktrees).join(sep);
|
|
51
|
+
return basename(common) === ".git" ? dirname(common) : dir;
|
|
52
|
+
}
|
|
53
|
+
/** Absolute repository root for cwd, walking upward until a directory holds a `.git` entry. */
|
|
18
54
|
function gitRoot(cwd) {
|
|
19
55
|
let dir = resolve(cwd);
|
|
20
56
|
for (;;) {
|
|
21
57
|
if (existsSync(join(dir, ".git")))
|
|
22
|
-
return dir;
|
|
58
|
+
return repositoryRoot(dir);
|
|
23
59
|
const parent = dirname(dir);
|
|
24
60
|
if (parent === dir)
|
|
25
61
|
return undefined;
|
|
26
62
|
dir = parent;
|
|
27
63
|
}
|
|
28
64
|
}
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
const root = gitRoot(absolute) ?? absolute;
|
|
33
|
-
const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
|
|
34
|
-
return `${basename(root)}-${digest}`;
|
|
35
|
-
}
|
|
36
|
-
/** Session identity comes from the pi session manager; ids are filesystem-safe by construction. */
|
|
37
|
-
function sessionId(ctx) {
|
|
38
|
-
return ctx.sessionManager.getSessionId();
|
|
39
|
-
}
|
|
40
|
-
/** The one legal home-name shape: lowercase [a-z0-9-] runs separated by single dashes. */
|
|
41
|
-
export const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
42
|
-
/**
|
|
43
|
-
* Identity slugs: one declared name per home, never detected from prompt content.
|
|
44
|
-
* `PI_NOTES_AGENT` declares who is running (default "root"); the model slug derives
|
|
45
|
-
* from the live model id, provider prefix stripped. Both slugified to [a-z0-9-].
|
|
46
|
-
*/
|
|
47
|
-
export function slugify(value) {
|
|
48
|
-
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
49
|
-
return slug.length > 0 ? slug : "root";
|
|
50
|
-
}
|
|
51
|
-
/** The current agent's home name: the launch-declared identity, defaulting to "root". */
|
|
52
|
-
export function agentSlug(_ctx) {
|
|
53
|
-
return slugify(process.env.PI_NOTES_AGENT ?? "root");
|
|
54
|
-
}
|
|
55
|
-
/** The current model's home name, live-resolved from ctx.model; "default" when unknown. */
|
|
56
|
-
export function modelSlug(ctx) {
|
|
57
|
-
const id = ctx.model?.id;
|
|
58
|
-
if (!id)
|
|
59
|
-
return "default";
|
|
60
|
-
return slugify(id.split("/").pop() ?? id);
|
|
65
|
+
/** Absolute directory holding the per-session note homes. */
|
|
66
|
+
export function sessionHomesRoot(home) {
|
|
67
|
+
return join(home, "pi", "session");
|
|
61
68
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
/** Absolute directory holding one scope's notes. The context's home is already resolved. */
|
|
70
|
+
export function scopeDir(scope, context, who) {
|
|
71
|
+
if (!["session", "project", "human", "agent", "model"].includes(scope))
|
|
72
|
+
throw new TypeError("invalid notes scope");
|
|
73
|
+
if (who !== undefined && (scope !== "agent" && scope !== "model" || !SLUG_PATTERN.test(who)))
|
|
74
|
+
throw new TypeError("who must be a canonical agent/model slug");
|
|
68
75
|
if (scope === "human")
|
|
69
|
-
return join(
|
|
76
|
+
return join(context.home, "human");
|
|
70
77
|
if (scope === "project")
|
|
71
|
-
return join(
|
|
78
|
+
return join(context.home, "project", context.projectKey);
|
|
72
79
|
if (scope === "agent")
|
|
73
|
-
return join(
|
|
80
|
+
return join(context.home, "agents", who ?? context.agent);
|
|
74
81
|
if (scope === "model")
|
|
75
|
-
return join(
|
|
76
|
-
return join(sessionHomesRoot(), sessionId
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* One-time migration of the pre-v0.25 `personal/` home to `human/`. Runs at extension
|
|
80
|
-
* activation; returns a warning string when both directories exist (no auto-merge),
|
|
81
|
-
* undefined otherwise. Old note bodies are history, not addresses, and stay untouched.
|
|
82
|
-
*/
|
|
83
|
-
export function migrateLegacyHomes(home = notesRoot()) {
|
|
84
|
-
const legacy = join(home, "personal");
|
|
85
|
-
const modern = join(home, "human");
|
|
86
|
-
if (!existsSync(legacy))
|
|
87
|
-
return undefined;
|
|
88
|
-
if (existsSync(modern))
|
|
89
|
-
return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
|
|
90
|
-
renameSync(legacy, modern);
|
|
91
|
-
return undefined;
|
|
82
|
+
return join(context.home, "models", who ?? context.model);
|
|
83
|
+
return join(sessionHomesRoot(context.home), context.sessionId);
|
|
92
84
|
}
|
|
93
|
-
/** Every existing home directory of the agents/ or models/ namespace, as
|
|
94
|
-
export function namespaceSlugs(namespace, home
|
|
85
|
+
/** Every existing home directory of the agents/ or models/ namespace, as names. */
|
|
86
|
+
export async function namespaceSlugs(namespace, home) {
|
|
95
87
|
try {
|
|
96
|
-
return
|
|
88
|
+
return (await readdir(join(home, namespace), { withFileTypes: true }))
|
|
97
89
|
.filter((entry) => entry.isDirectory())
|
|
98
90
|
.map((entry) => entry.name)
|
|
99
91
|
.sort();
|
|
@@ -110,6 +102,10 @@ export function noteFileName(vpath) {
|
|
|
110
102
|
return vpath.endsWith(".md") ? vpath : `${vpath}.md`;
|
|
111
103
|
}
|
|
112
104
|
/** Absolute file path for a virtual path in a scope. Callers validate the vpath first. */
|
|
113
|
-
export function physicalPath(scope, vpath,
|
|
114
|
-
|
|
105
|
+
export function physicalPath(scope, vpath, context, who) {
|
|
106
|
+
if (typeof vpath !== "string" || vpath.length === 0 || vpath.includes("\0") || vpath.includes("\\") || vpath.startsWith("/"))
|
|
107
|
+
throw new TypeError("path must be a safe virtual relative path");
|
|
108
|
+
if (vpath.split("/").some((part) => part.length === 0 || part === "." || part === ".."))
|
|
109
|
+
throw new TypeError("path contains an unsupported component");
|
|
110
|
+
return join(scopeDir(scope, context, who), ...noteFileName(vpath).split("/"));
|
|
115
111
|
}
|