mirror of
https://github.com/hanzoai/cc.git
synced 2026-09-24 16:57:40 +00:00
System prompts (#115)
This commit is contained in:
@@ -56,13 +56,17 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"chalk": "^5.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"figlet": "^1.8.2",
|
||||
"globby": "^14.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"ink": "^6.1.0",
|
||||
"ink-link": "^4.1.0",
|
||||
"ollama": "^0.6.0",
|
||||
"openai": "^6.3.0",
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+144
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@babel/parser':
|
||||
specifier: ^7.28.4
|
||||
version: 7.28.4
|
||||
'@types/figlet':
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0
|
||||
@@ -23,12 +26,21 @@ importers:
|
||||
globby:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
ink:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@types/[email protected])([email protected])([email protected])
|
||||
ink-link:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
ollama:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
openai:
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0([email protected])
|
||||
react:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1
|
||||
@@ -82,6 +94,23 @@ packages:
|
||||
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
|
||||
engines: {node: '>=14.13.1'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
|
||||
|
||||
@@ -668,6 +697,9 @@ packages:
|
||||
resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
@@ -943,6 +975,11 @@ packages:
|
||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -966,6 +1003,10 @@ packages:
|
||||
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -1083,6 +1124,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1185,6 +1230,10 @@ packages:
|
||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1282,6 +1331,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
@@ -1302,6 +1355,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1449,10 +1506,25 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FHjdU2Ok5x2HZsxPui/MBJZ5J+HzmxoWYa/p9wk736eT+uAhS8nvIICar5YgwlG5MFNjDR6UA5F3RSKq+JseOA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1656,6 +1728,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -1727,6 +1803,9 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1768,6 +1847,10 @@ packages:
|
||||
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1958,6 +2041,9 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2034,6 +2120,19 @@ snapshots:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 4.0.0
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.4
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.4
|
||||
@@ -2519,6 +2618,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -2945,6 +3048,8 @@ snapshots:
|
||||
acorn-jsx: 5.3.2([email protected])
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
@@ -2963,6 +3068,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -3087,6 +3196,13 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
js-yaml: 3.14.1
|
||||
kind-of: 6.0.3
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -3208,6 +3324,8 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -3300,6 +3418,11 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -3321,6 +3444,8 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -3448,10 +3573,18 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
whatwg-fetch: 3.6.20
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
[email protected]([email protected]):
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -3671,6 +3804,11 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -3752,6 +3890,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
escape-string-regexp: 2.0.0
|
||||
@@ -3819,6 +3959,8 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 6.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4019,6 +4161,8 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
|
||||
+13
-9
@@ -14,13 +14,14 @@ import {
|
||||
Settings,
|
||||
StartupCheckInfo,
|
||||
TweakccConfig,
|
||||
SYSTEM_PROMPTS_DIR,
|
||||
} from './utils/types.js';
|
||||
import {
|
||||
readConfigFile,
|
||||
restoreClijsFromBackup,
|
||||
updateConfigFile,
|
||||
} from './utils/config.js';
|
||||
import { revealFileInExplorer } from './utils/misc.js';
|
||||
import { openInExplorer, revealFileInExplorer } from './utils/misc.js';
|
||||
import { applyCustomization } from './utils/patches/index.js';
|
||||
|
||||
export const SettingsContext = createContext({
|
||||
@@ -113,14 +114,6 @@ Please reapply your changes below.`,
|
||||
const handleMainSubmit = (item: MainMenuItem) => {
|
||||
setNotification(null);
|
||||
switch (item) {
|
||||
case MainMenuItem.THEMES:
|
||||
case MainMenuItem.LAUNCH_TEXT:
|
||||
case MainMenuItem.THINKING_VERBS:
|
||||
case MainMenuItem.THINKING_STYLE:
|
||||
case MainMenuItem.USER_MESSAGE_DISPLAY:
|
||||
case MainMenuItem.INPUT_BOX:
|
||||
setCurrentView(item);
|
||||
break;
|
||||
case MainMenuItem.APPLY_CHANGES:
|
||||
if (startupCheckInfo.ccInstInfo) {
|
||||
setNotification({
|
||||
@@ -138,6 +131,17 @@ Please reapply your changes below.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case MainMenuItem.THEMES:
|
||||
case MainMenuItem.LAUNCH_TEXT:
|
||||
case MainMenuItem.THINKING_VERBS:
|
||||
case MainMenuItem.THINKING_STYLE:
|
||||
case MainMenuItem.USER_MESSAGE_DISPLAY:
|
||||
case MainMenuItem.INPUT_BOX:
|
||||
setCurrentView(item);
|
||||
break;
|
||||
case MainMenuItem.VIEW_SYSTEM_PROMPTS:
|
||||
openInExplorer(SYSTEM_PROMPTS_DIR);
|
||||
break;
|
||||
case MainMenuItem.RESTORE_ORIGINAL:
|
||||
if (startupCheckInfo.ccInstInfo) {
|
||||
restoreClijsFromBackup(startupCheckInfo.ccInstInfo).then(() => {
|
||||
|
||||
@@ -40,6 +40,10 @@ const baseMenuItems: SelectItem[] = [
|
||||
name: MainMenuItem.INPUT_BOX,
|
||||
desc: 'Customize the input box appearance (e.g., remove border)',
|
||||
},
|
||||
{
|
||||
name: MainMenuItem.VIEW_SYSTEM_PROMPTS,
|
||||
desc: 'Opens the system prompts directory where you can customize Claude Code\'s system prompts',
|
||||
},
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
|
||||
+36
-44
@@ -29,53 +29,45 @@ const main = async () => {
|
||||
console.log('Applying saved customizations to Claude Code...');
|
||||
console.log(`Configuration saved at: ${CONFIG_FILE}`);
|
||||
|
||||
try {
|
||||
// Read the saved configuration
|
||||
const config = await readConfigFile();
|
||||
// Read the saved configuration
|
||||
const config = await readConfigFile();
|
||||
|
||||
if (!config.settings || Object.keys(config.settings).length === 0) {
|
||||
console.error('No saved customizations found in ' + CONFIG_FILE);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find Claude Code installation
|
||||
const startupCheckInfo = await startupCheck();
|
||||
|
||||
if (!startupCheckInfo || !startupCheckInfo.ccInstInfo) {
|
||||
console.error(`Cannot find Claude Code's cli.js`);
|
||||
console.error('Searched at the following locations:');
|
||||
CLIJS_SEARCH_PATH_INFO.forEach(info => {
|
||||
if (info.isGlob) {
|
||||
if (info.expandedPaths.length === 0) {
|
||||
console.error(` - ${info.pattern} (no matches)`);
|
||||
} else {
|
||||
console.error(` - ${info.pattern}`);
|
||||
info.expandedPaths.forEach(path => {
|
||||
console.error(` - ${path}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(` - ${info.pattern}`);
|
||||
}
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Found Claude Code at: ${startupCheckInfo.ccInstInfo.cliPath}`
|
||||
);
|
||||
console.log(`Version: ${startupCheckInfo.ccInstInfo.version}`);
|
||||
|
||||
// Apply the customizations
|
||||
console.log('Applying customizations...');
|
||||
await applyCustomization(config, startupCheckInfo.ccInstInfo);
|
||||
console.log('Customizations applied successfully!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error:');
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
if (!config.settings || Object.keys(config.settings).length === 0) {
|
||||
console.error('No saved customizations found in ' + CONFIG_FILE);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find Claude Code installation
|
||||
const startupCheckInfo = await startupCheck();
|
||||
|
||||
if (!startupCheckInfo || !startupCheckInfo.ccInstInfo) {
|
||||
console.error(`Cannot find Claude Code's cli.js`);
|
||||
console.error('Searched at the following locations:');
|
||||
CLIJS_SEARCH_PATH_INFO.forEach(info => {
|
||||
if (info.isGlob) {
|
||||
if (info.expandedPaths.length === 0) {
|
||||
console.error(` - ${info.pattern} (no matches)`);
|
||||
} else {
|
||||
console.error(` - ${info.pattern}`);
|
||||
info.expandedPaths.forEach(path => {
|
||||
console.error(` - ${path}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(` - ${info.pattern}`);
|
||||
}
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found Claude Code at: ${startupCheckInfo.ccInstInfo.cliPath}`);
|
||||
console.log(`Version: ${startupCheckInfo.ccInstInfo.version}`);
|
||||
|
||||
// Apply the customizations
|
||||
console.log('Applying customizations...');
|
||||
await applyCustomization(config, startupCheckInfo.ccInstInfo);
|
||||
console.log('Customizations applied successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const startupCheckInfo = await startupCheck();
|
||||
|
||||
@@ -11,6 +11,7 @@ import fs from 'node:fs/promises';
|
||||
import type { Stats } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as misc from './misc.js';
|
||||
import * as systemPromptHashIndex from './systemPromptHashIndex.js';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
|
||||
@@ -35,6 +36,11 @@ describe('config.ts', () => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'clear').mockImplementation(() => {});
|
||||
// Mock hasUnappliedSystemPromptChanges to always return false by default
|
||||
vi.spyOn(
|
||||
systemPromptHashIndex,
|
||||
'hasUnappliedSystemPromptChanges'
|
||||
).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe('ensureConfigDir', () => {
|
||||
@@ -87,11 +93,17 @@ describe('config.ts', () => {
|
||||
|
||||
describe('restoreClijsFromBackup', () => {
|
||||
it('should copy the backup file and update the config', async () => {
|
||||
// Mock the clearAllAppliedHashes function to avoid file system operations
|
||||
vi.spyOn(
|
||||
systemPromptHashIndex,
|
||||
'clearAllAppliedHashes'
|
||||
).mockResolvedValue(undefined);
|
||||
|
||||
// Mock reading the backup file
|
||||
const readFileSpy = vi
|
||||
.spyOn(fs, 'readFile')
|
||||
.mockResolvedValueOnce(Buffer.from('backup content')) // Reading backup file
|
||||
.mockRejectedValue(createEnoent()); // Reading config file
|
||||
.mockRejectedValue(createEnoent()); // Reading config file and others
|
||||
|
||||
// Mock file operations for the helper function
|
||||
vi.spyOn(fs, 'stat').mockRejectedValue(createEnoent()); // File doesn't exist
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CONFIG_FILE,
|
||||
DEFAULT_SETTINGS,
|
||||
StartupCheckInfo,
|
||||
SYSTEM_PROMPTS_DIR,
|
||||
Theme,
|
||||
ThinkingVerbsConfig,
|
||||
TweakccConfig,
|
||||
@@ -17,9 +18,15 @@ import {
|
||||
isDebug,
|
||||
replaceFileBreakingHardLinks,
|
||||
} from './misc.js';
|
||||
import { syncSystemPrompts, displaySyncResults } from './promptSync.js';
|
||||
import {
|
||||
hasUnappliedSystemPromptChanges,
|
||||
clearAllAppliedHashes,
|
||||
} from './systemPromptHashIndex.js';
|
||||
|
||||
export const ensureConfigDir = async (): Promise<void> => {
|
||||
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
||||
await fs.mkdir(SYSTEM_PROMPTS_DIR, { recursive: true });
|
||||
};
|
||||
|
||||
let lastConfig: TweakccConfig = {
|
||||
@@ -121,6 +128,14 @@ export const readConfigFile = async (): Promise<TweakccConfig> => {
|
||||
DEFAULT_SETTINGS.userMessageDisplay;
|
||||
}
|
||||
|
||||
// Check if system prompts have been modified since they were last applied
|
||||
// If so, mark changesApplied as false to show the "*Apply customizations" indicator
|
||||
const hasSystemPromptChanges =
|
||||
await hasUnappliedSystemPromptChanges(SYSTEM_PROMPTS_DIR);
|
||||
if (hasSystemPromptChanges) {
|
||||
readConfig.changesApplied = false;
|
||||
}
|
||||
|
||||
lastConfig = readConfig;
|
||||
return readConfig;
|
||||
} catch (error) {
|
||||
@@ -181,6 +196,9 @@ export const restoreClijsFromBackup = async (
|
||||
'restore'
|
||||
);
|
||||
|
||||
// Clear all applied hashes since we're restoring to defaults
|
||||
await clearAllAppliedHashes();
|
||||
|
||||
await updateConfigFile(config => {
|
||||
config.changesApplied = false;
|
||||
});
|
||||
@@ -291,6 +309,12 @@ export async function startupCheck(): Promise<StartupCheckInfo | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sync system prompts with the current CC version
|
||||
if (ccInstInfo.version) {
|
||||
const syncSummary = await syncSystemPrompts(ccInstInfo.version);
|
||||
displaySyncResults(syncSummary);
|
||||
}
|
||||
|
||||
const realVersion = ccInstInfo.version;
|
||||
const backedUpVersion = config.ccVersion;
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { StringsFile } from './promptSync.js';
|
||||
|
||||
/**
|
||||
* Downloads the strings file for a given CC version from GitHub
|
||||
* @param version - Version string in format "X.Y.Z" (e.g., "0.17.0")
|
||||
* @returns Promise that resolves to the parsed JSON content
|
||||
*/
|
||||
export async function downloadStringsFile(
|
||||
version: string
|
||||
): Promise<StringsFile> {
|
||||
// Construct the GitHub raw URL
|
||||
//const url = `https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-${version}.json`;
|
||||
const url = `https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/strings-tmp/strings-${version}.json`;
|
||||
|
||||
try {
|
||||
// Fetch the file from GitHub
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to download strings file: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
// Parse JSON directly
|
||||
const jsonData = (await response.json()) as StringsFile;
|
||||
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error downloading strings file for version ${version}: ${error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads strings files for multiple versions
|
||||
* @param versions - Array of version strings
|
||||
* @returns Promise that resolves to a map of version to parsed JSON content
|
||||
*/
|
||||
export async function downloadMultipleStringsFiles(
|
||||
versions: string[]
|
||||
): Promise<Map<string, StringsFile>> {
|
||||
const results = new Map<string, StringsFile>();
|
||||
|
||||
for (const version of versions) {
|
||||
try {
|
||||
const data = await downloadStringsFile(version);
|
||||
results.set(version, data);
|
||||
} catch (error) {
|
||||
console.error(`Failed to download version ${version}:`, error);
|
||||
// Continue with other versions
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -80,6 +80,31 @@ export function getColorKeys(theme: Theme): string[] {
|
||||
return Object.keys(theme.colors);
|
||||
}
|
||||
|
||||
export function openInExplorer(filePath: string) {
|
||||
if (process.platform === 'win32') {
|
||||
child_process
|
||||
.spawn('explorer', [filePath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
})
|
||||
.unref();
|
||||
} else if (process.platform === 'darwin') {
|
||||
child_process
|
||||
.spawn('open', [filePath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
})
|
||||
.unref();
|
||||
} else {
|
||||
child_process
|
||||
.spawn('xdg-open', [filePath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
})
|
||||
.unref();
|
||||
}
|
||||
}
|
||||
|
||||
export function revealFileInExplorer(filePath: string) {
|
||||
if (process.platform === 'win32') {
|
||||
child_process
|
||||
|
||||
@@ -36,6 +36,7 @@ import { writeWelcomeMessage } from './welcomeMessage.js';
|
||||
import { writeModelCustomizations } from './modelSelector.js';
|
||||
import { writeIgnoreMaxSubscription } from './ignoreMaxSubscription.js';
|
||||
import { writeVersionOutput } from './versionOutput.js';
|
||||
import { applySystemPrompts } from './systemPrompts.js';
|
||||
|
||||
export interface LocationResult {
|
||||
startIndex: number;
|
||||
@@ -121,6 +122,9 @@ export const applyCustomization = async (
|
||||
|
||||
let content = await fs.readFile(ccInstInfo.cliPath, { encoding: 'utf8' });
|
||||
|
||||
// Apply system prompt customizations
|
||||
content = await applySystemPrompts(content, ccInstInfo.version);
|
||||
|
||||
// Apply themes
|
||||
let result: string | null = null;
|
||||
if (config.settings.themes && config.settings.themes.length > 0) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import chalk from 'chalk';
|
||||
import { isDebug } from '../misc.js';
|
||||
import { showDiff } from './index.js';
|
||||
import { loadSystemPromptsWithRegex } from '../promptSync.js';
|
||||
import { setAppliedHash, computeMD5Hash } from '../systemPromptHashIndex.js';
|
||||
|
||||
/**
|
||||
* Apply system prompt customizations to cli.js content
|
||||
* @param content - The current content of cli.js
|
||||
* @param version - The Claude Code version
|
||||
* @returns The modified content with system prompts applied
|
||||
*/
|
||||
export const applySystemPrompts = async (
|
||||
content: string,
|
||||
version: string
|
||||
): Promise<string> => {
|
||||
// Load system prompts and generate regexes
|
||||
const systemPrompts = await loadSystemPromptsWithRegex(version);
|
||||
if (isDebug()) {
|
||||
console.log(`Loaded ${systemPrompts.length} system prompts with regexes`);
|
||||
}
|
||||
|
||||
// Search for and replace each prompt in cli.js
|
||||
for (const {
|
||||
promptId,
|
||||
prompt,
|
||||
regex,
|
||||
getInterpolatedContent,
|
||||
} of systemPrompts) {
|
||||
const pattern = new RegExp(regex, 's'); // 's' flag for dotAll mode
|
||||
const match = content.match(pattern);
|
||||
|
||||
if (match && match.index !== undefined) {
|
||||
// Generate the interpolated content using the actual variables from the match
|
||||
const interpolatedContent = getInterpolatedContent(match);
|
||||
|
||||
if (isDebug()) {
|
||||
console.log(`\nFound match for prompt: ${prompt.name}`);
|
||||
console.log(
|
||||
` Match location: index ${match.index}, length ${match[0].length}`
|
||||
);
|
||||
console.log(
|
||||
` Original content (first 100 chars): ${match[0].substring(0, 100)}...`
|
||||
);
|
||||
console.log(
|
||||
` Replacement content (first 100 chars): ${interpolatedContent.substring(0, 100)}...`
|
||||
);
|
||||
console.log(` Captured variables: ${match.slice(1).join(', ')}`);
|
||||
console.log(` Content identical: ${match[0] === interpolatedContent}`);
|
||||
}
|
||||
|
||||
const oldContent = content;
|
||||
const matchIndex = match.index;
|
||||
const matchLength = match[0].length;
|
||||
|
||||
// Replace the matched content with the interpolated content from the markdown file
|
||||
content = content.replace(pattern, interpolatedContent);
|
||||
|
||||
// Store the hash of the applied prompt content
|
||||
const appliedHash = computeMD5Hash(prompt.content);
|
||||
await setAppliedHash(promptId, appliedHash);
|
||||
|
||||
// Show diff in debug mode
|
||||
if (isDebug()) {
|
||||
showDiff(
|
||||
oldContent,
|
||||
content,
|
||||
interpolatedContent,
|
||||
matchIndex,
|
||||
matchIndex + matchLength
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Could not find system prompt "${prompt.name}" in cli.js (using regex /${regex}/)`
|
||||
)
|
||||
);
|
||||
|
||||
if (isDebug()) {
|
||||
console.log(`\n Debug info for ${prompt.name}:`);
|
||||
console.log(
|
||||
` Regex pattern (first 200 chars): ${regex.substring(0, 200)}...`
|
||||
);
|
||||
console.log(` Trying to match pattern in cli.js...`);
|
||||
const testMatch = content.match(new RegExp(regex.substring(0, 100)));
|
||||
console.log(
|
||||
` Partial match result: ${testMatch ? 'found partial' : 'no match'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -0,0 +1,889 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as promptSync from './promptSync.js';
|
||||
import type { StringsPrompt, StringsFile } from './promptSync.js';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
vi.mock('./download.js');
|
||||
|
||||
const createEnoent = () => {
|
||||
const error: NodeJS.ErrnoException = new Error(
|
||||
'ENOENT: no such file or directory'
|
||||
);
|
||||
error.code = 'ENOENT';
|
||||
return error;
|
||||
};
|
||||
|
||||
describe('promptSync.ts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('parseMarkdownPrompt', () => {
|
||||
it('should parse markdown with frontmatter', () => {
|
||||
const markdown = `<!--
|
||||
name: Test Prompt
|
||||
description: A test prompt
|
||||
ccVersion: 1.0.0
|
||||
variables:
|
||||
- SETTINGS
|
||||
- CONFIG
|
||||
-->
|
||||
|
||||
This is the content with \${SETTINGS.preferredName} and \${CONFIG.taskType}.`;
|
||||
|
||||
const result = promptSync.parseMarkdownPrompt(markdown);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'Test Prompt',
|
||||
description: 'A test prompt',
|
||||
ccVersion: '1.0.0',
|
||||
variables: ['SETTINGS', 'CONFIG'],
|
||||
content:
|
||||
'This is the content with ${SETTINGS.preferredName} and ${CONFIG.taskType}.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle markdown without variables', () => {
|
||||
const markdown = `<!--
|
||||
name: Simple Prompt
|
||||
description: No variables
|
||||
ccVersion: 1.0.0
|
||||
-->
|
||||
|
||||
Simple content.`;
|
||||
|
||||
const result = promptSync.parseMarkdownPrompt(markdown);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'Simple Prompt',
|
||||
description: 'No variables',
|
||||
ccVersion: '1.0.0',
|
||||
variables: [],
|
||||
content: 'Simple content.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing frontmatter fields', () => {
|
||||
const markdown = `<!--
|
||||
-->
|
||||
|
||||
Content only.`;
|
||||
|
||||
const result = promptSync.parseMarkdownPrompt(markdown);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: '',
|
||||
description: '',
|
||||
ccVersion: '',
|
||||
variables: [],
|
||||
content: 'Content only.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconstructContentFromPieces', () => {
|
||||
it('should reconstruct content with placeholders for minified variables', () => {
|
||||
const pieces = [
|
||||
'You will greet the user as ${',
|
||||
'.preferredName}, running on ${',
|
||||
'.platform}!',
|
||||
];
|
||||
const identifiers = [1, 2];
|
||||
const identifierMap = { '1': 'SETTINGS', '2': 'CONFIG' };
|
||||
|
||||
const result = promptSync.reconstructContentFromPieces(
|
||||
pieces,
|
||||
identifiers,
|
||||
identifierMap
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'You will greet the user as ${SETTINGS.preferredName}, running on ${CONFIG.platform}!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty identifier map', () => {
|
||||
const pieces = ['Just text'];
|
||||
const identifiers: number[] = [];
|
||||
const identifierMap = {};
|
||||
|
||||
const result = promptSync.reconstructContentFromPieces(
|
||||
pieces,
|
||||
identifiers,
|
||||
identifierMap
|
||||
);
|
||||
|
||||
expect(result).toBe('Just text');
|
||||
});
|
||||
|
||||
it('should handle missing identifier mappings', () => {
|
||||
const pieces = ['Start ', ' end'];
|
||||
const identifiers = [999];
|
||||
const identifierMap = {};
|
||||
|
||||
const result = promptSync.reconstructContentFromPieces(
|
||||
pieces,
|
||||
identifiers,
|
||||
identifierMap
|
||||
);
|
||||
|
||||
expect(result).toBe('Start UNKNOWN_999 end');
|
||||
});
|
||||
|
||||
it('should handle numeric and string identifiers', () => {
|
||||
const pieces = ['Use ${', '.currentMode} and ${', '.theme} together'];
|
||||
const identifiers = [1, '2'];
|
||||
const identifierMap = { '1': 'STATE', '2': 'CONFIG' };
|
||||
|
||||
const result = promptSync.reconstructContentFromPieces(
|
||||
pieces,
|
||||
identifiers,
|
||||
identifierMap
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'Use ${STATE.currentMode} and ${CONFIG.theme} together'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateMarkdownFromPrompt', () => {
|
||||
it('should generate markdown with variables', () => {
|
||||
const prompt: StringsPrompt = {
|
||||
id: 'test-id',
|
||||
name: 'Test Prompt',
|
||||
description: 'Test description',
|
||||
version: '1.0.0',
|
||||
pieces: ['You will greet the user as ${', '.preferredName}!'],
|
||||
identifiers: [1],
|
||||
identifierMap: { '1': 'SETTINGS' },
|
||||
};
|
||||
|
||||
const result = promptSync.generateMarkdownFromPrompt(prompt);
|
||||
|
||||
expect(result).toContain('name: Test Prompt');
|
||||
expect(result).toContain('description: Test description');
|
||||
expect(result).toContain('ccVersion: 1.0.0');
|
||||
expect(result).toContain('variables:');
|
||||
expect(result).toContain('- SETTINGS');
|
||||
expect(result).toContain(
|
||||
'You will greet the user as ${SETTINGS.preferredName}!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate markdown without variables when identifierMap is empty', () => {
|
||||
const prompt: StringsPrompt = {
|
||||
id: 'test-id',
|
||||
name: 'Simple Prompt',
|
||||
description: 'No vars',
|
||||
version: '1.0.0',
|
||||
pieces: ['Plain text'],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
};
|
||||
|
||||
const result = promptSync.generateMarkdownFromPrompt(prompt);
|
||||
|
||||
expect(result).toContain('name: Simple Prompt');
|
||||
expect(result).not.toContain('variables:');
|
||||
expect(result).toContain('Plain text');
|
||||
});
|
||||
|
||||
it('should use custom content when provided', () => {
|
||||
const prompt: StringsPrompt = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
version: '1.0.0',
|
||||
pieces: ['Original'],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
};
|
||||
|
||||
const result = promptSync.generateMarkdownFromPrompt(
|
||||
prompt,
|
||||
'Custom content here'
|
||||
);
|
||||
|
||||
expect(result).toContain('Custom content here');
|
||||
expect(result).not.toContain('Original');
|
||||
});
|
||||
|
||||
it('should deduplicate variables in frontmatter', () => {
|
||||
const prompt: StringsPrompt = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
version: '1.0.0',
|
||||
pieces: [
|
||||
'The user name is ${',
|
||||
'.userName} and again ${',
|
||||
'.userName}.',
|
||||
],
|
||||
identifiers: [1, 1], // Same identifier used twice
|
||||
identifierMap: { '1': 'SETTINGS' },
|
||||
};
|
||||
|
||||
const result = promptSync.generateMarkdownFromPrompt(prompt);
|
||||
|
||||
// Count occurrences of "- SETTINGS"
|
||||
const matches = result.match(/^ {2}- SETTINGS$/gm);
|
||||
expect(matches?.length).toBe(1); // Should only appear once in variables list
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRegexFromPieces', () => {
|
||||
it('should build regex that captures content between pieces', () => {
|
||||
const pieces = ['Hello ', ', welcome to ', '!'];
|
||||
const regex = promptSync.buildRegexFromPieces(pieces);
|
||||
|
||||
const testString = 'Hello John, welcome to Earth!';
|
||||
const match = testString.match(regex);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![1]).toBe('John');
|
||||
expect(match![2]).toBe('Earth');
|
||||
});
|
||||
|
||||
it('should escape special regex characters in pieces', () => {
|
||||
const pieces = ['(Start) ', ' [End]'];
|
||||
const regex = promptSync.buildRegexFromPieces(pieces);
|
||||
|
||||
const testString = '(Start) content [End]';
|
||||
const match = testString.match(regex);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![1]).toBe('content');
|
||||
});
|
||||
|
||||
it('should handle single piece (no placeholders)', () => {
|
||||
const pieces = ['Just text'];
|
||||
const regex = promptSync.buildRegexFromPieces(pieces);
|
||||
|
||||
const testString = 'Just text';
|
||||
const match = testString.match(regex);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
expect(match!.length).toBe(1); // Only full match, no captures
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractUserCustomizations', () => {
|
||||
it('should extract what user put in place of minified identifiers', () => {
|
||||
const pieces = [
|
||||
'You will greet as ${',
|
||||
'.preferredName}, on platform ${',
|
||||
'.platform}!',
|
||||
];
|
||||
const userContent =
|
||||
'You will greet as ${SETTINGS.preferredName}, on platform ${CONFIG.platform}!';
|
||||
|
||||
const result = promptSync.extractUserCustomizations(userContent, pieces);
|
||||
|
||||
expect(result).toEqual(['SETTINGS', 'CONFIG']);
|
||||
});
|
||||
|
||||
it('should handle multiline content with identifiers', () => {
|
||||
const pieces = ['Start\n${', '.value1}\nMiddle\n${', '.value2}\nEnd'];
|
||||
const userContent =
|
||||
'Start\n${CONFIG.value1}\nMiddle\n${STATE.value2}\nEnd';
|
||||
|
||||
const result = promptSync.extractUserCustomizations(userContent, pieces);
|
||||
|
||||
expect(result).toEqual(['CONFIG', 'STATE']);
|
||||
});
|
||||
|
||||
it('should throw error if content does not match structure', () => {
|
||||
const pieces = ['Hello ', '!'];
|
||||
const userContent = 'Goodbye world!';
|
||||
|
||||
expect(() => {
|
||||
promptSync.extractUserCustomizations(userContent, pieces);
|
||||
}).toThrow('User content does not match expected structure from pieces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildHumanToRealMapping', () => {
|
||||
it('should build mapping from human-readable identifiers to what user wrote', () => {
|
||||
const identifiers = [1, 2];
|
||||
const identifierMap = { '1': 'SETTINGS', '2': 'CONFIG' };
|
||||
const extractedCustomizations = ['SETTINGS', 'CONFIG'];
|
||||
|
||||
const result = promptSync.buildHumanToRealMapping(
|
||||
identifiers,
|
||||
identifierMap,
|
||||
extractedCustomizations
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
SETTINGS: 'SETTINGS',
|
||||
CONFIG: 'CONFIG',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate identifiers with same values', () => {
|
||||
const identifiers = [1, 2, 1];
|
||||
const identifierMap = { '1': 'SETTINGS', '2': 'CONFIG' };
|
||||
const extractedCustomizations = ['SETTINGS', 'CONFIG', 'SETTINGS'];
|
||||
|
||||
const result = promptSync.buildHumanToRealMapping(
|
||||
identifiers,
|
||||
identifierMap,
|
||||
extractedCustomizations
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
SETTINGS: 'SETTINGS',
|
||||
CONFIG: 'CONFIG',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error for duplicate identifiers with different values', () => {
|
||||
const identifiers = [1, 1];
|
||||
const identifierMap = { '1': 'SETTINGS' };
|
||||
const extractedCustomizations = ['SETTINGS', 'CUSTOM'];
|
||||
|
||||
expect(() => {
|
||||
promptSync.buildHumanToRealMapping(
|
||||
identifiers,
|
||||
identifierMap,
|
||||
extractedCustomizations
|
||||
);
|
||||
}).toThrow('Conflicting mappings for "SETTINGS": "SETTINGS" vs "CUSTOM"');
|
||||
});
|
||||
|
||||
it('should skip identifiers without mappings', () => {
|
||||
const identifiers = [1, 999, 2];
|
||||
const identifierMap = { '1': 'SETTINGS', '2': 'CONFIG' };
|
||||
const extractedCustomizations = ['SETTINGS', 'UNKNOWN', 'CONFIG'];
|
||||
|
||||
const result = promptSync.buildHumanToRealMapping(
|
||||
identifiers,
|
||||
identifierMap,
|
||||
extractedCustomizations
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
SETTINGS: 'SETTINGS',
|
||||
CONFIG: 'CONFIG',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyCustomizationsToPrompt', () => {
|
||||
it('should apply user customizations to new prompt version', () => {
|
||||
const newPrompt: StringsPrompt = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
version: '2.0.0',
|
||||
pieces: ['Greet user as ${', '.userName}, running on ${', '.osType}!'],
|
||||
identifiers: [1, 2],
|
||||
identifierMap: { '1': 'SETTINGS', '2': 'CONFIG' },
|
||||
};
|
||||
const humanToRealMapping = {
|
||||
SETTINGS: 'SETTINGS',
|
||||
CONFIG: 'CONFIG',
|
||||
};
|
||||
|
||||
const result = promptSync.applyCustomizationsToPrompt(
|
||||
newPrompt,
|
||||
humanToRealMapping
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'Greet user as ${SETTINGS.userName}, running on ${CONFIG.osType}!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use placeholder for new variables not in old version', () => {
|
||||
const newPrompt: StringsPrompt = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
version: '2.0.0',
|
||||
pieces: ['User ${', '.userName}, new field: ${', '.newFeature}!'],
|
||||
identifiers: [1, 3],
|
||||
identifierMap: { '1': 'SETTINGS', '3': 'NEW_IDENTIFIER' },
|
||||
};
|
||||
const humanToRealMapping = {
|
||||
SETTINGS: 'SETTINGS',
|
||||
// NEW_IDENTIFIER not in mapping (new in this version)
|
||||
};
|
||||
|
||||
const result = promptSync.applyCustomizationsToPrompt(
|
||||
newPrompt,
|
||||
humanToRealMapping
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'User ${SETTINGS.userName}, new field: ${${NEW_IDENTIFIER}.newFeature}!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareVersions', () => {
|
||||
it('should return -1 when v1 < v2', () => {
|
||||
expect(promptSync.compareVersions('1.0.0', '2.0.0')).toBe(-1);
|
||||
expect(promptSync.compareVersions('1.2.3', '1.2.4')).toBe(-1);
|
||||
expect(promptSync.compareVersions('1.9.0', '1.10.0')).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return 0 when versions are equal', () => {
|
||||
expect(promptSync.compareVersions('1.0.0', '1.0.0')).toBe(0);
|
||||
expect(promptSync.compareVersions('2.5.7', '2.5.7')).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 1 when v1 > v2', () => {
|
||||
expect(promptSync.compareVersions('2.0.0', '1.0.0')).toBe(1);
|
||||
expect(promptSync.compareVersions('1.2.4', '1.2.3')).toBe(1);
|
||||
expect(promptSync.compareVersions('1.10.0', '1.9.0')).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle versions with different lengths', () => {
|
||||
expect(promptSync.compareVersions('1.0', '1.0.0')).toBe(0);
|
||||
expect(promptSync.compareVersions('1.0.1', '1.0')).toBe(1);
|
||||
expect(promptSync.compareVersions('1.0', '1.0.1')).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPromptFilePath', () => {
|
||||
it('should return correct file path for prompt', () => {
|
||||
const result = promptSync.getPromptFilePath('test-prompt');
|
||||
expect(result).toContain('test-prompt.md');
|
||||
expect(result).toContain('system-prompts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptFileExists', () => {
|
||||
it('should return true if file exists', async () => {
|
||||
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
||||
|
||||
const result = await promptSync.promptFileExists('test-prompt');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if file does not exist', async () => {
|
||||
vi.spyOn(fs, 'access').mockRejectedValue(createEnoent());
|
||||
|
||||
const result = await promptSync.promptFileExists('test-prompt');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readPromptFile', () => {
|
||||
it('should read and parse prompt file', async () => {
|
||||
const mockContent = `<!--
|
||||
name: Test Prompt
|
||||
description: Test
|
||||
ccVersion: 1.0.0
|
||||
-->
|
||||
|
||||
Content here`;
|
||||
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
|
||||
const result = await promptSync.readPromptFile('test-prompt');
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'Test Prompt',
|
||||
description: 'Test',
|
||||
ccVersion: '1.0.0',
|
||||
variables: [],
|
||||
content: 'Content here',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('writePromptFile', () => {
|
||||
it('should write prompt file', async () => {
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await promptSync.writePromptFile('test-prompt', 'content');
|
||||
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
const [filePath, content] = writeFileSpy.mock.calls[0];
|
||||
expect(filePath).toContain('test-prompt.md');
|
||||
expect(content).toBe('content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateVariables', () => {
|
||||
it('should update variables in frontmatter', async () => {
|
||||
const mockContent = `<!--
|
||||
name: Test
|
||||
description: Test
|
||||
ccVersion: 1.0.0
|
||||
variables:
|
||||
- OLD_VAR
|
||||
-->
|
||||
|
||||
Content`;
|
||||
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const newIdentifierMap = { '1': 'SETTINGS', '2': 'CONFIG' };
|
||||
await promptSync.updateVariables('test-prompt', newIdentifierMap);
|
||||
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
const writtenContent = writeFileSpy.mock.calls[0][1] as string;
|
||||
expect(writtenContent).toContain('SETTINGS');
|
||||
expect(writtenContent).toContain('CONFIG');
|
||||
expect(writtenContent).not.toContain('OLD_VAR');
|
||||
});
|
||||
|
||||
it('should remove variables field when identifierMap is empty', async () => {
|
||||
const mockContent = `<!--
|
||||
name: Test
|
||||
description: Test
|
||||
ccVersion: 1.0.0
|
||||
variables:
|
||||
- OLD_VAR
|
||||
-->
|
||||
|
||||
Content`;
|
||||
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await promptSync.updateVariables('test-prompt', {});
|
||||
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
const writtenContent = writeFileSpy.mock.calls[0][1] as string;
|
||||
expect(writtenContent).not.toContain('variables:');
|
||||
expect(writtenContent).not.toContain('OLD_VAR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDiffHtml', () => {
|
||||
it('should generate HTML diff file with two side-by-side diffs', async () => {
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const htmlPath = await promptSync.generateDiffHtml(
|
||||
'test-prompt',
|
||||
'Test Prompt',
|
||||
'Old baseline\nLine 2',
|
||||
'User customization\nLine 2',
|
||||
'New baseline\nLine 2',
|
||||
'1.0.0',
|
||||
'2.0.0',
|
||||
'/path/to/test-prompt.md'
|
||||
);
|
||||
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
expect(htmlPath).toContain('test-prompt.diff.html');
|
||||
|
||||
const htmlContent = writeFileSpy.mock.calls[0][1] as string;
|
||||
expect(htmlContent).toContain('<!DOCTYPE html>');
|
||||
expect(htmlContent).toContain('test-prompt');
|
||||
expect(htmlContent).toContain('1.0.0');
|
||||
expect(htmlContent).toContain('2.0.0');
|
||||
expect(htmlContent).toContain('Your Customizations');
|
||||
expect(htmlContent).toContain('Upstream Changes');
|
||||
});
|
||||
|
||||
it('should escape HTML in content', async () => {
|
||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
await promptSync.generateDiffHtml(
|
||||
'test',
|
||||
'Test',
|
||||
'<script>alert("xss")</script>',
|
||||
'<script>alert("xss")</script>',
|
||||
'Safe & sound',
|
||||
'1.0.0',
|
||||
'2.0.0',
|
||||
'/path/to/test.md'
|
||||
);
|
||||
|
||||
const htmlContent = (
|
||||
fs.writeFile as typeof fs.writeFile & {
|
||||
mock: { calls: [string, string][] };
|
||||
}
|
||||
).mock.calls[0][1] as string;
|
||||
expect(htmlContent).not.toContain('<script>');
|
||||
expect(htmlContent).toContain('<script>');
|
||||
expect(htmlContent).toContain('&');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncPrompt', () => {
|
||||
const mockPrompt: StringsPrompt = {
|
||||
id: 'test-id',
|
||||
name: 'test-prompt',
|
||||
description: 'Test prompt',
|
||||
version: '2.0.0',
|
||||
pieces: ['Greet user as ${', '.preferredName}!'],
|
||||
identifiers: [1],
|
||||
identifierMap: { '1': 'SETTINGS' },
|
||||
};
|
||||
|
||||
it('should create new file if it does not exist', async () => {
|
||||
vi.spyOn(fs, 'access').mockRejectedValue(createEnoent());
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await promptSync.syncPrompt(mockPrompt);
|
||||
|
||||
expect(result.action).toBe('created');
|
||||
expect(result.newVersion).toBe('2.0.0');
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if versions match', async () => {
|
||||
const mockContent = `<!--
|
||||
name: test-prompt
|
||||
description: Test prompt
|
||||
ccVersion: 2.0.0
|
||||
-->
|
||||
|
||||
Greet user as \${SETTINGS.preferredName}!`;
|
||||
|
||||
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
const result = await promptSync.syncPrompt(mockPrompt);
|
||||
|
||||
expect(result.action).toBe('skipped');
|
||||
expect(result.oldVersion).toBe('2.0.0');
|
||||
expect(result.newVersion).toBe('2.0.0');
|
||||
});
|
||||
|
||||
it('should warn about version mismatch when user version is older', async () => {
|
||||
const mockContent = `<!--
|
||||
name: test-prompt
|
||||
description: Test prompt
|
||||
ccVersion: 1.0.0
|
||||
-->
|
||||
|
||||
Greet user as \${SETTINGS.preferredName}!`;
|
||||
|
||||
// Mock the old strings file for downloading old version
|
||||
const mockOldStringsFile = {
|
||||
version: '1.0.0',
|
||||
prompts: [
|
||||
{
|
||||
id: 'test-prompt',
|
||||
name: 'test-prompt',
|
||||
description: 'Test prompt',
|
||||
pieces: ['Greet user as ', '!'],
|
||||
identifiers: [1],
|
||||
identifierMap: { '1': 'SETTINGS.preferredName' },
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { downloadStringsFile } = await import('./download.js');
|
||||
const hashIndexModule = await import('./systemPromptHashIndex.js');
|
||||
|
||||
vi.mocked(downloadStringsFile).mockResolvedValue(
|
||||
mockOldStringsFile as StringsFile
|
||||
);
|
||||
vi.spyOn(hashIndexModule, 'getPromptHash').mockResolvedValue(
|
||||
'different-hash'
|
||||
); // Simulate user modification
|
||||
|
||||
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
const result = await promptSync.syncPrompt(mockPrompt);
|
||||
|
||||
expect(result.action).toBe('conflict');
|
||||
expect(result.oldVersion).toBe('1.0.0');
|
||||
expect(result.newVersion).toBe('2.0.0');
|
||||
expect(result.diffHtmlPath).toBeDefined();
|
||||
});
|
||||
|
||||
it('should always update variables list', async () => {
|
||||
const mockContent = `<!--
|
||||
name: test-prompt
|
||||
description: Test prompt
|
||||
ccVersion: 2.0.0
|
||||
variables:
|
||||
- OLD_VAR
|
||||
-->
|
||||
|
||||
Greet user as \${SETTINGS.preferredName}!`;
|
||||
|
||||
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockContent);
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fs, 'writeFile')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await promptSync.syncPrompt(mockPrompt);
|
||||
|
||||
// Should have called writeFile for updating variables
|
||||
expect(writeFileSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncSystemPrompts', () => {
|
||||
it('should sync all prompts from strings file', async () => {
|
||||
const mockStringsFile: StringsFile = {
|
||||
version: '2.0.0',
|
||||
prompts: [
|
||||
{
|
||||
id: 'prompt1',
|
||||
name: 'Prompt 1',
|
||||
description: 'First prompt',
|
||||
version: '2.0.0',
|
||||
pieces: ['Content 1'],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
},
|
||||
{
|
||||
id: 'prompt2',
|
||||
name: 'Prompt 2',
|
||||
description: 'Second prompt',
|
||||
version: '2.0.0',
|
||||
pieces: ['Content 2'],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { downloadStringsFile } = await import('./download.js');
|
||||
const hashIndexModule = await import('./systemPromptHashIndex.js');
|
||||
|
||||
vi.mocked(downloadStringsFile).mockResolvedValue(
|
||||
mockStringsFile as StringsFile
|
||||
);
|
||||
vi.spyOn(hashIndexModule, 'storeHashes').mockResolvedValue(0);
|
||||
|
||||
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined);
|
||||
vi.spyOn(fs, 'access').mockRejectedValue(createEnoent());
|
||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
const result = await promptSync.syncSystemPrompts('2.0.0');
|
||||
|
||||
expect(result.ccVersion).toBe('2.0.0');
|
||||
expect(result.results).toHaveLength(2);
|
||||
expect(result.results[0].action).toBe('created');
|
||||
expect(result.results[1].action).toBe('created');
|
||||
});
|
||||
|
||||
it('should throw error for failed prompt syncs', async () => {
|
||||
const mockStringsFile: StringsFile = {
|
||||
version: '2.0.0',
|
||||
prompts: [
|
||||
{
|
||||
id: 'prompt1',
|
||||
name: 'Prompt 1',
|
||||
description: 'First prompt',
|
||||
version: '2.0.0',
|
||||
pieces: ['Content'],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { downloadStringsFile } = await import('./download.js');
|
||||
vi.mocked(downloadStringsFile).mockResolvedValue(
|
||||
mockStringsFile as StringsFile
|
||||
);
|
||||
|
||||
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined);
|
||||
// Make the file exist (so it tries to read it)
|
||||
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
||||
// But make reading fail with permission error
|
||||
vi.spyOn(fs, 'readFile').mockRejectedValue(
|
||||
new Error('Permission denied')
|
||||
);
|
||||
|
||||
await expect(promptSync.syncSystemPrompts('2.0.0')).rejects.toThrow(
|
||||
'Permission denied'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if download fails', async () => {
|
||||
const { downloadStringsFile } = await import('./download.js');
|
||||
vi.mocked(downloadStringsFile).mockRejectedValue(
|
||||
new Error('Download failed')
|
||||
);
|
||||
|
||||
await expect(promptSync.syncSystemPrompts('2.0.0')).rejects.toThrow(
|
||||
'Download failed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displaySyncResults', () => {
|
||||
it('should display sync results without errors', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
const summary = {
|
||||
ccVersion: '2.0.0',
|
||||
results: [
|
||||
{
|
||||
id: 'prompt1',
|
||||
name: 'Prompt 1',
|
||||
description: 'Test prompt 1',
|
||||
action: 'created' as const,
|
||||
newVersion: '2.0.0',
|
||||
},
|
||||
{
|
||||
id: 'prompt2',
|
||||
name: 'Prompt 2',
|
||||
description: 'Test prompt 2',
|
||||
action: 'skipped' as const,
|
||||
oldVersion: '2.0.0',
|
||||
newVersion: '2.0.0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
promptSync.displaySyncResults(summary);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const output = consoleSpy.mock.calls.map(call => call[0]).join('\n');
|
||||
expect(output).toContain('Skipped 1 up-to-date file');
|
||||
expect(output).toContain('Created 1 new prompt file');
|
||||
});
|
||||
|
||||
it('should display conflicts', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
const summary = {
|
||||
ccVersion: '2.0.0',
|
||||
results: [
|
||||
{
|
||||
id: 'prompt1',
|
||||
name: 'Prompt 1',
|
||||
description: 'Test prompt',
|
||||
action: 'conflict' as const,
|
||||
oldVersion: '1.0.0',
|
||||
newVersion: '2.0.0',
|
||||
diffHtmlPath: '/path/to/diff.html',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
promptSync.displaySyncResults(summary);
|
||||
|
||||
const output = consoleSpy.mock.calls.map(call => call[0]).join('\n');
|
||||
expect(output).toContain('Conflicts detected');
|
||||
expect(output).toContain('1.0.0');
|
||||
expect(output).toContain('2.0.0');
|
||||
expect(output).toContain('diff.html');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,303 @@
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { CONFIG_DIR } from './types.js';
|
||||
import { StringsFile, reconstructContentFromPieces } from './promptSync.js';
|
||||
|
||||
/**
|
||||
* Path to the system prompt hash index file
|
||||
*/
|
||||
export const HASH_INDEX_PATH = path.join(CONFIG_DIR, 'systemPromptHashes.json');
|
||||
|
||||
/**
|
||||
* Path to the system prompt applied hashes file
|
||||
* This tracks which hash was last applied to cli.js for each prompt
|
||||
*/
|
||||
export const APPLIED_HASHES_PATH = path.join(
|
||||
CONFIG_DIR,
|
||||
'systemPromptAppliedHashes.json'
|
||||
);
|
||||
|
||||
/**
|
||||
* Structure of the hash index
|
||||
* Maps: "prompt-id-version" => "md5hash"
|
||||
* Example: "main-system-prompt-2.0.14" => "a1b2c3..."
|
||||
*/
|
||||
export interface HashIndex {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a hash key for a prompt
|
||||
* Format: "{promptId}-{version}"
|
||||
*/
|
||||
export const getHashKey = (promptId: string, version: string): string => {
|
||||
return `${promptId}-${version}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the MD5 hash of a string (after trimming leading/trailing whitespace)
|
||||
*/
|
||||
export const computeMD5Hash = (content: string): string => {
|
||||
return crypto.createHash('md5').update(content.trim(), 'utf8').digest('hex');
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the hash index from disk. Returns empty object if file doesn't exist.
|
||||
*/
|
||||
export const readHashIndex = async (): Promise<HashIndex> => {
|
||||
try {
|
||||
const content = await fs.readFile(HASH_INDEX_PATH, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes the hash index to disk
|
||||
*/
|
||||
export const writeHashIndex = async (index: HashIndex): Promise<void> => {
|
||||
// Ensure config directory exists
|
||||
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
||||
|
||||
// Sort keys for consistent formatting
|
||||
const sortedIndex: HashIndex = {};
|
||||
const sortedKeys = Object.keys(index).sort();
|
||||
for (const key of sortedKeys) {
|
||||
sortedIndex[key] = index[key];
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
HASH_INDEX_PATH,
|
||||
JSON.stringify(sortedIndex, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main utility function: Takes the entire contents of a strings-x.y.z.json file
|
||||
* and inserts all the hashes that aren't already in the index.
|
||||
*
|
||||
* @param stringsFile - The parsed strings file (from downloadStringsFile or similar)
|
||||
* @returns The number of new hashes added
|
||||
*/
|
||||
export const storeHashes = async (
|
||||
stringsFile: StringsFile
|
||||
): Promise<number> => {
|
||||
const index = await readHashIndex();
|
||||
let newHashCount = 0;
|
||||
|
||||
for (const prompt of stringsFile.prompts) {
|
||||
const hashKey = getHashKey(prompt.id, prompt.version);
|
||||
|
||||
// Only compute and store if not already present
|
||||
if (!index[hashKey]) {
|
||||
const content = reconstructContentFromPieces(
|
||||
prompt.pieces,
|
||||
prompt.identifiers,
|
||||
prompt.identifierMap
|
||||
);
|
||||
const hash = computeMD5Hash(content);
|
||||
index[hashKey] = hash;
|
||||
newHashCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to disk
|
||||
await writeHashIndex(index);
|
||||
|
||||
return newHashCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the hash for a specific prompt version from the index
|
||||
* Returns undefined if not found
|
||||
*/
|
||||
export const getPromptHash = async (
|
||||
promptId: string,
|
||||
version: string
|
||||
): Promise<string | undefined> => {
|
||||
const index = await readHashIndex();
|
||||
const hashKey = getHashKey(promptId, version);
|
||||
return index[hashKey];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a markdown file's content (excluding frontmatter) matches the expected hash
|
||||
*
|
||||
* @param markdownContent - The full content of a markdown file with frontmatter
|
||||
* @param expectedHash - The expected MD5 hash
|
||||
* @returns true if the content matches the hash (unmodified), false otherwise
|
||||
*/
|
||||
export const verifyMarkdownHash = (
|
||||
markdownContent: string,
|
||||
expectedHash: string
|
||||
): boolean => {
|
||||
// Extract content after frontmatter
|
||||
const frontmatterMatch = markdownContent.match(
|
||||
/^---\n[\s\S]+?\n---\n([\s\S]*)$/
|
||||
);
|
||||
|
||||
if (!frontmatterMatch) {
|
||||
// No frontmatter found - hash the entire content
|
||||
const hash = computeMD5Hash(markdownContent.trim());
|
||||
return hash === expectedHash;
|
||||
}
|
||||
|
||||
// Hash only the content part (after frontmatter), trimmed
|
||||
const contentOnly = frontmatterMatch[1].trim();
|
||||
const hash = computeMD5Hash(contentOnly);
|
||||
return hash === expectedHash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure of the applied hashes file
|
||||
* Maps: "prompt-id" => "md5hash" | null
|
||||
* Example: "main-system-prompt" => "a1b2c3..." or null if not applied/defaults restored
|
||||
*/
|
||||
export interface AppliedHashIndex {
|
||||
[promptId: string]: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the applied hashes index from disk. Returns empty object if file doesn't exist.
|
||||
*/
|
||||
export const readAppliedHashIndex = async (): Promise<AppliedHashIndex> => {
|
||||
try {
|
||||
const content = await fs.readFile(APPLIED_HASHES_PATH, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes the applied hashes index to disk
|
||||
*/
|
||||
export const writeAppliedHashIndex = async (
|
||||
index: AppliedHashIndex
|
||||
): Promise<void> => {
|
||||
// Ensure config directory exists
|
||||
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
||||
|
||||
// Sort keys for consistent formatting
|
||||
const sortedIndex: AppliedHashIndex = {};
|
||||
const sortedKeys = Object.keys(index).sort();
|
||||
for (const key of sortedKeys) {
|
||||
sortedIndex[key] = index[key];
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
APPLIED_HASHES_PATH,
|
||||
JSON.stringify(sortedIndex, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the applied hash for a specific prompt ID
|
||||
*/
|
||||
export const setAppliedHash = async (
|
||||
promptId: string,
|
||||
hash: string
|
||||
): Promise<void> => {
|
||||
const index = await readAppliedHashIndex();
|
||||
index[promptId] = hash;
|
||||
await writeAppliedHashIndex(index);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets all applied hashes to null (used when restoring defaults)
|
||||
*/
|
||||
export const clearAllAppliedHashes = async (): Promise<void> => {
|
||||
const index = await readAppliedHashIndex();
|
||||
const clearedIndex: AppliedHashIndex = {};
|
||||
|
||||
// Set all existing entries to null
|
||||
for (const key of Object.keys(index)) {
|
||||
clearedIndex[key] = null;
|
||||
}
|
||||
|
||||
await writeAppliedHashIndex(clearedIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the applied hash for a specific prompt ID
|
||||
* Returns undefined if not found, null if explicitly set to null
|
||||
*/
|
||||
export const getAppliedHash = async (
|
||||
promptId: string
|
||||
): Promise<string | null | undefined> => {
|
||||
const index = await readAppliedHashIndex();
|
||||
return index[promptId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if any system prompts have been modified since they were last applied.
|
||||
* Compares the current hash of each prompt file with the hash stored in systemPromptAppliedHashes.json.
|
||||
*
|
||||
* @param systemPromptsDir - Path to the system prompts directory
|
||||
* @returns true if any prompts have been modified, false otherwise
|
||||
*/
|
||||
export const hasUnappliedSystemPromptChanges = async (
|
||||
systemPromptsDir: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const appliedHashes = await readAppliedHashIndex();
|
||||
|
||||
// If there are no applied hashes yet, nothing has been applied so no changes to track
|
||||
if (Object.keys(appliedHashes).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read all .md files in the system prompts directory
|
||||
const files = await fs.readdir(systemPromptsDir);
|
||||
const mdFiles = files.filter(f => f.endsWith('.md'));
|
||||
|
||||
for (const file of mdFiles) {
|
||||
const promptId = file.replace('.md', '');
|
||||
const appliedHash = appliedHashes[promptId];
|
||||
|
||||
// If this prompt doesn't have an applied hash entry, skip it
|
||||
if (appliedHash === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the applied hash is null (restored to defaults), skip comparison
|
||||
if (appliedHash === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read the current file and compute its hash
|
||||
const filePath = path.join(systemPromptsDir, file);
|
||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Parse the markdown to extract just the content (excluding frontmatter)
|
||||
const matter = await import('gray-matter');
|
||||
const parsed = matter.default(fileContent, {
|
||||
delimiters: ['<!--', '-->'],
|
||||
});
|
||||
const currentHash = computeMD5Hash(parsed.content);
|
||||
|
||||
// If the current hash doesn't match the applied hash, changes have been made
|
||||
if (currentHash !== appliedHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// If we can't read the directory or files, assume no changes
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
+3
-1
@@ -135,13 +135,14 @@ export interface StartupCheckInfo {
|
||||
}
|
||||
|
||||
export enum MainMenuItem {
|
||||
APPLY_CHANGES = '*Apply customizations to cli.js',
|
||||
THEMES = 'Themes',
|
||||
LAUNCH_TEXT = 'Launch text',
|
||||
THINKING_VERBS = 'Thinking verbs',
|
||||
THINKING_STYLE = 'Thinking style',
|
||||
USER_MESSAGE_DISPLAY = 'User message display',
|
||||
INPUT_BOX = 'Input box',
|
||||
APPLY_CHANGES = '*Apply customizations to cli.js',
|
||||
VIEW_SYSTEM_PROMPTS = 'View system prompts',
|
||||
RESTORE_ORIGINAL = 'Restore original Claude Code (preserves tweakcc.json)',
|
||||
OPEN_CONFIG = 'Open tweakcc.json',
|
||||
OPEN_CLI = "Open Claude Code's cli.js",
|
||||
@@ -839,6 +840,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
export const CONFIG_DIR = path.join(os.homedir(), '.tweakcc');
|
||||
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||
export const CLIJS_BACKUP_FILE = path.join(CONFIG_DIR, 'cli.js.backup');
|
||||
export const SYSTEM_PROMPTS_DIR = path.join(CONFIG_DIR, 'system-prompts');
|
||||
|
||||
export interface SearchPathInfo {
|
||||
pattern: string;
|
||||
|
||||
Generated
+530
@@ -0,0 +1,530 @@
|
||||
{
|
||||
"name": "prompt-extractor",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "prompt-extractor",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@babel/parser": "^7.26.3",
|
||||
"ollama": "^0.6.0",
|
||||
"openai": "^4.73.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
|
||||
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.0.tgz",
|
||||
"integrity": "sha512-FHjdU2Ok5x2HZsxPui/MBJZ5J+HzmxoWYa/p9wk736eT+uAhS8nvIICar5YgwlG5MFNjDR6UA5F3RSKq+JseOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.104.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
||||
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "prompt-extractor",
|
||||
"version": "0.0.0",
|
||||
"description": "Extract AI system prompts from minified JavaScript",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@babel/parser": "^7.26.3",
|
||||
"ollama": "^0.6.0",
|
||||
"openai": "^4.73.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const parser = require('@babel/parser');
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function validateInput(text, minLength = 500) {
|
||||
if (!text || typeof text !== 'string') return false;
|
||||
if (text.length < minLength) return false;
|
||||
|
||||
const first10 = text.substring(0, 10);
|
||||
if (first10.startsWith('AGFzbQ') || /^[A-Z0-9+/=]{10}$/.test(first10)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sample = text.substring(0, 500);
|
||||
const words = sample.split(/\s+/).filter(w => w.length > 0);
|
||||
|
||||
if (words.length === 0) return false;
|
||||
|
||||
const uppercaseWords = words.filter(
|
||||
w => w === w.toUpperCase() && /[A-Z]/.test(w)
|
||||
);
|
||||
const uppercaseRatio = uppercaseWords.length / words.length;
|
||||
|
||||
if (uppercaseRatio > 0.6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
const hasYou = lowerText.includes('you');
|
||||
const hasAI = lowerText.includes('ai') || lowerText.includes('assistant');
|
||||
const hasInstruct =
|
||||
lowerText.includes('must') ||
|
||||
lowerText.includes('should') ||
|
||||
lowerText.includes('always');
|
||||
|
||||
if (!hasYou && !hasAI && !hasInstruct) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sentencePattern = /[.!?]\s+[A-Z\(]/;
|
||||
const hasSentences = sentencePattern.test(text);
|
||||
if (!hasSentences) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const avgWordLength =
|
||||
words.reduce((sum, w) => sum + w.length, 0) / words.length;
|
||||
|
||||
if (avgWordLength > 15) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const spaceCount = (sample.match(/\s/g) || []).length;
|
||||
const spaceRatio = spaceCount / sample.length;
|
||||
|
||||
if (spaceRatio < 0.1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function extractStrings(filepath, minLength = 500) {
|
||||
const code = fs.readFileSync(filepath, 'utf-8');
|
||||
|
||||
const ast = parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['jsx', 'typescript'],
|
||||
});
|
||||
|
||||
const stringData = [];
|
||||
|
||||
const traverse = node => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
// Extract string literals
|
||||
if (node.type === 'StringLiteral') {
|
||||
if (validateInput(node.value, minLength)) {
|
||||
stringData.push({
|
||||
name: '',
|
||||
id: '',
|
||||
description: '',
|
||||
pieces: [node.value],
|
||||
identifiers: [],
|
||||
identifierMap: {},
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract template literals
|
||||
if (node.type === 'TemplateLiteral') {
|
||||
const { expressions } = node;
|
||||
|
||||
// Extract the entire template content directly from source (excluding backticks)
|
||||
const contentStart = node.start + 1; // After opening backtick
|
||||
const contentEnd = node.end - 1; // Before closing backtick
|
||||
const fullContent = code.substring(contentStart, contentEnd);
|
||||
|
||||
// Validate before processing
|
||||
if (!validateInput(fullContent, minLength)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all identifiers with their positions
|
||||
const allIdentifiers = []; // Array of {name, start, end} sorted by position
|
||||
|
||||
for (let i = 0; i < expressions.length; i++) {
|
||||
const expr = expressions[i];
|
||||
|
||||
const traverseExpr = (exprNode, isTopLevel = true) => {
|
||||
if (!exprNode || typeof exprNode !== 'object') return;
|
||||
|
||||
if (exprNode.type === 'Identifier' && isTopLevel) {
|
||||
allIdentifiers.push({
|
||||
name: exprNode.name,
|
||||
start: exprNode.start - contentStart,
|
||||
end: exprNode.end - contentStart,
|
||||
});
|
||||
}
|
||||
|
||||
if (exprNode.type === 'CallExpression') {
|
||||
traverseExpr(exprNode.callee, true);
|
||||
if (exprNode.arguments) {
|
||||
exprNode.arguments.forEach(arg => traverseExpr(arg, true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (exprNode.type === 'MemberExpression') {
|
||||
traverseExpr(exprNode.object, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exprNode.type === 'TemplateLiteral') {
|
||||
if (exprNode.expressions) {
|
||||
exprNode.expressions.forEach(nestedExpr =>
|
||||
traverseExpr(nestedExpr, true)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (exprNode.type === 'ObjectExpression') {
|
||||
if (exprNode.properties) {
|
||||
exprNode.properties.forEach(prop => {
|
||||
if (prop.value) {
|
||||
traverseExpr(prop.value, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in exprNode) {
|
||||
if (key === 'loc' || key === 'start' || key === 'end') continue;
|
||||
const value = exprNode[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => traverseExpr(v, true));
|
||||
} else if (value && typeof value === 'object') {
|
||||
traverseExpr(value, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverseExpr(expr, true);
|
||||
}
|
||||
|
||||
// Sort identifiers by position
|
||||
allIdentifiers.sort((a, b) => a.start - b.start);
|
||||
|
||||
// Build pieces array by splitting around identifiers, keeping ${ and }
|
||||
const pieces = [];
|
||||
const identifierList = [];
|
||||
const identifierMap = {};
|
||||
|
||||
let lastPos = 0;
|
||||
|
||||
for (const id of allIdentifiers) {
|
||||
// Find the ${ before this identifier (search backwards from id.start)
|
||||
let beforeIdentifier = fullContent.substring(lastPos, id.start);
|
||||
|
||||
// Find the } after this identifier (search forwards from id.end)
|
||||
// We need to find the matching closing brace for the interpolation
|
||||
let afterIdentifierStart = id.end;
|
||||
|
||||
// Add the piece including everything up to and including just before the identifier
|
||||
pieces.push(beforeIdentifier);
|
||||
|
||||
// Add identifier to the list
|
||||
identifierList.push(id.name);
|
||||
|
||||
// Add to map if not already there
|
||||
if (!identifierMap[id.name]) {
|
||||
identifierMap[id.name] = '';
|
||||
}
|
||||
|
||||
lastPos = id.end;
|
||||
}
|
||||
|
||||
// Add the final piece after the last identifier
|
||||
pieces.push(fullContent.substring(lastPos));
|
||||
|
||||
// Label encode the identifiers
|
||||
const uniqueVars = [...new Set(identifierList)];
|
||||
const varToLabel = {};
|
||||
uniqueVars.forEach((varName, idx) => {
|
||||
varToLabel[varName] = idx;
|
||||
});
|
||||
|
||||
const labelEncodedIdentifiers = identifierList.map(
|
||||
varName => varToLabel[varName]
|
||||
);
|
||||
const labelEncodedMap = {};
|
||||
Object.keys(varToLabel).forEach(varName => {
|
||||
labelEncodedMap[varToLabel[varName]] = '';
|
||||
});
|
||||
|
||||
stringData.push({
|
||||
name: '',
|
||||
id: '',
|
||||
description: '',
|
||||
pieces,
|
||||
identifiers: labelEncodedIdentifiers,
|
||||
identifierMap: labelEncodedMap,
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively traverse
|
||||
for (const key in node) {
|
||||
if (key === 'loc' || key === 'start' || key === 'end') continue;
|
||||
|
||||
const value = node[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(traverse);
|
||||
} else if (value && typeof value === 'object') {
|
||||
traverse(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverse(ast);
|
||||
|
||||
// Filter out strings that are subsets of other strings
|
||||
// Step 1: Sort by start index (ascending), then by end index (descending)
|
||||
// This puts earliest strings first, and among strings with same start, longest first
|
||||
stringData.sort((a, b) => {
|
||||
if (a.start !== b.start) {
|
||||
return a.start - b.start;
|
||||
}
|
||||
return b.end - a.end;
|
||||
});
|
||||
|
||||
// Step 2: Track seen ranges and filter out subsets
|
||||
const seenRanges = [];
|
||||
const filteredData = [];
|
||||
|
||||
for (const item of stringData) {
|
||||
const isSubset = seenRanges.some(
|
||||
range => item.start >= range.start && item.end <= range.end
|
||||
);
|
||||
|
||||
if (!isSubset) {
|
||||
filteredData.push(item);
|
||||
seenRanges.push({ start: item.start, end: item.end });
|
||||
}
|
||||
}
|
||||
|
||||
return { prompts: filteredData };
|
||||
}
|
||||
|
||||
function mergeWithExisting(newData, oldData, currentVersion) {
|
||||
if (!oldData || !oldData.prompts) {
|
||||
// No old data, add current version to all new prompts
|
||||
return {
|
||||
prompts: newData.prompts.map(item => ({
|
||||
...item,
|
||||
version: currentVersion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to reconstruct content from pieces and identifiers
|
||||
const reconstructContent = item => {
|
||||
return item.pieces.join(''); // Don't actually insert the vairables.
|
||||
};
|
||||
|
||||
const newPrompts = newData.prompts.map((newItem, idx) => {
|
||||
const newContent = reconstructContent(newItem);
|
||||
|
||||
// Try to find a matching old item by content and label-encoded identifiers
|
||||
const matchingOld = oldData.prompts.find(oldItem => {
|
||||
const oldContent = reconstructContent(oldItem);
|
||||
if (newContent !== oldContent) return false;
|
||||
|
||||
// Also compare label-encoded identifiers
|
||||
if (newItem.identifiers.length !== oldItem.identifiers.length)
|
||||
return false;
|
||||
return (
|
||||
JSON.stringify(newItem.identifiers) ===
|
||||
JSON.stringify(oldItem.identifiers)
|
||||
);
|
||||
});
|
||||
|
||||
// If we found a match, copy over the metadata
|
||||
if (matchingOld) {
|
||||
// Prompt matches exactly
|
||||
// If old prompt has no version, use current version; otherwise use old version
|
||||
return {
|
||||
...newItem,
|
||||
name: matchingOld.name,
|
||||
id: matchingOld.id || slugify(matchingOld.name),
|
||||
description: matchingOld.description,
|
||||
identifierMap: matchingOld.identifierMap,
|
||||
version: matchingOld.version || currentVersion,
|
||||
};
|
||||
}
|
||||
|
||||
// No exact match found - check if there's a prompt with same metadata but different content
|
||||
const similarOld = oldData.prompts.find(oldItem => {
|
||||
// Check if names match (not placeholder) as a heuristic for "same prompt, different content"
|
||||
return oldItem.name !== '' && oldItem.name === newItem.name;
|
||||
});
|
||||
|
||||
if (similarOld && similarOld.version) {
|
||||
// Old prompt exists with a version and content changed - use current version
|
||||
console.log(
|
||||
`Content changed for "${newItem.name}", updating version from ${similarOld.version} to ${currentVersion}`
|
||||
);
|
||||
return {
|
||||
...newItem,
|
||||
id: similarOld.id || slugify(similarOld.name),
|
||||
version: currentVersion,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if there's any old prompt without a version (we should add current version)
|
||||
const oldWithoutVersion = oldData.prompts.find(oldItem => !oldItem.version);
|
||||
|
||||
// New prompt or old prompt didn't have version - add current version
|
||||
console.log(
|
||||
`No match for item ${idx}: ${JSON.stringify(newContent.slice(0, 100))}`
|
||||
);
|
||||
console.log();
|
||||
return {
|
||||
...newItem,
|
||||
id: slugify(newItem.name),
|
||||
version: currentVersion,
|
||||
};
|
||||
});
|
||||
|
||||
return { prompts: newPrompts };
|
||||
}
|
||||
|
||||
// CLI
|
||||
if (require.main === module) {
|
||||
const filepath = process.argv[2];
|
||||
|
||||
if (!filepath) {
|
||||
console.error(
|
||||
'Usage: node promptExtractor.cjs <path-to-cli.js> [output-file]'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputFile = process.argv[3] || 'prompts.json';
|
||||
|
||||
// Try to read existing output file
|
||||
let existingData = null;
|
||||
if (fs.existsSync(outputFile)) {
|
||||
try {
|
||||
const existingContent = fs.readFileSync(outputFile, 'utf-8');
|
||||
existingData = JSON.parse(existingContent);
|
||||
console.log(
|
||||
`Found existing output file with ${existingData.prompts?.length || 0} prompts`
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Warning: Could not parse existing output file: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for package.json alongside the input file
|
||||
const path = require('path');
|
||||
const inputDir = path.dirname(path.resolve(filepath));
|
||||
const packageJsonPath = path.join(inputDir, 'package.json');
|
||||
|
||||
let version = null;
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
version = packageJson.version;
|
||||
console.log(`Found package.json with version ${version}`);
|
||||
} catch (err) {
|
||||
console.warn(`Warning: Could not parse package.json: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to replace version strings with placeholder
|
||||
const replaceVersionInString = (str, versionStr) => {
|
||||
if (!versionStr) return str;
|
||||
// Escape dots for regex
|
||||
const escapedVersion = versionStr.replace(/\./g, '\\.');
|
||||
// Replace version with placeholder
|
||||
return str.replace(new RegExp(escapedVersion, 'g'), '<<CCVERSION>>');
|
||||
};
|
||||
|
||||
const replaceVersionInPrompts = (data, versionStr) => {
|
||||
if (!versionStr) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
prompts: data.prompts.map(prompt => ({
|
||||
...prompt,
|
||||
pieces: prompt.pieces.map(piece =>
|
||||
replaceVersionInString(piece, versionStr)
|
||||
),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const result = extractStrings(filepath);
|
||||
// Replace version in newly extracted strings BEFORE merging
|
||||
const versionReplacedResult = replaceVersionInPrompts(result, version);
|
||||
|
||||
const mergedResult = mergeWithExisting(
|
||||
versionReplacedResult,
|
||||
existingData,
|
||||
version
|
||||
);
|
||||
|
||||
// Sort prompts by lexicographic order of pieces joined together (without interpolated vars)
|
||||
mergedResult.prompts.sort((a, b) => {
|
||||
const contentA = a.pieces.join('');
|
||||
const contentB = b.pieces.join('');
|
||||
return contentA.localeCompare(contentB);
|
||||
});
|
||||
|
||||
// Remove start/end fields before writing
|
||||
mergedResult.prompts = mergedResult.prompts.map(({ start, end, ...rest }) => rest);
|
||||
|
||||
// Add version as top-level field
|
||||
const outputData = {
|
||||
version,
|
||||
...mergedResult,
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
||||
|
||||
console.log(`Extracted ${mergedResult.prompts.length} strings`);
|
||||
console.log(`Written to ${outputFile}`);
|
||||
}
|
||||
|
||||
module.exports = extractStrings;
|
||||
Reference in New Issue
Block a user