feat(memory): 5 TS tools with auto-setup and fallback (#101)
* feat(memory): memory-save tool with auto-setup * feat(memory): memory-search with keyword and semantic fallback * feat(memory): memory-list and memory-access tools * feat(memory): memory-doctor diagnostic tool * docs(handoff): add handoff and ADR for ts memory tools * docs(handoff): set PR number * docs(handoff): finalize pr-101 frontmatter and pending * docs(project-map): add 5 memory TS tools after PR#101 * refactor(memory): extract shared helpers to _memory-shared.ts * refactor(memory): split oversized execute functions * fix(memory): handle git checkout and catch blocks --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
a6491df5a6
commit
45bbdc50f3
11 changed files with 1912 additions and 2 deletions
974
.opencode/package-lock.json
generated
Normal file
974
.opencode/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,974 @@
|
|||
{
|
||||
"name": ".opencode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.18.3",
|
||||
"@vscode/ripgrep": "^1.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.1",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
|
||||
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz",
|
||||
"integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz",
|
||||
"integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz",
|
||||
"integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.3.tgz",
|
||||
"integrity": "sha512-hAm/hZkSCsMSNHVy9DKmFtZAve/qYoIWpduNPcvXnnKK7NMlJEOQitA6CQC67Ym5DFKypDW1TC9YO6S1WdGtpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "1.18.3",
|
||||
"effect": "4.0.0-beta.83",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.4.3",
|
||||
"@opentui/keymap": ">=0.4.3",
|
||||
"@opentui/solid": ">=0.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/keymap": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.3.tgz",
|
||||
"integrity": "sha512-Mevo4e6kQwbvto9E+42KSIVMhp+JBu+SwQhC5AomAvrV6Xkio3U249T+xDILDCXhl5Z/Hi/DlAuVLzpGnuh0gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
||||
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/ripgrep": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.18.0.tgz",
|
||||
"integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@vscode/ripgrep-darwin-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-darwin-x64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-arm": "1.18.0",
|
||||
"@vscode/ripgrep-linux-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-ia32": "1.18.0",
|
||||
"@vscode/ripgrep-linux-ppc64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-riscv64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-s390x": "1.18.0",
|
||||
"@vscode/ripgrep-linux-x64": "1.18.0",
|
||||
"@vscode/ripgrep-win32-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-win32-ia32": "1.18.0",
|
||||
"@vscode/ripgrep-win32-x64": "1.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-darwin-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-arm64/-/ripgrep-darwin-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-darwin-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-x64/-/ripgrep-darwin-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-arm": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm/-/ripgrep-linux-arm-1.18.0.tgz",
|
||||
"integrity": "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm64/-/ripgrep-linux-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-ia32": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ia32/-/ripgrep-linux-ia32-1.18.0.tgz",
|
||||
"integrity": "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-ppc64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ppc64/-/ripgrep-linux-ppc64-1.18.0.tgz",
|
||||
"integrity": "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-riscv64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-riscv64/-/ripgrep-linux-riscv64-1.18.0.tgz",
|
||||
"integrity": "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-s390x": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-s390x/-/ripgrep-linux-s390x-1.18.0.tgz",
|
||||
"integrity": "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-x64/-/ripgrep-linux-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-arm64/-/ripgrep-win32-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-ia32": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-ia32/-/ripgrep-win32-ia32-1.18.0.tgz",
|
||||
"integrity": "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-x64/-/ripgrep-win32-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "4.0.0-beta.83",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz",
|
||||
"integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-check": "^4.8.0",
|
||||
"find-my-way-ts": "^0.1.6",
|
||||
"ini": "^7.0.0",
|
||||
"kubernetes-types": "^1.30.0",
|
||||
"msgpackr": "^2.0.1",
|
||||
"multipasta": "^0.2.7",
|
||||
"toml": "^4.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz",
|
||||
"integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-my-way-ts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz",
|
||||
"integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/kubernetes-types": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz",
|
||||
"integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz",
|
||||
"integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/multipasta": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz",
|
||||
"integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
|
||||
"integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz",
|
||||
"integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
|
||||
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
|
||||
"integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,11 @@
|
|||
"test": "bun test config/plugins/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.18.3"
|
||||
"@opencode-ai/plugin": "1.18.3",
|
||||
"@vscode/ripgrep": "^1.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.1",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
.opencode/tools/_memory-shared.ts
Normal file
137
.opencode/tools/_memory-shared.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { spawnSync } from "child_process"
|
||||
import { createRequire } from "node:module"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
const require2 = createRequire(import.meta.url)
|
||||
|
||||
/**
|
||||
* Resolve the memory directory from OPENCODE_MEMORY_DIR env var or fall back
|
||||
* to the default ~/.local/share/opencode/opencode-memory. Identical behaviour
|
||||
* across all 5 memory tools — previously duplicated 5×.
|
||||
*/
|
||||
export function resolveMemoryDir(): string {
|
||||
const env = process.env.OPENCODE_MEMORY_DIR
|
||||
if (env && env.length > 0) return path.normalize(env)
|
||||
return path.join(os.homedir(), ".local/share/opencode/opencode-memory")
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory categories (also the directories auto-created by memory-save).
|
||||
* Previously duplicated in memory-save and memory-list.
|
||||
*/
|
||||
export const CATEGORIES = [
|
||||
"preferences",
|
||||
"repos",
|
||||
"technical",
|
||||
"people",
|
||||
"workflows",
|
||||
"snippets",
|
||||
"notes",
|
||||
]
|
||||
|
||||
/**
|
||||
* Frontmatter delimiter regex (flat YAML between --- fences). Previously
|
||||
* duplicated in memory-search, memory-list, memory-access.
|
||||
*/
|
||||
export const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/
|
||||
|
||||
export interface FrontmatterMeta {
|
||||
title?: string
|
||||
summary?: string
|
||||
importance?: string
|
||||
updated?: string
|
||||
tags?: string[]
|
||||
related?: string[]
|
||||
access_count?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse flat frontmatter (regex per line, NOT real YAML — see memory note
|
||||
* "Frontmatter parser is regex, not YAML"). Returns typed meta; callers
|
||||
* ignore fields they don't use. Previously duplicated in memory-search (full)
|
||||
* and memory-list (partial) — unified to the full version.
|
||||
*/
|
||||
export function parseFrontmatter(content: string): FrontmatterMeta {
|
||||
const match = content.match(FRONTMATTER_RE)
|
||||
if (!match) return {}
|
||||
const yaml = match[1]
|
||||
const meta: FrontmatterMeta = {}
|
||||
for (const line of yaml.split("\n")) {
|
||||
const kv = line.match(/^(\w+):\s*(.+)$/)
|
||||
if (!kv) continue
|
||||
const [, key, rawValue] = kv
|
||||
const value = rawValue.trim()
|
||||
if (key === "title") meta.title = value
|
||||
else if (key === "summary") meta.summary = value
|
||||
else if (key === "importance") meta.importance = value
|
||||
else if (key === "updated") meta.updated = value
|
||||
else if (key === "access_count") {
|
||||
const parsed = parseInt(value, 10)
|
||||
meta.access_count = Number.isFinite(parsed) ? parsed : 0
|
||||
} else if (key === "tags" || key === "related") {
|
||||
const arr = value.match(/\[([^\]]*)\]/)
|
||||
if (arr && arr[1].trim()) {
|
||||
meta[key] = arr[1].split(",").map((t) => t.trim()).filter(Boolean)
|
||||
} else {
|
||||
meta[key] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ripgrep binary path. Checks @vscode/ripgrep npm package first
|
||||
* (platform-specific binary). When `allowSystemFallback` is true (memory-search),
|
||||
* falls back to a system `rg` on $PATH. When false (memory-doctor), returns
|
||||
* null if the npm package is unavailable so the doctor can report the two
|
||||
* cases separately. Previously duplicated with divergent behaviour.
|
||||
*/
|
||||
export function resolveRgBinary(opts?: { allowSystemFallback?: boolean }): string | null {
|
||||
try {
|
||||
const mod = require2("@vscode/ripgrep")
|
||||
if (typeof mod.rgPath === "string" && mod.rgPath.length > 0 && fs.existsSync(mod.rgPath)) {
|
||||
return mod.rgPath
|
||||
}
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory] @vscode/ripgrep require failed: ${(e as Error).message}\n`)
|
||||
}
|
||||
if (opts?.allowSystemFallback) {
|
||||
const sys = spawnSync("rg", ["--version"], { encoding: "utf-8" })
|
||||
if (sys.status === 0) return "rg"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect absolute paths of *.md files under `dir`, skipping
|
||||
* dotfiles, .git/, .rag/, and INDEX.md. Previously duplicated in memory-list
|
||||
* (collecting relative paths) and memory-doctor (counting only); unified to
|
||||
* return absolute paths — callers convert with path.relative() as needed.
|
||||
*/
|
||||
export function walkMd(dir: string): string[] {
|
||||
const out: string[] = []
|
||||
const walk = (d: string) => {
|
||||
let entries: fs.Dirent[]
|
||||
try {
|
||||
entries = fs.readdirSync(d, { withFileTypes: true })
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory] walkMd readdir failed on ${d}: ${(e as Error).message}\n`)
|
||||
return
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (e.name.startsWith(".")) continue
|
||||
const full = path.join(d, e.name)
|
||||
if (e.isDirectory()) {
|
||||
if (e.name === ".git" || e.name === ".rag") continue
|
||||
walk(full)
|
||||
} else if (e.isFile() && e.name.endsWith(".md") && e.name !== "INDEX.md") {
|
||||
out.push(full)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir)
|
||||
return out
|
||||
}
|
||||
66
.opencode/tools/memory-access.ts
Normal file
66
.opencode/tools/memory-access.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { FRONTMATTER_RE, resolveMemoryDir } from "./_memory-shared"
|
||||
|
||||
function todayISO(): string {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function bumpAccessFields(yaml: string, dateStr: string): { yaml: string; newCount: number } {
|
||||
const countMatch = yaml.match(/^access_count:\s*(\d+)/m)
|
||||
const currentCount = countMatch ? parseInt(countMatch[1], 10) : 0
|
||||
const newCount = currentCount + 1
|
||||
let updated = yaml
|
||||
if (updated.match(/^last_accessed:/m)) {
|
||||
updated = updated.replace(/^last_accessed:.*$/m, `last_accessed: ${dateStr}`)
|
||||
} else {
|
||||
updated += `\nlast_accessed: ${dateStr}`
|
||||
}
|
||||
if (updated.match(/^access_count:/m)) {
|
||||
updated = updated.replace(/^access_count:.*$/m, `access_count: ${newCount}`)
|
||||
} else {
|
||||
updated += `\naccess_count: ${newCount}`
|
||||
}
|
||||
return { yaml: updated, newCount }
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Record that a memory file was accessed (read and used). Updates last_accessed date and increments access_count in frontmatter. " +
|
||||
"Call this AFTER reading a memory file that you actually used to inform your work — not for casual browsing. " +
|
||||
"This helps the memory system track which memories are actively useful vs. stale. " +
|
||||
"Atomic write (tmp + rename). Does NOT commit — the next memory_save will sync the change.",
|
||||
args: {
|
||||
path: tool.schema
|
||||
.string()
|
||||
.describe("Relative path within the memory directory (e.g. 'technical/build-tooling.md')"),
|
||||
},
|
||||
async execute(args) {
|
||||
const memoryDir = resolveMemoryDir()
|
||||
const filePath = path.join(memoryDir, args.path)
|
||||
if (!fs.existsSync(filePath)) return `Could not update ${args.path} (file not found)`
|
||||
let content: string
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8")
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-access] read failed ${args.path}: ${(e as Error).message}\n`)
|
||||
return `Could not update ${args.path}`
|
||||
}
|
||||
const fmMatch = content.match(FRONTMATTER_RE)
|
||||
if (!fmMatch) return `No frontmatter in ${args.path} — skipped`
|
||||
const yaml = fmMatch[1]
|
||||
const body = fmMatch[2]
|
||||
const { yaml: updatedYaml, newCount } = bumpAccessFields(yaml, todayISO())
|
||||
const newContent = `---\n${updatedYaml}\n---\n${body}`
|
||||
try {
|
||||
const tmp = filePath + ".tmp"
|
||||
fs.writeFileSync(tmp, newContent, "utf-8")
|
||||
fs.renameSync(tmp, filePath)
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-access] write failed ${args.path}: ${(e as Error).message}\n`)
|
||||
return `Could not update ${args.path}`
|
||||
}
|
||||
return `Accessed: ${args.path}, count=${newCount}`
|
||||
},
|
||||
})
|
||||
128
.opencode/tools/memory-doctor.ts
Normal file
128
.opencode/tools/memory-doctor.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { spawnSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { resolveMemoryDir, resolveRgBinary, walkMd } from "./_memory-shared"
|
||||
|
||||
function mark(ok: boolean): string {
|
||||
return ok ? "✅" : "❌"
|
||||
}
|
||||
|
||||
function checkRipgrep(rgBin: string | null): string[] {
|
||||
const lines: string[] = []
|
||||
if (rgBin) {
|
||||
const r = spawnSync(rgBin, ["--version"], { encoding: "utf-8" })
|
||||
const ver = (r.stdout || "").split("\n")[0].trim()
|
||||
lines.push(`- ${mark(r.status === 0)} ripgrep (keyword search): ${rgBin}${ver ? ` — ${ver}` : ""}`)
|
||||
} else {
|
||||
const sys = spawnSync("rg", ["--version"], { encoding: "utf-8" })
|
||||
if (sys.status === 0) {
|
||||
lines.push(`- ${mark(true)} ripgrep (keyword search): system rg on $PATH`)
|
||||
} else {
|
||||
lines.push(`- ❌ ripgrep (keyword search): NOT resolvable — install @vscode/ripgrep or system rg`)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
function checkPythonMemory(worktree: string): { lines: string[]; ok: boolean } {
|
||||
const pyR = spawnSync("python3", ["-c", "import src.memory; print('ok')"], {
|
||||
encoding: "utf-8",
|
||||
cwd: worktree,
|
||||
})
|
||||
const lines = [
|
||||
`- ${mark(pyR.status === 0)} Python src.memory importable (semantic search): ${
|
||||
pyR.status === 0 ? "yes" : `no — ${(pyR.stderr || pyR.stdout || "").trim().split("\n").pop() || ""}`
|
||||
}`,
|
||||
]
|
||||
return { lines, ok: pyR.status === 0 }
|
||||
}
|
||||
|
||||
function checkEnvVars(): string[] {
|
||||
return [
|
||||
`- ${mark(!!process.env.OPENAI_BASE_URL)} OPENAI_BASE_URL set (embeddings endpoint): ${
|
||||
process.env.OPENAI_BASE_URL ? "✓" : "not set — semantic search disabled, keyword still works"
|
||||
}`,
|
||||
`- ${mark(!!process.env.OPENAI_API_KEY)} OPENAI_API_KEY set (embeddings auth): ${
|
||||
process.env.OPENAI_API_KEY ? "✓" : "not set"
|
||||
}`,
|
||||
`- ${mark(!!process.env.OPENCODE_MEMORY_REMOTE)} OPENCODE_MEMORY_REMOTE set (auto-push to git): ${
|
||||
process.env.OPENCODE_MEMORY_REMOTE ? "✓" : "not set — auto-push disabled"
|
||||
}`,
|
||||
]
|
||||
}
|
||||
|
||||
function checkIndex(memoryDir: string): string[] {
|
||||
const lines: string[] = []
|
||||
lines.push(
|
||||
`- ${mark(fs.existsSync(memoryDir))} OPENCODE_MEMORY_DIR exists: ${memoryDir}${
|
||||
fs.existsSync(memoryDir) ? "" : "— run memory_save to auto-setup"
|
||||
}`
|
||||
)
|
||||
if (!fs.existsSync(memoryDir)) return lines
|
||||
|
||||
const gitDir = path.join(memoryDir, ".git")
|
||||
lines.push(
|
||||
`- ${mark(fs.existsSync(gitDir))} memory dir is a git repo: ${
|
||||
fs.existsSync(gitDir) ? "yes" : "no — will be init on first memory_save"
|
||||
}`
|
||||
)
|
||||
const indexJson = path.join(memoryDir, ".rag", "index.json")
|
||||
lines.push(
|
||||
`- ${mark(fs.existsSync(indexJson))} RAG index exists: ${
|
||||
fs.existsSync(indexJson)
|
||||
? path.relative(memoryDir, indexJson)
|
||||
: "not built — first memory_save with OPENAI_BASE_URL will trigger reindex"
|
||||
}`
|
||||
)
|
||||
if (fs.existsSync(indexJson)) {
|
||||
try {
|
||||
const stat = fs.statSync(indexJson)
|
||||
const sizeMb = (stat.size / 1024 / 1024).toFixed(1)
|
||||
lines.push(` (size: ${sizeMb} MB, modified: ${stat.mtime.toISOString().slice(0, 10)})`)
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-doctor] stat index.json failed: ${(e as Error).message}\n`)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const mdCount = walkMd(memoryDir).length
|
||||
lines.push(`- ℹ️ memory files (.md): ${mdCount}`)
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-doctor] walkMd failed: ${(e as Error).message}\n`)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
function formatDoctorReport(rgBin: string | null, pyOk: boolean, memoryDir: string): string {
|
||||
const indexJson = path.join(memoryDir, ".rag", "index.json")
|
||||
const allGreen =
|
||||
rgBin !== null && pyOk && fs.existsSync(memoryDir) && fs.existsSync(indexJson)
|
||||
if (allGreen) return "All green — both keyword and semantic search available."
|
||||
if (rgBin !== null) return "Keyword search works. Semantic search unavailable — see ❌ items above."
|
||||
return "ripgrep missing — keyword search will return no results. Install @vscode/ripgrep."
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Read-only diagnostic for the memory subsystem. Reports: ripgrep (keyword) availability, Python src.memory importability, " +
|
||||
"env vars (OPENAI_BASE_URL, OPENAI_API_KEY, OPENCODE_MEMORY_REMOTE), memory dir existence, RAG index existence. " +
|
||||
"Does NOT modify anything. Run when memory_search/memory_save misbehave or to verify setup.",
|
||||
args: {},
|
||||
async execute(_args, context) {
|
||||
const memoryDir = resolveMemoryDir()
|
||||
const lines: string[] = ["## Memory Doctor\n"]
|
||||
|
||||
const rgBin = resolveRgBinary()
|
||||
lines.push(...checkRipgrep(rgBin))
|
||||
|
||||
const py = checkPythonMemory(context.worktree)
|
||||
lines.push(...py.lines)
|
||||
|
||||
lines.push(...checkEnvVars())
|
||||
lines.push(...checkIndex(memoryDir))
|
||||
|
||||
lines.push("")
|
||||
lines.push(formatDoctorReport(rgBin, py.ok, memoryDir))
|
||||
return lines.join("\n")
|
||||
},
|
||||
})
|
||||
65
.opencode/tools/memory-list.ts
Normal file
65
.opencode/tools/memory-list.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { CATEGORIES, parseFrontmatter, resolveMemoryDir, walkMd } from "./_memory-shared"
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Browse memories in the memory directory. Without a category, lists all categories with file counts. " +
|
||||
"With a category, lists files in that category with their summaries (parsed from frontmatter). " +
|
||||
"Pure TS, no subprocess. Categories: preferences, repos, technical, people, workflows, snippets, notes.",
|
||||
args: {
|
||||
category: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Category to list: preferences, repos, technical, people, workflows, snippets, notes"),
|
||||
},
|
||||
async execute(args) {
|
||||
const memoryDir = resolveMemoryDir()
|
||||
if (!args.category) {
|
||||
const lines = ["## Memory Categories\n"]
|
||||
for (const cat of CATEGORIES) {
|
||||
const catDir = path.join(memoryDir, cat)
|
||||
const files = fs.existsSync(catDir) ? walkMd(catDir) : []
|
||||
lines.push(`- **${cat}/** (${files.length} files)`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
const catDir = path.join(memoryDir, args.category)
|
||||
if (!fs.existsSync(catDir)) return `Category not found: ${args.category}`
|
||||
const absFiles = walkMd(catDir)
|
||||
if (absFiles.length === 0) return `No memories in category: ${args.category}`
|
||||
|
||||
interface Row {
|
||||
path: string
|
||||
title: string
|
||||
summary: string
|
||||
importance: string
|
||||
updated: string
|
||||
}
|
||||
const rows: Row[] = []
|
||||
for (const abs of absFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(abs, "utf-8")
|
||||
const meta = parseFrontmatter(content)
|
||||
const rel = path.relative(catDir, abs)
|
||||
rows.push({
|
||||
path: `${args.category}/${rel}`,
|
||||
title: meta.title || rel.replace(/\.md$/, ""),
|
||||
summary: meta.summary || "",
|
||||
importance: meta.importance || "medium",
|
||||
updated: meta.updated || "",
|
||||
})
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-list] read failed ${abs}: ${(e as Error).message}\n`)
|
||||
}
|
||||
}
|
||||
rows.sort((a, b) => (b.updated || "").localeCompare(a.updated || ""))
|
||||
const lines = [`## ${args.category}/ (${rows.length} files)\n`]
|
||||
for (const f of rows) {
|
||||
lines.push(`- **${f.path}** [${f.importance}] ${f.updated ? `(${f.updated})` : ""}`)
|
||||
if (f.summary) lines.push(` ${f.summary}`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
},
|
||||
})
|
||||
119
.opencode/tools/memory-save.ts
Normal file
119
.opencode/tools/memory-save.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { spawn, spawnSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { CATEGORIES, resolveMemoryDir } from "./_memory-shared"
|
||||
|
||||
const HOOK_BODY = "#!/bin/bash\ngit push origin master 2>/dev/null || true\n"
|
||||
|
||||
function run(cmd: string, args: string[], opts?: { cwd?: string }) {
|
||||
return spawnSync(cmd, args, { encoding: "utf-8", ...opts })
|
||||
}
|
||||
|
||||
function isEmptyDir(dir: string): boolean {
|
||||
if (!fs.existsSync(dir)) return true
|
||||
return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0
|
||||
}
|
||||
|
||||
function autoSetup(memoryDir: string, remote: string | undefined): string[] {
|
||||
const steps: string[] = []
|
||||
if (!fs.existsSync(memoryDir)) {
|
||||
fs.mkdirSync(memoryDir, { recursive: true })
|
||||
steps.push(`created directory: ${memoryDir}`)
|
||||
}
|
||||
if (!fs.existsSync(path.join(memoryDir, ".git"))) {
|
||||
if (remote && isEmptyDir(memoryDir)) {
|
||||
const r = run("git", ["clone", "--origin", "origin", remote, memoryDir])
|
||||
if (r.status !== 0) {
|
||||
run("git", ["init", memoryDir])
|
||||
steps.push(`git init (clone failed: ${(r.stderr || "").trim()})`)
|
||||
} else {
|
||||
const co = run("git", ["-C", memoryDir, "checkout", "master"])
|
||||
if (co.status !== 0) {
|
||||
process.stderr.write(
|
||||
`[memory-save] git checkout master failed (exit ${co.status}): ${(co.stderr || co.stdout || "").trim()}\n`
|
||||
)
|
||||
steps.push("cloned remote (checkout master failed — check stderr)")
|
||||
} else {
|
||||
steps.push(`cloned remote: ${remote}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
run("git", ["init", memoryDir])
|
||||
steps.push("git init")
|
||||
}
|
||||
}
|
||||
if (remote) {
|
||||
const hookPath = path.join(memoryDir, ".git", "hooks", "post-commit")
|
||||
let needHook = true
|
||||
if (fs.existsSync(hookPath)) {
|
||||
if (fs.readFileSync(hookPath, "utf-8") === HOOK_BODY) needHook = false
|
||||
}
|
||||
if (needHook) {
|
||||
fs.mkdirSync(path.dirname(hookPath), { recursive: true })
|
||||
fs.writeFileSync(hookPath, HOOK_BODY, { mode: 0o755 })
|
||||
steps.push("installed post-commit hook (auto-push)")
|
||||
}
|
||||
const cur = run("git", ["-C", memoryDir, "remote", "get-url", "origin"])
|
||||
if (cur.status !== 0 || (cur.stdout || "").trim() !== remote) {
|
||||
run("git", ["-C", memoryDir, "remote", "set-url", "origin", remote])
|
||||
steps.push(`set remote origin: ${remote}`)
|
||||
}
|
||||
}
|
||||
for (const cat of CATEGORIES) {
|
||||
const catDir = path.join(memoryDir, cat)
|
||||
if (!fs.existsSync(catDir)) fs.mkdirSync(catDir, { recursive: true })
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Commit and re-index all pending memory changes. Call AFTER using Write/Edit tools on ~/.local/share/opencode/opencode-memory/{category}/{filename}.md. " +
|
||||
"Handles: auto-setup (mkdir + git init/clone + hook) on first call, git add -A, commit (message derived from changed files), async RAG re-index (fire-and-forget, log to .rag/reindex.log). " +
|
||||
"WHEN TO SAVE (always save when you discover something reusable): learned non-obvious API pattern/tool quirk/workaround, discovered repo structure/conventions/gotchas, resolved tricky debugging with non-obvious root cause, learned team/ownership info. " +
|
||||
"DO NOT save: one-off answers, things in public docs, context only relevant to the current task. " +
|
||||
"CATEGORIES: preferences, repos, technical, people, workflows, snippets, notes. " +
|
||||
"FRONTMATTER FORMAT: ---\\ntitle: ...\\ntags: [...]\\nsummary: ...\\ncreated: YYYY-MM-DD\\nupdated: YYYY-MM-DD\\nimportance: high|medium|low\\nrelated: [...]\\n---",
|
||||
args: {},
|
||||
async execute(_args, context) {
|
||||
const memoryDir = resolveMemoryDir()
|
||||
const remote = process.env.OPENCODE_MEMORY_REMOTE
|
||||
const setupSteps = autoSetup(memoryDir, remote)
|
||||
|
||||
const addR = run("git", ["-C", memoryDir, "add", "-A"])
|
||||
if (addR.status !== 0) {
|
||||
return `⚠️ memory-save: git add failed (exit ${addR.status}): ${addR.stderr || addR.stdout}`
|
||||
}
|
||||
const diffR = run("git", ["-C", memoryDir, "diff", "--name-only", "--cached"])
|
||||
if (diffR.status !== 0) {
|
||||
return `⚠️ memory-save: git diff failed (exit ${diffR.status}): ${diffR.stderr || diffR.stdout}`
|
||||
}
|
||||
const changed = (diffR.stdout || "").trim().split("\n").filter(Boolean)
|
||||
if (changed.length === 0) return "No changes to sync"
|
||||
|
||||
const msg = `memory: sync ${changed.join(", ")}`
|
||||
const commitR = run("git", ["-C", memoryDir, "commit", "-m", msg])
|
||||
if (commitR.status !== 0) {
|
||||
return `⚠️ memory-save: git commit failed (exit ${commitR.status}): ${commitR.stderr || commitR.stdout}`
|
||||
}
|
||||
|
||||
if (process.env.OPENAI_BASE_URL) {
|
||||
const indexDir = path.join(memoryDir, ".rag")
|
||||
fs.mkdirSync(indexDir, { recursive: true })
|
||||
const child = spawn("python3", ["-m", "src.memory", "index", memoryDir, "-o", indexDir], {
|
||||
cwd: context.worktree,
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
})
|
||||
child.unref()
|
||||
}
|
||||
|
||||
const parts = [`Synced: ${changed.join(", ")}`]
|
||||
if (setupSteps.length > 0) parts.push(`Auto-setup: ${setupSteps.join("; ")}`)
|
||||
if (!process.env.OPENAI_BASE_URL) {
|
||||
parts.push("(semantic index skipped: OPENAI_BASE_URL not set — keyword search still works)")
|
||||
}
|
||||
return parts.join("\n")
|
||||
},
|
||||
})
|
||||
335
.opencode/tools/memory-search.ts
Normal file
335
.opencode/tools/memory-search.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import { spawnSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import {
|
||||
parseFrontmatter,
|
||||
resolveMemoryDir,
|
||||
resolveRgBinary,
|
||||
type FrontmatterMeta,
|
||||
} from "./_memory-shared"
|
||||
|
||||
const STOP_WORDS = new Set(["the", "a", "an", "is", "of", "to", "in", "for", "on", "and", "or"])
|
||||
|
||||
function parseSearchTerms(query: string): string[] {
|
||||
const terms: string[] = []
|
||||
const regex = /"([^"]+)"|(\S+)/g
|
||||
let match
|
||||
while ((match = regex.exec(query)) !== null) {
|
||||
const original = match[1] || match[2]
|
||||
const lower = original.toLowerCase()
|
||||
if (lower.length < 2) continue
|
||||
if (STOP_WORDS.has(lower)) continue
|
||||
terms.push(original)
|
||||
}
|
||||
return terms
|
||||
}
|
||||
|
||||
function countTermMatches(text: string, terms: string[]): number {
|
||||
if (terms.length === 0) return 0
|
||||
const lowerText = text.toLowerCase()
|
||||
return terms.filter((t) => lowerText.includes(t.toLowerCase())).length
|
||||
}
|
||||
|
||||
interface ScoreInput {
|
||||
rgMatch: boolean
|
||||
ragScore?: number
|
||||
termMatches: number
|
||||
totalTerms: number
|
||||
tags?: string[]
|
||||
path: string
|
||||
importance?: string
|
||||
accessCount?: number
|
||||
terms: string[]
|
||||
}
|
||||
|
||||
function scoreCandidate(input: ScoreInput): number {
|
||||
let score = 0
|
||||
if (input.rgMatch) {
|
||||
const termCoverage = input.totalTerms > 0 ? input.termMatches / input.totalTerms : 1
|
||||
score += 0.15 + 0.35 * termCoverage
|
||||
}
|
||||
if (input.ragScore) score += input.ragScore * 1.4
|
||||
if (input.rgMatch && input.ragScore) score += 0.1
|
||||
if (input.tags && input.tags.length > 0 && input.terms.length > 0) {
|
||||
const lowerTags = input.tags.map((t) => t.toLowerCase())
|
||||
const tagHits = input.terms.filter((t) =>
|
||||
lowerTags.some((tag) => tag.includes(t.toLowerCase()))
|
||||
).length
|
||||
if (tagHits > 0) score += 0.2 * (tagHits / input.terms.length)
|
||||
}
|
||||
const pathLower = input.path.toLowerCase().replace(/[-_/.]/g, " ")
|
||||
const pathHits = input.terms.filter((t) => pathLower.includes(t.toLowerCase())).length
|
||||
if (pathHits > 0 && input.terms.length > 0) {
|
||||
score += 0.15 * (pathHits / input.terms.length)
|
||||
}
|
||||
if (input.importance === "high") score += 0.15
|
||||
else if (input.importance === "low") score -= 0.1
|
||||
if (input.accessCount && input.accessCount >= 5) score += 0.1
|
||||
else if (input.accessCount && input.accessCount >= 2) score += 0.05
|
||||
return score
|
||||
}
|
||||
|
||||
interface RagHit {
|
||||
source: string
|
||||
score: number
|
||||
text: string
|
||||
}
|
||||
|
||||
function parseRagHits(ragText: string): RagHit[] {
|
||||
if (!ragText) return []
|
||||
try {
|
||||
const parsed = JSON.parse(ragText)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed.filter(
|
||||
(hit): hit is RagHit =>
|
||||
hit &&
|
||||
typeof hit.source === "string" &&
|
||||
typeof hit.score === "number" &&
|
||||
typeof hit.text === "string"
|
||||
)
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-search] parseRagHits JSON parse failed: ${(e as Error).message}\n`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function buildRgArgs(terms: string[]): string[] {
|
||||
const args = ["-il", "--glob", "*.md", "--glob", "!.git", "--glob", "!.rag", "--glob", "!**/INDEX.md"]
|
||||
for (const term of terms) args.push("-e", term)
|
||||
return args
|
||||
}
|
||||
|
||||
function toRelPath(memoryDir: string, absPath: string): string {
|
||||
return path.relative(memoryDir, path.normalize(absPath))
|
||||
}
|
||||
|
||||
interface CandInfo {
|
||||
rgMatch: boolean
|
||||
ragScore?: number
|
||||
ragText?: string
|
||||
}
|
||||
|
||||
function runKeywordSearch(
|
||||
rgBin: string,
|
||||
terms: string[],
|
||||
searchDir: string,
|
||||
memoryDir: string
|
||||
): Map<string, CandInfo> {
|
||||
const resultMap = new Map<string, CandInfo>()
|
||||
const rgR = spawnSync(rgBin, [...buildRgArgs(terms), searchDir], { encoding: "utf-8" })
|
||||
if (rgR.status === 0) {
|
||||
for (const line of (rgR.stdout || "").trim().split("\n").filter(Boolean)) {
|
||||
const rel = toRelPath(memoryDir, line)
|
||||
if (rel.endsWith(".md") && !rel.startsWith(".")) {
|
||||
resultMap.set(rel, { rgMatch: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
function runSemanticSearch(
|
||||
query: string,
|
||||
indexDir: string,
|
||||
worktree: string
|
||||
): RagHit[] {
|
||||
if (!process.env.OPENAI_BASE_URL) return []
|
||||
if (!fs.existsSync(path.join(indexDir, "index.json"))) return []
|
||||
const r = spawnSync(
|
||||
"python3",
|
||||
["-m", "src.memory", "search", query, "-i", indexDir, "-k", "15", "--json"],
|
||||
{ cwd: worktree, encoding: "utf-8" }
|
||||
)
|
||||
if (r.status !== 0) {
|
||||
if (r.stderr) process.stderr.write(`[memory-search] rag search failed (exit ${r.status}): ${r.stderr.trim()}\n`)
|
||||
return []
|
||||
}
|
||||
return parseRagHits(r.stdout || "")
|
||||
}
|
||||
|
||||
function mergeRagHits(
|
||||
resultMap: Map<string, CandInfo>,
|
||||
ragHits: RagHit[],
|
||||
category?: string
|
||||
): void {
|
||||
for (const hit of ragHits) {
|
||||
if (hit.source.endsWith("INDEX.md")) continue
|
||||
if (category && !hit.source.startsWith(category + "/")) continue
|
||||
const existing = resultMap.get(hit.source) || { rgMatch: false }
|
||||
if (!existing.ragScore || hit.score > existing.ragScore) {
|
||||
existing.ragScore = hit.score
|
||||
existing.ragText = hit.text
|
||||
}
|
||||
resultMap.set(hit.source, existing)
|
||||
}
|
||||
}
|
||||
|
||||
interface ResultRow {
|
||||
path: string
|
||||
meta: FrontmatterMeta
|
||||
score: number
|
||||
termMatches: number
|
||||
rgMatch: boolean
|
||||
ragScore?: number
|
||||
ragText?: string
|
||||
}
|
||||
|
||||
function scoreAndCombine(
|
||||
resultMap: Map<string, CandInfo>,
|
||||
terms: string[],
|
||||
memoryDir: string
|
||||
): ResultRow[] {
|
||||
const results: ResultRow[] = []
|
||||
for (const [p, info] of resultMap) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(memoryDir, p), "utf-8")
|
||||
const meta = parseFrontmatter(content)
|
||||
const termMatches =
|
||||
terms.length > 0 ? countTermMatches(content, terms) : info.rgMatch ? 1 : 0
|
||||
const score = scoreCandidate({
|
||||
rgMatch: info.rgMatch,
|
||||
ragScore: info.ragScore,
|
||||
termMatches,
|
||||
totalTerms: terms.length,
|
||||
tags: meta.tags,
|
||||
path: p,
|
||||
importance: meta.importance,
|
||||
accessCount: meta.access_count,
|
||||
terms,
|
||||
})
|
||||
results.push({
|
||||
path: p,
|
||||
meta,
|
||||
score,
|
||||
termMatches,
|
||||
rgMatch: info.rgMatch,
|
||||
ragScore: info.ragScore,
|
||||
ragText: info.ragText,
|
||||
})
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-search] score read failed ${p}: ${(e as Error).message}\n`)
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => b.score - a.score)
|
||||
return results
|
||||
}
|
||||
|
||||
function formatResults(
|
||||
results: ResultRow[],
|
||||
query: string,
|
||||
terms: string[],
|
||||
category: string | undefined,
|
||||
limit: number,
|
||||
memoryDir: string
|
||||
): string {
|
||||
const lines = [`## Results for "${query}" (${results.length} matches)`]
|
||||
if (category && results.length > 0) {
|
||||
lines.push(`_Filtered to **${category}/**_`)
|
||||
}
|
||||
if (terms.length > 1) lines.push(`_Searching for: ${terms.join(", ")}_`)
|
||||
|
||||
const filtered = results.filter((r) => r.score >= 0.2)
|
||||
const topResults = filtered.length > 0 ? filtered : results.slice(0, 3)
|
||||
const MAX = Math.min(limit, 7)
|
||||
for (const [i, r] of topResults.slice(0, MAX).entries()) {
|
||||
const isDirectHit =
|
||||
r.rgMatch && r.ragScore !== undefined && r.ragScore > 0.4 && r.termMatches === terms.length
|
||||
const hitLabel = isDirectHit ? " ★ DIRECT HIT" : ""
|
||||
const importance = r.meta.importance || "medium"
|
||||
lines.push(`${i + 1}. **${r.path}** [${importance}]${hitLabel}`)
|
||||
if (r.meta.tags?.length) lines.push(` Tags: ${r.meta.tags.join(", ")}`)
|
||||
if (r.meta.summary) lines.push(` ${r.meta.summary}`)
|
||||
const sources: string[] = []
|
||||
if (r.rgMatch) {
|
||||
const termInfo = terms.length > 1 ? ` (${r.termMatches}/${terms.length} terms)` : ""
|
||||
sources.push(`keyword${termInfo}`)
|
||||
}
|
||||
if (r.ragScore) sources.push(`semantic: ${r.ragScore.toFixed(2)}`)
|
||||
lines.push(` Match: ${sources.join(" + ")} | score: ${r.score.toFixed(2)}`)
|
||||
if (r.meta.related?.length) lines.push(` Related: ${r.meta.related.join(", ")}`)
|
||||
if (r.ragText) lines.push(` Preview: "...${r.ragText.slice(0, 200).trim()}..."`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (topResults.length > 0) {
|
||||
lines.push(`_Read the top result: ${topResults[0].path}_`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
const shownPaths = new Set(topResults.slice(0, 7).map((r) => r.path))
|
||||
const relatedSuggestions: string[] = []
|
||||
for (const r of topResults.slice(0, 7)) {
|
||||
for (const rel of r.meta.related || []) {
|
||||
if (!shownPaths.has(rel) && !relatedSuggestions.includes(rel)) relatedSuggestions.push(rel)
|
||||
}
|
||||
}
|
||||
const verified: string[] = []
|
||||
for (const rel of relatedSuggestions) {
|
||||
try {
|
||||
if (fs.existsSync(path.join(memoryDir, rel))) verified.push(rel)
|
||||
} catch (e) {
|
||||
process.stderr.write(`[memory-search] related exists check failed ${rel}: ${(e as Error).message}\n`)
|
||||
}
|
||||
}
|
||||
if (verified.length > 0) {
|
||||
lines.push("---")
|
||||
lines.push(`**Related files** (not in results — use the Read tool):`)
|
||||
for (const rel of verified) lines.push(` - ${rel}`)
|
||||
lines.push("")
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Search memories in the memory directory using both keyword (ripgrep) and semantic (Python rag) search. " +
|
||||
"Multi-term queries match files containing ANY search term (OR logic); files matching more terms rank higher. " +
|
||||
"Results are summaries only (path, tags, importance, short context). Use the Read tool on the memory path to get the full content. " +
|
||||
"WHEN TO SEARCH (do this BEFORE starting work): starting work on any repo, using unfamiliar tool/API, encountering unfamiliar codebase, debugging a problem, looking up people/team info, before writing new memory. " +
|
||||
"FALLBACK: if semantic search is unavailable (OPENAI_BASE_URL not set, Python crash, no index), keyword-only results are returned — never fails.",
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search terms or natural language query"),
|
||||
category: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter to a category: preferences, repos, technical, people, workflows, snippets, notes"),
|
||||
limit: tool.schema.number().optional().describe("Max results (default 15)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const memoryDir = resolveMemoryDir()
|
||||
const searchDir = args.category ? path.join(memoryDir, args.category) : memoryDir
|
||||
const terms = parseSearchTerms(args.query)
|
||||
const rgTerms = terms.length > 0 ? terms : [args.query]
|
||||
const limit = args.limit ?? 15
|
||||
const rgBin = resolveRgBinary({ allowSystemFallback: true })
|
||||
|
||||
if (!fs.existsSync(memoryDir)) {
|
||||
return `No memories found for: "${args.query}" (memory directory not initialized — run memory_save to auto-setup)`
|
||||
}
|
||||
|
||||
let resultMap = new Map<string, CandInfo>()
|
||||
if (rgBin) {
|
||||
resultMap = runKeywordSearch(rgBin, rgTerms, searchDir, memoryDir)
|
||||
}
|
||||
|
||||
const indexDir = path.join(memoryDir, ".rag")
|
||||
const ragHits = runSemanticSearch(args.query, indexDir, context.worktree)
|
||||
mergeRagHits(resultMap, ragHits, args.category)
|
||||
|
||||
if (resultMap.size === 0 && args.category) {
|
||||
if (rgBin) {
|
||||
const fallback = runKeywordSearch(rgBin, rgTerms, memoryDir, memoryDir)
|
||||
for (const [k, v] of fallback) resultMap.set(k, v)
|
||||
}
|
||||
mergeRagHits(resultMap, ragHits)
|
||||
}
|
||||
|
||||
if (resultMap.size === 0) {
|
||||
return `No memories found for: "${args.query}"`
|
||||
}
|
||||
|
||||
const results = scoreAndCombine(resultMap, terms, memoryDir)
|
||||
return formatResults(results, args.query, terms, args.category, limit, memoryDir)
|
||||
},
|
||||
})
|
||||
39
docs/decisions/043-pr-101-ts-memory-tools.md
Normal file
39
docs/decisions/043-pr-101-ts-memory-tools.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ADR-043: 5 TS memory tools with auto-setup and progressive enhancement
|
||||
|
||||
## Статус
|
||||
Accepted (2026-07-26)
|
||||
|
||||
## Контекст
|
||||
Плагин `@mathew-cf/opencode-memory` (MCP server wrapper) не вызывается из-за бага wrapper-пути: `MEMORY_WRAPPER_PATH` в `setup-memory.sh` указывает на `@mathew-cf/rag-cli/bin/rag.js`, который не существует или несовместим (GLIBC 2.39 vs 2.36 — см. ADR-030, PR#57, PR#71). Плагин деградирует до keyword-only через grep, но wrapper-hop добавляет fragility. Нужны нативные TS tools, вызывающие Python через `spawnSync` (паттерн `pipeline-status.ts`, `commit.ts`, `memory-setup.ts`).
|
||||
|
||||
Фаза 1 (PR#100) сделала embedder lazy-init (returns `None` без `OPENAI_BASE_URL`, не raises), index.py логирует reindex в `.rag/reindex.log`, search.py обрабатывает `None` от embedder → возвращает `[]`. Это разблокировало TS tools: Python backend безопасен при отсутствии env vars.
|
||||
|
||||
Требования к миграции (issue #96, #94):
|
||||
- 5 tools: `memory-save`, `memory-search`, `memory-list`, `memory-access`, `memory-doctor`
|
||||
- Keyword search (ripgrep) работает ВСЕГДА (zero env dependency)
|
||||
- Semantic search (Python + OpenRouter) как enhancement с fallback на keyword-only
|
||||
- Zero-config: первый `memory-save` авто-инициализирует память (mkdir + git init/clone + hook + categories)
|
||||
- Scoring портирован из плагина `scoreCandidate` (rgMatch + ragScore×1.4 + tags + importance + accessCount)
|
||||
- Pure-orchestrator model (ADR-010): TS tools вызываются из главного чата, делегируют в Python через subprocess
|
||||
|
||||
## Решение
|
||||
5 TS tools в `.opencode/tools/`, каждый следует паттерну `tool()` из `@opencode-ai/plugin` (description + args (zod) + async execute(args, context)):
|
||||
|
||||
1. **memory-save** — auto-setup + git commit + async reindex. `resolveMemoryDir()` (env или `~/.local/share/opencode/opencode-memory`). `autoSetup()`: mkdir, git init (или clone если `OPENCODE_MEMORY_REMOTE` set и dir пуста), post-commit hook (auto-push, только если remote), 7 категорий. `git add -A` + `git diff --cached --name-only` → commit с derived message `memory: sync <files>`. Async reindex: `spawn(python3, ["-m","src.memory","index",...], {detached:true, stdio:"ignore"}) + child.unref()` (fire-and-forget). Skip semantic если `OPENAI_BASE_URL` не set.
|
||||
|
||||
2. **memory-search** — ripgrep + Python semantic + scoring + fallback. `resolveRgBinary()`: `@vscode/ripgrep` (платформенный binary) → fallback system `rg`. Keyword: `rg -il --glob *.md --glob !.git --glob !.rag --glob !**/INDEX.md -e <term>` per term (OR). Semantic: `spawnSync("python3", ["-m","src.memory","search",...])` если `OPENAI_BASE_URL` set AND `.rag/index.json` существует. Fallback на keyword-only если Python упал/`[]`/env не set. Scoring — порт `scoreCandidate` из плагина (`dist/index.js:12822-12852`). Cross-category fallback если category filter дал 0 hits.
|
||||
|
||||
3. **memory-list** — pure TS, без subprocess. Список категорий (count .md) или файлов в категории (title/summary/importance/updated из frontmatter).
|
||||
|
||||
4. **memory-access** — pure TS, bump frontmatter `last_accessed`/`access_count` через regex replace (порт `bumpAccessFields` из плагина `dist/index.js:12677-12694`). Atomic write (tmp + rename).
|
||||
|
||||
5. **memory-doctor** — read-only диагностика (rg, Python importability, env vars, memory dir, RAG index).
|
||||
|
||||
Зависимости: `@vscode/ripgrep ^1.18.0` (keyword search, платформенный binary), devDeps `@types/node` + `typescript` (typecheck).
|
||||
|
||||
## Альтернативы
|
||||
- **Python MCP server** (Option A в `technical/cloud-memory-opencode-landscape.md`, `technical/python-mcp-server-replace-plugin-feasibility.md`) — отклонён: добавляет `mcp` dep (22 transitive), Dockerfile layer, embedder.py lazy-init (сделано в PR#100), naming verification. TS tools проще для opencode-only use case (reuse proven `spawnSync` pattern, no new Python deps, no Dockerfile changes).
|
||||
- **Оставить плагин + починить wrapper** — отклонено: wrapper-hop (`setup-memory.sh` → JS wrapper → Python CLI) добавляет fragility, `@mathew-cf/rag-cli` требует GLIBC 2.39 (несовместим с node:20-slim на Bookworm, см. ADR-030). TS tools вызывают Python напрямую через `python3 -m src.memory`.
|
||||
- **System `rg` без `@vscode/ripgrep`** — отклонён: системный `rg` может отсутствовать (Docker slim images). `@vscode/ripgrep` предоставляет платформенный binary (npm-managed, reproducible). memory-search имеет fallback на system `rg` через `spawnSync("rg", ["--version"])`.
|
||||
- **Полноценный YAML parser (js-yaml) для frontmatter** — отклонён: наш формат плоский (плоские ключи + `[a, b]` для tags/related). Regex-парсер (тот же что в плагине `parseFrontmatter`) покрывает формат и не добавляет dep.
|
||||
- **Conventional commit format для memory-save commits** — отклонён: memory repo не part основного репо, commit message derived из changed files (`memory: sync <files>`), не human-curated. Тот же паттерн что в плагине `runSave`. `commit` tool (для main repo) остаётся с conventional validation.
|
||||
37
docs/handoff/pr-101-ts-memory-tools.md
Normal file
37
docs/handoff/pr-101-ts-memory-tools.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
pr: 101
|
||||
title: feat(memory): 5 TS tools with auto-setup and fallback
|
||||
---
|
||||
|
||||
## Что сделано
|
||||
5 TS tools в `.opencode/tools/` заменяющие плагин `@mathew-cf/opencode-memory`. Прогрессивная модель: keyword (ripgrep) работает всегда, semantic (Python+OpenRouter) как enhancement с fallback. Zero-config: первый `memory-save` авто-инициализирует память.
|
||||
|
||||
- `memory-save.ts` (~120 LOC) — auto-setup + git commit + async reindex. Определяет `memoryDir` (env или `~/.local/share/opencode/opencode-memory`). При первом вызове: `mkdir -p`, `git init` (или `git clone` если `OPENCODE_MEMORY_REMOTE` set и dir пуста), установка post-commit hook (`git push origin master 2>/dev/null || true`) при наличии remote, создание 7 категорий. Затем `git add -A` + `git diff --cached --name-only` → если пусто, return "No changes to sync". `git commit -m "memory: sync <files>"`. Async reindex: `spawn("python3", ["-m","src.memory","index",memoryDir,"-o",.rag], {cwd: context.worktree, stdio:"ignore", detached:true}) + child.unref()` — fire-and-forget, НЕ блокирует ответ. Лог в `.rag/reindex.log` (Python из PR#100). Skip semantic если `OPENAI_BASE_URL` не set.
|
||||
- `memory-search.ts` (~270 LOC) — ripgrep + Python semantic + scoring + fallback. `resolveRgBinary()`: `require2("@vscode/ripgrep").rgPath` (если установлен и существует), fallback на системный `rg`. Keyword: `spawnSync(rgBin, [...buildRgArgs(terms), searchDir])` — `--glob *.md --glob !.git --glob !.rag --glob !**/INDEX.md -e <term>` per term (OR logic). Semantic: если `OPENAI_BASE_URL` set AND `.rag/index.json` существует → `spawnSync("python3", ["-m","src.memory","search",query,"-i",.rag,"-k","15","--json"], {cwd: context.worktree})`. Если Python упал (status≠0) / вернул `[]` / env не set → skip semantic (fallback keyword-only). Scoring — порт `scoreCandidate()` из плагина `dist/index.js:12822-12852`: rgMatch (+0.15+0.35*termCoverage), ragScore×1.4, rgMatch+ragScore bonus +0.10, tags overlap +0.20*(tagHits/terms), path hits +0.15, importance (high +0.15, low -0.10), accessCount (≥5 +0.10, ≥2 +0.05). Cross-category fallback если category filter дал 0 hits. Output: markdown с top-7 результатами + Related files.
|
||||
- `memory-list.ts` (~100 LOC) — pure TS, без subprocess. Без `category` → список 7 категорий с count .md файлов (`walkMd` рекурсивно, skip `.git`/`.rag`/`INDEX.md`). С `category` → список файлов (`{path, title, summary, importance, updated}` из frontmatter), отсортированы по `updated` descending.
|
||||
- `memory-access.ts` (~70 LOC) — pure TS, bump frontmatter. Читает файл, парсит frontmatter, обновляет `last_accessed: <today YYYY-MM-DD>` и `access_count: <current+1>` (regex replace, порт `bumpAccessFields` из плагина `dist/index.js:12677-12694`). Atomic write: `writeFileSync(tmp) + renameSync(tmp,filePath)`. НЕ коммитит — следующий `memory-save` синхронизирует.
|
||||
- `memory-doctor.ts` (~85 LOC) — read-only диагностика. Проверяет: `rg` (через `@vscode/ripgrep` + fallback system rg + `spawnSync --version`), Python `src.memory` importable (`python3 -c "import src.memory"`, cwd=worktree), env vars (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENCODE_MEMORY_REMOTE`), `OPENCODE_MEMORY_DIR` exists, memory dir is git repo, `.rag/index.json` exists (size+mtime), count .md files. Возвращает markdown с ✅/❌ по каждому пункту + summary (all-green / keyword-only / rg missing).
|
||||
- `.opencode/package.json` — добавлены deps: `@vscode/ripgrep ^1.18.0` (keyword search, платформенный binary `@vscode/ripgrep-linux-x64/bin/rg`), devDeps: `@types/node ^26.1.1` (typecheck) + `typescript ^7.0.2` (typecheck).
|
||||
- ADR-043 + этот handoff.
|
||||
|
||||
Scoring logic портирован 1-в-1 из плагина (rgMatch 0.15+0.35*termCoverage, ragScore×1.4, +0.10 за совпадение обоих, tags +0.20, path +0.15, importance, accessCount). Тестирование: typecheck (`tsc --noEmit --target es2022 --moduleResolution bundler --types node`) — clean. Smoke-tests всех 5 tools на реальной памяти (84 .md файла, 210 MB index.json): memory-search (6 keyword matches по "embedder"), memory-list (preferences 0 / repos 19 / technical 63 / workflows 2), memory-access (3→4 count, atomic), memory-doctor (all green), memory-save (auto-setup на пустой /tmp dir с git init + hook + remote + commit).
|
||||
|
||||
## Почему
|
||||
Плагин `@mathew-cf/opencode-memory` не вызывается из-за бага wrapper-пути MCP server (`MEMORY_WRAPPER_PATH` указывает на `@mathew-cf/rag-cli/bin/rag.js`, который не существует/несовместим — см. ADR-030, PR#57, PR#71). TS tools вызывают Python через `spawnSync` (паттерн `pipeline-status.ts`, `commit.ts`, `memory-setup.ts`) — нативный, без wrapper-hop. Это Фаза 2 миграции к единой облачной памяти (issue #94, ADR-030): Фаза 1 (PR#100) сделала embedder lazy-init (returns None без env), Фаза 2 (этот PR) — TS tools, Фаза 3 — удалить плагин + `memory-setup.ts` + `setup-memory.sh`.
|
||||
|
||||
Прогрессивная модель (progressive enhancement): keyword (ripgrep) работает ВСЕГДА, semantic (Python+OpenRouter) как enhancement с fallback на keyword-only. Zero-config: первый `memory-save` авто-инициализирует память (mkdir + git init/clone + hook + 7 категорий) — не требует отдельного `memory-setup` вызова. Это упрощает onboarding и убирает зависимость от `OPENCODE_MEMORY_REMOTE` (хотя hook auto-push ставится только если remote set).
|
||||
|
||||
## Pending
|
||||
—
|
||||
- Фаза 3 (отдельный PR): удалить `.opencode/tools/memory-setup.ts` + `.opencode/scripts/setup-memory.sh` + плагин `@mathew-cf/opencode-memory` из `opencode.json` `plugin` block + обновить AGENTS.md Tool Usage Policy таблицу (10 tools → 14 tools: добавить `memory-save`, `memory-search`, `memory-list`, `memory-access`, `memory-doctor`; убрать `memory-setup`).
|
||||
|
||||
## Watch out
|
||||
- **`@vscode/ripgrep` платформенный binary** — `require("@vscode/ripgrep").rgPath` возвращает путь к `@vscode/ripgrep-<platform>/bin/rg` (не к `@vscode/ripgrep/bin/rg`). На linux-x64: `@vscode/ripgrep-linux-x64/bin/rg`. Проверяется через `fs.existsSync(mod.rgPath)`. Если платформенный пакет не установлен (unsupported platform) → fallback на системный `rg` через `spawnSync("rg", ["--version"])`. Если оба недоступны → memory-search возвращает "No memories found" (keyword не работает, semantic может). memory-doctor показывает ❌.
|
||||
- **`OPENCODE_MEMORY_REMOTE` опционален для memory-save** (в отличие от `setup-memory.sh`, который exit 1 без remote). memory-save авто-инициализирует git repo (`git init`) даже без remote — hook auto-push не ставится. Это делает zero-config возможным (новый пользователь без remote может начать писать память, push настроит позже через `OPENCODE_MEMORY_REMOTE`). Hook ставится ТОЛЬКО если remote set AND hook отсутствует/несовпадает.
|
||||
- **`spawn(... detached: true) + child.unref()`** для async reindex — fire-and-forget, НЕ блокирует return memory-save. Если процесс opencode умрёт до завершения reindex, orphaned python процесс может остаться (но `.rag/.lock` через `fcntl.flock(LOCK_EX)` в `index.py` предотвращает concurrent reindex corruption). Reindex логирует прогресс в `.rag/reindex.log` (из PR#100).
|
||||
- **Scoring weights портированы 1-в-1** из плагина `scoreCandidate` (`dist/index.js:12822-12852`). НЕ изменяй веса без ADR — это нарушит совместимость результатов поиска между плагином (если останется) и TS tools. Path hits +0.15 — это было в плагине (`input.path.toLowerCase().replace(/[-_/.]/g, " ")`), сохранено.
|
||||
- **Typecheck требует `@types/node` + `typescript`** — добавлены как devDeps в `.opencode/package.json`. CI runner должен делать `npm install` в `.opencode/` перед typecheck. Runtime (opencode plugin loader) использует Bun/Node native TS, эти devDeps не нужны в production.
|
||||
- **Frontmatter parsing упрощён** (regex `^(\w+):\s*(.+)$` per line, не полноценный YAML parser) — покрывает наш формат (плоские ключи + `[a, b]` для tags/related). Многострочные значения НЕ поддерживаются. Это тот же парсер что в плагине (`parseFrontmatter` `dist/index.js:12639-12676`).
|
||||
- **memory-search cross-category fallback** — если `args.category` указан и дал 0 hits, search повторяет по всему `memoryDir` (без category filter). Это поведение плагина (`crossCategoryFallback`, `dist/index.js:12999-13022`). Сохранено для UX.
|
||||
- **`spawnSync("python3", ["-c", "import src.memory"], {cwd: context.worktree})`** в memory-doctor — требует что `context.worktree` имеет `src/memory/` пакет importable (`.venv/bin/python` или system python с `src.memory` установлен). Если worktree не настроен (нет `.venv`, нет `pip install -e .`) → ❌, но это диагностический сигнал, не блокер.
|
||||
- **`git -C memoryDir commit`** через `spawnSync` — НЕ через `commit` tool (тот для main repo, с conventional format validation). Memory repo commit message: `memory: sync <files>` (derived, не conventional format). Это тот же паттерн что в плагине `runSave` (`dist/index.js:13259-13260`).
|
||||
|
|
@ -44,6 +44,11 @@ opencode-config/
|
|||
│ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65
|
||||
│ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65
|
||||
│ │ ├── merge-pr.ts # merge-pr tool wrapper (orchestrator-safe gh pr merge; optional repo?: string) — PR#30, PR#65
|
||||
│ │ ├── memory-access.ts # memory-access tool (bump frontmatter last_accessed/access_count, regex replace, atomic write tmp+rename) — PR#101
|
||||
│ │ ├── memory-doctor.ts # memory-doctor tool (read-only diagnostics: rg, Python src.memory importability, env vars, memory dir, RAG index; markdown ✅/❌ report) — PR#101
|
||||
│ │ ├── memory-list.ts # memory-list tool (pure TS, categories count .md or files in category with frontmatter) — PR#101
|
||||
│ │ ├── memory-save.ts # memory-save tool (auto-setup mkdir+git init/clone+hook+7 categories, git commit, async reindex via spawn detached+unref) — PR#101
|
||||
│ │ ├── memory-search.ts # memory-search tool (ripgrep keyword + Python semantic via spawnSync + scoring port + cross-category fallback) — PR#101
|
||||
│ │ ├── memory-setup.ts # memory-setup tool wrapper (0 args, calls setup-memory.sh) — PR#36
|
||||
│ │ ├── pipeline-status.ts # pipeline-status tool wrapper
|
||||
│ │ ├── post-docs-review.ts # post-docs-review tool wrapper (3 args: pr_number, verdict enum, body; deterministic ## Docs Review Summary heading; optional repo?: string) — PR#46, PR#65
|
||||
|
|
@ -60,7 +65,7 @@ opencode-config/
|
|||
│ │ ├── spec-status.py # 9-phase spec oracle
|
||||
│ │ └── tunnel.sh # Cloudflare tunnel toggle bash (named mode via CLOUDFLARE_TUNNEL_TOKEN) — PR#34
|
||||
│ ├── opencode.json # MCP servers, providers, permissions, agents (role-based tools), plugins — PR#40, PR#69
|
||||
│ ├── package.json # npm deps for tools/*.ts
|
||||
│ ├── package.json # npm deps for tools/*.ts (deps: @vscode/ripgrep; devDeps: @types/node, typescript) — PR#101
|
||||
│ └── .gitignore # Ignores node_modules, etc.
|
||||
├── docs/
|
||||
│ ├── handoff/ # PR handoffs (pr-<N>-<slug>.md)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue