@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/README.md +48 -0
- package/index.ts +698 -0
- package/inject.ts +49 -0
- package/package.json +42 -0
- package/project-id.ts +77 -0
- package/storage.ts +680 -0
- package/types.ts +72 -0
- package/ui.ts +480 -0
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
|
+
}
|