@ryan_nookpi/pi-extension-memory-layer 0.1.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/inject.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { readMemoryMd } from "./storage.ts";
2
+
3
+ const MAX_LINES = 200;
4
+
5
+ /**
6
+ * Build the memory prompt injected into systemPrompt every turn.
7
+ *
8
+ * Strategy (Claude Code style):
9
+ * - Read user/MEMORY.md + project/MEMORY.md (index files)
10
+ * - If combined ≤ 200 lines → inject full index
11
+ * - If > 200 lines → truncate with hint to use recall
12
+ *
13
+ * The index already contains every memory title grouped by topic,
14
+ * so the LLM knows what's available without needing a separate recall call.
15
+ */
16
+ export async function buildMemoryPrompt(projectId?: string): Promise<string | null> {
17
+ const userIndex = (await readMemoryMd("user")).trim();
18
+ const projectIndex = projectId ? (await readMemoryMd("project", projectId)).trim() : "";
19
+
20
+ if (!userIndex && !projectIndex) return null;
21
+
22
+ const parts: string[] = [];
23
+
24
+ if (userIndex) {
25
+ parts.push("[User Memory]");
26
+ parts.push(userIndex);
27
+ }
28
+
29
+ if (projectIndex) {
30
+ if (parts.length) parts.push("");
31
+ parts.push("[Project Memory]");
32
+ parts.push(projectIndex);
33
+ }
34
+
35
+ let lines = parts.join("\n").split("\n");
36
+
37
+ if (lines.length > MAX_LINES) {
38
+ lines = lines.slice(0, MAX_LINES);
39
+ lines.push("... (truncated — use recall tool for full details)");
40
+ }
41
+
42
+ return [
43
+ "",
44
+ "",
45
+ "[Memory Layer]",
46
+ lines.join("\n"),
47
+ "상세 내용은 recall({ query })로 검색 후, 결과의 ID로 recall({ id })를 호출하면 볼 수 있습니다.",
48
+ ].join("\n");
49
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ryan_nookpi/pi-extension-memory-layer",
3
+ "version": "0.1.0",
4
+ "description": "Long-term memory layer for pi — remember, recall, forget, and browse memories across sessions.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Jonghakseo/pi-extension.git",
9
+ "directory": "packages/memory-layer"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Jonghakseo/pi-extension/issues"
13
+ },
14
+ "homepage": "https://github.com/Jonghakseo/pi-extension/tree/main/packages/memory-layer#readme",
15
+ "type": "module",
16
+ "keywords": [
17
+ "pi-package"
18
+ ],
19
+ "files": [
20
+ "index.ts",
21
+ "types.ts",
22
+ "storage.ts",
23
+ "inject.ts",
24
+ "ui.ts",
25
+ "project-id.ts",
26
+ "README.md"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./index.ts"
31
+ ]
32
+ },
33
+ "peerDependencies": {
34
+ "@mariozechner/pi-ai": "*",
35
+ "@mariozechner/pi-coding-agent": "*",
36
+ "@mariozechner/pi-tui": "*",
37
+ "@sinclair/typebox": "*"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
package/project-id.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { execSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import type { ProjectIdResult } from "./types.ts";
4
+
5
+ /**
6
+ * Normalize a git remote URL to a slug.
7
+ * https://github.com/acme-org/my-app.git → github-acme-org-my-app
8
+ * git@github.com:acme-org/my-app.git → github-acme-org-my-app
9
+ */
10
+ export function normalizeRemoteUrl(url: string): string {
11
+ let normalized = url.trim();
12
+
13
+ // SSH: git@github.com:org/repo.git → github.com/org/repo.git
14
+ const sshMatch = normalized.match(/^[\w-]+@([\w.-]+):(.*)/);
15
+ if (sshMatch) {
16
+ normalized = `${sshMatch[1]}/${sshMatch[2]}`;
17
+ }
18
+
19
+ // Strip protocol
20
+ normalized = normalized.replace(/^https?:\/\//, "");
21
+ normalized = normalized.replace(/^ssh:\/\//, "");
22
+
23
+ // Strip trailing .git
24
+ normalized = normalized.replace(/\.git$/, "");
25
+
26
+ // Strip trailing slashes
27
+ normalized = normalized.replace(/\/+$/, "");
28
+
29
+ // Strip user@ prefix (e.g. git@)
30
+ normalized = normalized.replace(/^[\w-]+@/, "");
31
+
32
+ // Replace non-alphanumeric with hyphens, collapse, trim
33
+ return normalized
34
+ .replace(/[^a-zA-Z0-9]+/g, "-")
35
+ .replace(/^-+|-+$/g, "")
36
+ .toLowerCase();
37
+ }
38
+
39
+ function shortHash(input: string): string {
40
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
41
+ }
42
+
43
+ function execGit(command: string, cwd: string): string | null {
44
+ try {
45
+ return execSync(command, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Resolve project ID from cwd, with git worktree support.
53
+ *
54
+ * Priority:
55
+ * 1. git remote origin URL → slug
56
+ * 2. Root commit hash → commit-{hash8}
57
+ * 3. cwd path → local-{hash8}
58
+ */
59
+ export function resolveProjectId(cwd: string): ProjectIdResult {
60
+ // 1. Try git remote origin
61
+ const remoteUrl = execGit("git remote get-url origin", cwd);
62
+ if (remoteUrl) {
63
+ return { id: normalizeRemoteUrl(remoteUrl), basis: "remote" };
64
+ }
65
+
66
+ // 2. Try root commit hash (git repo without remote)
67
+ const rootCommit = execGit("git rev-list --max-parents=0 HEAD", cwd);
68
+ if (rootCommit) {
69
+ const firstLine = rootCommit.split("\n")[0]?.trim();
70
+ if (firstLine) {
71
+ return { id: `commit-${firstLine.slice(0, 8)}`, basis: "commit" };
72
+ }
73
+ }
74
+
75
+ // 3. Fallback: cwd path hash
76
+ return { id: `local-${shortHash(cwd)}`, basis: "path" };
77
+ }