open-walnut 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/bin/open-walnut.js +2 -0
  4. package/bin/walnut.js +2 -0
  5. package/dist/cli.js +137849 -0
  6. package/dist/daemon-binaries/acp-worker.js +44 -0
  7. package/dist/daemon-binaries/acp-worker.js.version +1 -0
  8. package/dist/data/skills/daily-standup/SKILL.md +16 -0
  9. package/dist/data/skills/install-plugin/SKILL.md +118 -0
  10. package/dist/data/skills/learn/SKILL.md +46 -0
  11. package/dist/data/skills/register-repo/SKILL.md +20 -0
  12. package/dist/data/skills/setup-cloud-companion/SKILL.md +326 -0
  13. package/dist/data/skills/triage/SKILL.md +23 -0
  14. package/dist/data/skills/walnut-calendar/SKILL.md +92 -0
  15. package/dist/data/skills/walnut-manage-repos/SKILL.md +107 -0
  16. package/dist/data/skills/walnut-peer-sessions/SKILL.md +61 -0
  17. package/dist/data/skills/walnut-register-repo/SKILL.md +72 -0
  18. package/dist/data/skills/walnut-register-repo/references/yaml-format.md +111 -0
  19. package/dist/data/skills/walnut-register-repo/scripts/validate-repo.py +177 -0
  20. package/dist/data/skills/walnut-routines/SKILL.md +65 -0
  21. package/dist/data/skills/weekly-review/SKILL.md +22 -0
  22. package/dist/data/walnut-calendar.swift +213 -0
  23. package/dist/data/walnut-extract.swift +80 -0
  24. package/dist/hooks/on-compact.js +976 -0
  25. package/dist/hooks/on-stop.js +1329 -0
  26. package/dist/integrations/jira/index.js +4200 -0
  27. package/dist/integrations/jira/manifest.json +26 -0
  28. package/dist/integrations/local/index.js +51 -0
  29. package/dist/integrations/local/manifest.json +10 -0
  30. package/dist/integrations/ms-todo/index.js +4672 -0
  31. package/dist/integrations/ms-todo/manifest.json +17 -0
  32. package/dist/session-server/index.js +1382 -0
  33. package/dist/web/server.js +135275 -0
  34. package/dist/web/static/assets/apl-B4CMkyY2.js +1 -0
  35. package/dist/web/static/assets/asciiarmor-Df11BRmG.js +1 -0
  36. package/dist/web/static/assets/asn1-EdZsLKOL.js +1 -0
  37. package/dist/web/static/assets/asterisk-B-8jnY81.js +1 -0
  38. package/dist/web/static/assets/brainfuck-C4LP7Hcl.js +1 -0
  39. package/dist/web/static/assets/clike-B9uivgTg.js +1 -0
  40. package/dist/web/static/assets/clojure-BMjYHr_A.js +1 -0
  41. package/dist/web/static/assets/cmake-BQqOBYOt.js +1 -0
  42. package/dist/web/static/assets/cobol-CWcv1MsR.js +1 -0
  43. package/dist/web/static/assets/coffeescript-S37ZYGWr.js +1 -0
  44. package/dist/web/static/assets/commonlisp-DBKNyK5s.js +1 -0
  45. package/dist/web/static/assets/crystal-SjHAIU92.js +1 -0
  46. package/dist/web/static/assets/css-BnMrqG3P.js +1 -0
  47. package/dist/web/static/assets/cypher-C_CwsFkJ.js +1 -0
  48. package/dist/web/static/assets/d-pRatUO7H.js +1 -0
  49. package/dist/web/static/assets/diff-DbItnlRl.js +1 -0
  50. package/dist/web/static/assets/dockerfile-BKs6k2Af.js +1 -0
  51. package/dist/web/static/assets/dtd-DF_7sFjM.js +1 -0
  52. package/dist/web/static/assets/dylan-DwRh75JA.js +1 -0
  53. package/dist/web/static/assets/ebnf-CDyGwa7X.js +1 -0
  54. package/dist/web/static/assets/ecl-Cabwm37j.js +1 -0
  55. package/dist/web/static/assets/eiffel-CnydiIhH.js +1 -0
  56. package/dist/web/static/assets/elm-vLlmbW-K.js +1 -0
  57. package/dist/web/static/assets/erlang-BNw1qcRV.js +1 -0
  58. package/dist/web/static/assets/factor-kuTfRLto.js +1 -0
  59. package/dist/web/static/assets/fcl-Kvtd6kyn.js +1 -0
  60. package/dist/web/static/assets/forth-Ffai-XNe.js +1 -0
  61. package/dist/web/static/assets/fortran-DYz_wnZ1.js +1 -0
  62. package/dist/web/static/assets/gas-Bneqetm1.js +1 -0
  63. package/dist/web/static/assets/gherkin-heZmZLOM.js +1 -0
  64. package/dist/web/static/assets/groovy-D9Dt4D0W.js +1 -0
  65. package/dist/web/static/assets/haskell-Cw1EW3IL.js +1 -0
  66. package/dist/web/static/assets/haxe-H-WmDvRZ.js +1 -0
  67. package/dist/web/static/assets/http-DBlCnlav.js +1 -0
  68. package/dist/web/static/assets/idl-BEugSyMb.js +1 -0
  69. package/dist/web/static/assets/index-B3K5yAdA.js +1 -0
  70. package/dist/web/static/assets/index-B7OvpXOs.js +632 -0
  71. package/dist/web/static/assets/index-C07UNJIJ.js +1 -0
  72. package/dist/web/static/assets/index-C8k-3wfu.js +2 -0
  73. package/dist/web/static/assets/index-CBXGsT3t.js +1 -0
  74. package/dist/web/static/assets/index-CUrO3qpX.js +1 -0
  75. package/dist/web/static/assets/index-CpTvBBAa.js +1 -0
  76. package/dist/web/static/assets/index-D4J6slTf.js +1 -0
  77. package/dist/web/static/assets/index-DUgC5K_q.js +3 -0
  78. package/dist/web/static/assets/index-DhqXE1wa.js +1 -0
  79. package/dist/web/static/assets/index-DkGqHTCk.js +1 -0
  80. package/dist/web/static/assets/index-DsxFiBic.js +1 -0
  81. package/dist/web/static/assets/index-Dz3umkfm.js +1 -0
  82. package/dist/web/static/assets/index-G2v0oaqp.js +1 -0
  83. package/dist/web/static/assets/index-GNDKJKwy.js +1 -0
  84. package/dist/web/static/assets/index-U1CVtNUH.css +32 -0
  85. package/dist/web/static/assets/index-ft4kpQXq.js +1 -0
  86. package/dist/web/static/assets/index-ls7h0u-p.js +1 -0
  87. package/dist/web/static/assets/index-nL5l5WPW.js +7 -0
  88. package/dist/web/static/assets/javascript-iXu5QeM3.js +1 -0
  89. package/dist/web/static/assets/julia-DuME0IfC.js +1 -0
  90. package/dist/web/static/assets/livescript-BwQOo05w.js +1 -0
  91. package/dist/web/static/assets/lua-VAEuO923.js +1 -0
  92. package/dist/web/static/assets/mathematica-DTrFuWx2.js +1 -0
  93. package/dist/web/static/assets/mbox-CNhZ1qSd.js +1 -0
  94. package/dist/web/static/assets/mirc-CjQqDB4T.js +1 -0
  95. package/dist/web/static/assets/mllike-CXdrOF99.js +1 -0
  96. package/dist/web/static/assets/modelica-Dc1JOy9r.js +1 -0
  97. package/dist/web/static/assets/mscgen-BA5vi2Kp.js +1 -0
  98. package/dist/web/static/assets/mumps-BT43cFF4.js +1 -0
  99. package/dist/web/static/assets/nginx-DdIZxoE0.js +1 -0
  100. package/dist/web/static/assets/nsis-LdVXkNf5.js +1 -0
  101. package/dist/web/static/assets/ntriples-BfvgReVJ.js +1 -0
  102. package/dist/web/static/assets/octave-Ck1zUtKM.js +1 -0
  103. package/dist/web/static/assets/oz-BzwKVEFT.js +1 -0
  104. package/dist/web/static/assets/pascal--L3eBynH.js +1 -0
  105. package/dist/web/static/assets/perl-CdXCOZ3F.js +1 -0
  106. package/dist/web/static/assets/pig-CevX1Tat.js +1 -0
  107. package/dist/web/static/assets/powershell-CFHJl5sT.js +1 -0
  108. package/dist/web/static/assets/properties-C78fOPTZ.js +1 -0
  109. package/dist/web/static/assets/protobuf-ChK-085T.js +1 -0
  110. package/dist/web/static/assets/pug-DeIclll2.js +1 -0
  111. package/dist/web/static/assets/puppet-DMA9R1ak.js +1 -0
  112. package/dist/web/static/assets/python-BuPzkPfP.js +1 -0
  113. package/dist/web/static/assets/q-pXgVlZs6.js +1 -0
  114. package/dist/web/static/assets/r-B6wPVr8A.js +1 -0
  115. package/dist/web/static/assets/rpm-CTu-6PCP.js +1 -0
  116. package/dist/web/static/assets/ruby-B2Rjki9n.js +1 -0
  117. package/dist/web/static/assets/sas-B4kiWyti.js +1 -0
  118. package/dist/web/static/assets/scheme-C41bIUwD.js +1 -0
  119. package/dist/web/static/assets/shell-CjFT_Tl9.js +1 -0
  120. package/dist/web/static/assets/sieve-C3Gn_uJK.js +1 -0
  121. package/dist/web/static/assets/simple-mode-GW_nhZxv.js +1 -0
  122. package/dist/web/static/assets/smalltalk-CnHTOXQT.js +1 -0
  123. package/dist/web/static/assets/solr-DehyRSwq.js +1 -0
  124. package/dist/web/static/assets/sparql-DkYu6x3z.js +1 -0
  125. package/dist/web/static/assets/spreadsheet-BCZA_wO0.js +1 -0
  126. package/dist/web/static/assets/sql-D0XecflT.js +1 -0
  127. package/dist/web/static/assets/stex-C3f8Ysf7.js +1 -0
  128. package/dist/web/static/assets/stylus-B533Al4x.js +1 -0
  129. package/dist/web/static/assets/swift-BzpIVaGY.js +1 -0
  130. package/dist/web/static/assets/tcl-DVfN8rqt.js +1 -0
  131. package/dist/web/static/assets/textile-CnDTJFAw.js +1 -0
  132. package/dist/web/static/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  133. package/dist/web/static/assets/tiki-DGYXhP31.js +1 -0
  134. package/dist/web/static/assets/toml-Bm5Em-hy.js +1 -0
  135. package/dist/web/static/assets/troff-wAsdV37c.js +1 -0
  136. package/dist/web/static/assets/ttcn-CfJYG6tj.js +1 -0
  137. package/dist/web/static/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  138. package/dist/web/static/assets/turtle-B1tBg_DP.js +1 -0
  139. package/dist/web/static/assets/vb-CmGdzxic.js +1 -0
  140. package/dist/web/static/assets/vbscript-BuJXcnF6.js +1 -0
  141. package/dist/web/static/assets/velocity-D8B20fx6.js +1 -0
  142. package/dist/web/static/assets/verilog-C6RDOZhf.js +1 -0
  143. package/dist/web/static/assets/vhdl-lSbBsy5d.js +1 -0
  144. package/dist/web/static/assets/webidl-ZXfAyPTL.js +1 -0
  145. package/dist/web/static/assets/xquery-DzFWVndE.js +1 -0
  146. package/dist/web/static/assets/yacas-BJ4BC0dw.js +1 -0
  147. package/dist/web/static/assets/z80-Hz9HOZM7.js +1 -0
  148. package/dist/web/static/index.html +22 -0
  149. package/dist/web/static/walnut-icon.png +0 -0
  150. package/dist/workers/git-compaction-worker.js +898 -0
  151. package/dist/workers/qmd-index-worker.js +97162 -0
  152. package/package.json +143 -0
  153. package/patches/@anthropic-ai+claude-agent-sdk+0.2.50.patch +13 -0
  154. package/scripts/patch-qmd.mjs +171 -0
  155. package/scripts/postinstall.mjs +120 -0
package/package.json ADDED
@@ -0,0 +1,143 @@
1
+ {
2
+ "name": "open-walnut",
3
+ "version": "0.3.0",
4
+ "description": "Personal AI butler powered by Claude — task management, Claude Code sessions, memory system, and web UI",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/EvanZhang008/open-walnut",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/EvanZhang008/open-walnut.git"
10
+ },
11
+ "keywords": [
12
+ "ai",
13
+ "task-manager",
14
+ "claude",
15
+ "claude-code",
16
+ "personal-assistant",
17
+ "productivity",
18
+ "self-hosted",
19
+ "local-first",
20
+ "ai-agent",
21
+ "web-ui",
22
+ "knowledge-management",
23
+ "typescript",
24
+ "cli",
25
+ "ai-butler",
26
+ "automation"
27
+ ],
28
+ "type": "module",
29
+ "bin": {
30
+ "open-walnut": "./bin/open-walnut.js",
31
+ "walnut": "./bin/open-walnut.js"
32
+ },
33
+ "main": "./dist/cli.js",
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "dist",
40
+ "!dist/daemon-binaries/daemon-*",
41
+ "!dist/**/*.map",
42
+ "!dist/**/*.gz",
43
+ "patches",
44
+ "scripts/patch-qmd.mjs",
45
+ "scripts/postinstall.mjs",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "scripts": {
50
+ "postinstall": "node scripts/postinstall.mjs",
51
+ "prepublishOnly": "npm run build && cd web && npx vite build && cd .. && node scripts/check-publish.mjs",
52
+ "start": "npm run web:build && node dist/cli.js web --port 3456",
53
+ "build": "bash scripts/build-daemon.sh && tsup && rm -rf dist/data && cp -r src/data dist/ && for d in src/integrations/*/manifest.json; do cp \"$d\" \"dist/integrations/$(basename $(dirname $d))/\"; done",
54
+ "dev": "tsup --watch",
55
+ "test": "node scripts/test-parallel.mjs",
56
+ "test:quick": "vitest run --config vitest.quick.config.ts",
57
+ "test:slow": "vitest run --config vitest.slow.config.ts",
58
+ "test:focus": "vitest run --config vitest.focus.config.ts",
59
+ "test:frontend:ci": "node scripts/test-frontend.mjs",
60
+ "test:changed": "node scripts/test-changed.mjs",
61
+ "test:baseline": "node scripts/test-baseline.mjs check",
62
+ "test:baseline:record": "node scripts/test-baseline.mjs record",
63
+ "test:pre-commit": "node scripts/test-changed.mjs --tiers",
64
+ "test:unit": "vitest run --config vitest.unit.config.ts",
65
+ "test:integration": "vitest run --config vitest.integration.config.ts",
66
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
67
+ "test:live": "WALNUT_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
68
+ "test:live:daemon": "bash scripts/run-live-daemon-tests.sh",
69
+ "test:live:cloud": "bash scripts/run-live-cloud-tests.sh",
70
+ "test:live:aws-setup": "bash scripts/run-live-aws-cloud-setup.sh",
71
+ "test:full": "npm run test && npm run test:e2e && npm run test:live:daemon && npm run test:live:cloud",
72
+ "test:all": "npm run lint && npm run lint:paths && npm run test && npx playwright test",
73
+ "lint:paths": "bash scripts/lint-production-paths.sh",
74
+ "test:coverage": "vitest run --coverage",
75
+ "test:watch": "vitest",
76
+ "build:daemon": "bash scripts/build-daemon.sh",
77
+ "lint": "tsc --noEmit",
78
+ "web": "node dist/web/server.js",
79
+ "web:dev": "concurrently \"tsup --watch\" \"cd web && npx vite\"",
80
+ "web:build": "bash scripts/build-daemon.sh && tsup && rm -rf dist/data && cp -r src/data dist/ && for d in src/integrations/*/manifest.json; do cp \"$d\" \"dist/integrations/$(basename $(dirname $d))/\"; done && cd web && npx vite build",
81
+ "dev:ephemeral": "npm run web:build && node dist/cli.js web --ephemeral",
82
+ "dev:demo": "npm run web:build && (lsof -ti:3457 -sTCP:LISTEN | xargs kill -15 2>/dev/null || true); (cat /tmp/open-walnut-demo/daemon.pid 2>/dev/null | xargs kill -15 2>/dev/null || true); sleep 1; echo \"Demo server: http://localhost:3457 (data: $HOME/.open-walnut-demo, daemon: /tmp/open-walnut-demo — fully isolated from prod)\"; OPEN_WALNUT_HOME=\"$HOME/.open-walnut-demo\" WALNUT_DAEMON_DIR=/tmp/open-walnut-demo node dist/cli.js web --port 3457",
83
+ "dev:prod": "bash scripts/dev-prod.sh",
84
+ "prepare": "husky"
85
+ },
86
+ "dependencies": {
87
+ "@agentclientprotocol/codex-acp": "1.1.4",
88
+ "@agentclientprotocol/sdk": "1.2.1",
89
+ "@anthropic-ai/bedrock-sdk": "^0.26.3",
90
+ "@anthropic-ai/claude-agent-sdk": "0.2.50",
91
+ "@anthropic-ai/tokenizer": "^0.0.4",
92
+ "@aws-sdk/client-sts": "^3.996.0",
93
+ "@azure/msal-node": "^5.0.3",
94
+ "@google/generative-ai": "^0.24.1",
95
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
96
+ "@mozilla/readability": "^0.6.0",
97
+ "@slack/web-api": "^7.14.0",
98
+ "@tiptap/extension-image": "^3.20.1",
99
+ "@tobilu/qmd": "2.1.0",
100
+ "better-sqlite3": "^12.6.2",
101
+ "chalk": "^5.4.1",
102
+ "commander": "^14.0.0",
103
+ "compression": "^1.8.1",
104
+ "cors": "^2.8.6",
105
+ "croner": "^10.0.1",
106
+ "esbuild": "^0.27.3",
107
+ "express": "^5.2.1",
108
+ "image-size": "^2.0.2",
109
+ "js-yaml": "^4.1.0",
110
+ "linkedom": "^0.18.12",
111
+ "marked": "^15.0.12",
112
+ "marked-terminal": "^7.3.0",
113
+ "openai": "^6.29.0",
114
+ "sharp": "^0.34.5",
115
+ "ws": "^8.19.0"
116
+ },
117
+ "optionalDependencies": {
118
+ "screencapturekit-audio-capture": "^1.3.6"
119
+ },
120
+ "devDependencies": {
121
+ "@playwright/test": "^1.58.2",
122
+ "@types/better-sqlite3": "^7.6.13",
123
+ "@types/compression": "^1.8.1",
124
+ "@types/cors": "^2.8.19",
125
+ "@types/express": "^5.0.6",
126
+ "@types/image-size": "^0.7.0",
127
+ "@types/js-yaml": "^4.0.9",
128
+ "@types/marked-terminal": "^6.1.1",
129
+ "@types/node": "^22.15.0",
130
+ "@types/sharp": "^0.31.1",
131
+ "@types/supertest": "^6.0.3",
132
+ "@types/ws": "^8.18.1",
133
+ "@vitest/coverage-v8": "^3.1.0",
134
+ "concurrently": "^9.2.1",
135
+ "husky": "^9.1.7",
136
+ "patch-package": "^8.0.1",
137
+ "supertest": "^7.2.2",
138
+ "tsup": "^8.5.0",
139
+ "tsx": "^4.23.1",
140
+ "typescript": "^5.8.0",
141
+ "vitest": "^3.1.0"
142
+ }
143
+ }
@@ -0,0 +1,13 @@
1
+ diff --git a/node_modules/@anthropic-ai/claude-agent-sdk/cli.js b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js
2
+ index 394c46b..745b6cc 100755
3
+ --- a/node_modules/@anthropic-ai/claude-agent-sdk/cli.js
4
+ +++ b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js
5
+ @@ -4895,7 +4895,7 @@ The scratchpad directory is session-specific, isolated from the user's project,
6
+ <system-reminder>
7
+ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
8
+ </system-reminder>
9
+ -`,D4z;var XR=E(()=>{H4();P7();V8();rS();j4();cK6();NX8();hz();bH1();S1();N_();uz();$z1();cX8();kA();zV();TA();Rwq();gZ1();p1();Aq();j4();U1();Ue();t7z=new Set(["/dev/zero","/dev/random","/dev/urandom","/dev/full","/dev/stdin","/dev/tty","/dev/console","/dev/stdout","/dev/stderr","/dev/fd/0","/dev/fd/1","/dev/fd/2"]);A4z=String.fromCharCode(8239);K4z=[];jZ1=class jZ1 extends Error{tokenCount;maxTokens;constructor(A,q){super(`File content (${A} tokens) exceeds maximum allowed tokens (${q}). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.`);this.tokenCount=A;this.maxTokens=q;this.name="MaxFileReadTokenExceededError"}};z4z=new Set(["png","jpg","jpeg","gif","webp"]),w4z=new Set(["mp3","wav","flac","ogg","aac","m4a","wma","aiff","opus","mp4","avi","mov","wmv","flv","mkv","webm","m4v","mpeg","mpg","zip","rar","tar","gz","bz2","7z","xz","z","tgz","iso","exe","dll","so","dylib","app","msi","deb","rpm","bin","dat","db","sqlite","sqlite3","mdb","idx","doc","docx","xls","xlsx","ppt","pptx","odt","ods","odp","ttf","otf","woff","woff2","eot","psd","ai","eps","sketch","fig","xd","blend","obj","3ds","max","class","jar","war","pyc","pyo","rlib","swf","fla"]);$4z=y1(()=>u.strictObject({file_path:u.string().describe("The absolute path to the file to read"),offset:u.number().optional().describe("The line number to start reading from. Only provide if the file is too large to read at once"),limit:u.number().optional().describe("The number of lines to read. Only provide if the file is too large to read at once."),pages:u.string().optional().describe(`Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${_D6} pages per request.`)})),H4z=u.enum(["image/jpeg","image/png","image/gif","image/webp"]),O4z=y1(()=>u.discriminatedUnion("type",[u.object({type:u.literal("text"),file:u.object({filePath:u.string().describe("The path to the file that was read"),content:u.string().describe("The content of the file"),numLines:u.number().describe("Number of lines in the returned content"),startLine:u.number().describe("The starting line number"),totalLines:u.number().describe("Total number of lines in the file")})}),u.object({type:u.literal("image"),file:u.object({base64:u.string().describe("Base64-encoded image data"),type:H4z.describe("The MIME type of the image"),originalSize:u.number().describe("Original file size in bytes"),dimensions:u.object({originalWidth:u.number().optional().describe("Original image width in pixels"),originalHeight:u.number().optional().describe("Original image height in pixels"),displayWidth:u.number().optional().describe("Displayed image width in pixels (after resizing)"),displayHeight:u.number().optional().describe("Displayed image height in pixels (after resizing)")}).optional().describe("Image dimension info for coordinate mapping")})}),u.object({type:u.literal("notebook"),file:u.object({filePath:u.string().describe("The path to the notebook file"),cells:u.array(u.any()).describe("Array of notebook cells")})}),u.object({type:u.literal("pdf"),file:u.object({filePath:u.string().describe("The path to the PDF file"),base64:u.string().describe("Base64-encoded PDF data"),originalSize:u.number().describe("Original file size in bytes")})}),u.object({type:u.literal("parts"),file:u.object({filePath:u.string().describe("The path to the PDF file"),originalSize:u.number().describe("Original file size in bytes"),count:u.number().describe("Number of pages extracted"),outputDir:u.string().describe("Directory containing extracted page images")})})])),B5={name:wq,maxResultSizeChars:1e5,strict:!0,input_examples:[{file_path:"/Users/username/project/src/index.ts"},{file_path:"/Users/username/project/README.md",limit:100,offset:50}],async description(){return if7},async prompt(){return nf7},get inputSchema(){return $4z()},inputParamAliases:{filePath:"file_path",filepath:"file_path",path:"file_path"},get outputSchema(){return O4z()},userFacingName:ywq,getToolUseSummary:zg8,getActivityDescription(A){let q=zg8(A);return q?`Reading ${q}`:"Reading file"},isEnabled(){return!0},isConcurrencySafe(){return!0},isReadOnly(){return!0},isSearchOrReadCommand(){return{isSearch:!1,isRead:!0}},getPath({file_path:A}){return A||L1()},async checkPermissions(A,q){let K=await q.getAppState();return ve(B5,A,K.toolPermissionContext)},renderToolUseMessage:Vwq,renderToolUseTag:Nwq,renderToolUseProgressMessage:vwq,renderToolResultMessage:kwq,renderToolUseRejectedMessage:Ewq,renderToolUseErrorMessage:Lwq,async validateInput({file_path:A,pages:q},K){if(q!==void 0){let H=tY8(q);if(!H)return{result:!1,message:`Invalid pages parameter: "${q}". Use formats like "1-5", "3", or "10-20". Pages are 1-indexed.`,errorCode:7};if((H.lastPage===1/0?_D6+1:H.lastPage-H.firstPage+1)>_D6)return{result:!1,message:`Page range "${q}" exceeds maximum of ${_D6} pages per request. Please use a smaller range.`,errorCode:8}}let Y=e4(A),z=await K.getAppState();if(PM(Y,z.toolPermissionContext,"read","deny")!==null)return{result:!1,message:"File is in a directory that is denied by your permission settings.",errorCode:1};if(Y.startsWith("\\\\")||Y.startsWith("//"))return{result:!0};let $=Yz6.extname(Y).toLowerCase();if(w4z.has($.slice(1))&&!ID6($))return{result:!1,message:`This tool cannot read binary files. The file appears to be a binary ${$} file. Please use appropriate tools for binary file analysis.`,errorCode:4};if(e7z(Y))return{result:!1,message:`Cannot read '${A}': this device file would block or produce infinite output.`,errorCode:9};return{result:!0}},async call({file_path:A,offset:q=1,limit:K=void 0,pages:Y},z){let{readFileState:w,fileReadingLimits:_}=z,$=_?.maxSizeBytes??_g8,H=_?.maxTokens??wg8(),O=Yz6.extname(A).toLowerCase().slice(1),J=e4(A),j=L1(),D=vG6([J],j);if(D.length>0){for(let X of D)z.dynamicSkillDirTriggers?.add(X);kG6(D).catch(()=>{})}EG6([J],j);try{return await Swq(A,J,J,O,q,K,Y,$,H,w,z)}catch(X){if(X.code==="ENOENT"){let P=q4z(J);if(P)try{return await Swq(A,J,P,O,q,K,Y,$,H,w,z)}catch(T){if(T.code!=="ENOENT")throw T}let W=SN1(J),Z=await ke(J),G=`File does not exist. ${jZ} ${L1()}.`;if(Z)G+=` Did you mean ${Z}?`;else if(W)G+=` Did you mean ${W}?`;throw Error(G)}throw X}},mapToolResultToToolResultBlockParam(A,q){switch(A.type){case"image":return{tool_use_id:q,type:"tool_result",content:[{type:"image",source:{type:"base64",data:A.file.base64,media_type:A.file.type}}]};case"notebook":return So7(A.file.cells,q);case"pdf":return{tool_use_id:q,type:"tool_result",content:`PDF file read: ${A.file.filePath} (${E3(A.file.originalSize)})`};case"parts":return{tool_use_id:q,type:"tool_result",content:`PDF pages extracted: ${A.file.count} page(s) from ${A.file.filePath} (${E3(A.file.originalSize)})`};case"text":{let K;if(A.file.content)K=J4z(A.file)+(X4z()?j4z:"");else K=A.file.totalLines===0?"<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>":`<system-reminder>Warning: the file exists but is shorter than the provided offset (${A.file.startLine}). The file has ${A.file.totalLines} lines.</system-reminder>`;return{tool_use_id:q,type:"tool_result",content:K}}}}};D4z=new Set(["claude-opus-4-6"])});function Pv1(A=process.env){let q=A.BASH_DEFAULT_TIMEOUT_MS;if(q){let K=parseInt(q,10);if(!isNaN(K)&&K>0)return K}return 120000}function hwq(A=process.env){let q=A.BASH_MAX_TIMEOUT_MS;if(q){let K=parseInt(q,10);if(!isNaN(K)&&K>0)return Math.max(K,Pv1(A))}return Math.max(600000,Pv1(A))}function Wv1(){return Pv1()}function Zv1(){return hwq()}function M4z(){if(!bA.isSandboxingEnabled())return"";let A=bA.getFsReadConfig(),q=bA.getFsWriteConfig(),K=bA.getNetworkRestrictionConfig(),Y=bA.getAllowUnixSockets(),z=bA.getIgnoreViolations(),w=bA.areUnsandboxedCommandsAllowed(),_={read:A,write:q},$={...K?.allowedHosts&&{allowedHosts:K.allowedHosts},...K?.deniedHosts&&{deniedHosts:K.deniedHosts},...Y&&{allowUnixSockets:Y}},H=[];if(Object.keys(_).length>0)H.push(` - Filesystem: ${F6(_,null,2).split(`
10
+ +`,D4z;var XR=E(()=>{H4();P7();V8();rS();j4();cK6();NX8();hz();bH1();S1();N_();uz();$z1();cX8();kA();zV();TA();Rwq();gZ1();p1();Aq();j4();U1();Ue();t7z=new Set(["/dev/zero","/dev/random","/dev/urandom","/dev/full","/dev/stdin","/dev/tty","/dev/console","/dev/stdout","/dev/stderr","/dev/fd/0","/dev/fd/1","/dev/fd/2"]);A4z=String.fromCharCode(8239);K4z=[];jZ1=class jZ1 extends Error{tokenCount;maxTokens;constructor(A,q){super(`File content (${A} tokens) exceeds maximum allowed tokens (${q}). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.`);this.tokenCount=A;this.maxTokens=q;this.name="MaxFileReadTokenExceededError"}};z4z=new Set(["png","jpg","jpeg","gif","webp"]),w4z=new Set(["mp3","wav","flac","ogg","aac","m4a","wma","aiff","opus","mp4","avi","mov","wmv","flv","mkv","webm","m4v","mpeg","mpg","zip","rar","tar","gz","bz2","7z","xz","z","tgz","iso","exe","dll","so","dylib","app","msi","deb","rpm","bin","dat","db","sqlite","sqlite3","mdb","idx","doc","docx","xls","xlsx","ppt","pptx","odt","ods","odp","ttf","otf","woff","woff2","eot","psd","ai","eps","sketch","fig","xd","blend","obj","3ds","max","class","jar","war","pyc","pyo","rlib","swf","fla"]);$4z=y1(()=>u.strictObject({file_path:u.string().describe("The absolute path to the file to read"),offset:u.number().optional().describe("The line number to start reading from. Only provide if the file is too large to read at once"),limit:u.number().optional().describe("The number of lines to read. Only provide if the file is too large to read at once."),pages:u.string().optional().describe(`Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${_D6} pages per request.`)})),H4z=u.enum(["image/jpeg","image/png","image/gif","image/webp"]),O4z=y1(()=>u.discriminatedUnion("type",[u.object({type:u.literal("text"),file:u.object({filePath:u.string().describe("The path to the file that was read"),content:u.string().describe("The content of the file"),numLines:u.number().describe("Number of lines in the returned content"),startLine:u.number().describe("The starting line number"),totalLines:u.number().describe("Total number of lines in the file")})}),u.object({type:u.literal("image"),file:u.object({base64:u.string().describe("Base64-encoded image data"),type:H4z.describe("The MIME type of the image"),originalSize:u.number().describe("Original file size in bytes"),dimensions:u.object({originalWidth:u.number().optional().describe("Original image width in pixels"),originalHeight:u.number().optional().describe("Original image height in pixels"),displayWidth:u.number().optional().describe("Displayed image width in pixels (after resizing)"),displayHeight:u.number().optional().describe("Displayed image height in pixels (after resizing)")}).optional().describe("Image dimension info for coordinate mapping")})}),u.object({type:u.literal("notebook"),file:u.object({filePath:u.string().describe("The path to the notebook file"),cells:u.array(u.any()).describe("Array of notebook cells")})}),u.object({type:u.literal("pdf"),file:u.object({filePath:u.string().describe("The path to the PDF file"),base64:u.string().describe("Base64-encoded PDF data"),originalSize:u.number().describe("Original file size in bytes")})}),u.object({type:u.literal("parts"),file:u.object({filePath:u.string().describe("The path to the PDF file"),originalSize:u.number().describe("Original file size in bytes"),count:u.number().describe("Number of pages extracted"),outputDir:u.string().describe("Directory containing extracted page images")})})])),B5={name:wq,maxResultSizeChars:1e5,strict:!0,input_examples:[{file_path:"/Users/username/project/src/index.ts"},{file_path:"/Users/username/project/README.md",limit:100,offset:50}],async description(){return if7},async prompt(){return nf7},get inputSchema(){return $4z()},inputParamAliases:{filePath:"file_path",filepath:"file_path",path:"file_path"},get outputSchema(){return O4z()},userFacingName:ywq,getToolUseSummary:zg8,getActivityDescription(A){let q=zg8(A);return q?`Reading ${q}`:"Reading file"},isEnabled(){return!0},isConcurrencySafe(){return!0},isReadOnly(){return!0},isSearchOrReadCommand(){return{isSearch:!1,isRead:!0}},getPath({file_path:A}){return A||L1()},async checkPermissions(A,q){let K=await q.getAppState();return ve(B5,A,K.toolPermissionContext)},renderToolUseMessage:Vwq,renderToolUseTag:Nwq,renderToolUseProgressMessage:vwq,renderToolResultMessage:kwq,renderToolUseRejectedMessage:Ewq,renderToolUseErrorMessage:Lwq,async validateInput({file_path:A,pages:q},K){if(q!==void 0){let H=tY8(q);if(!H)return{result:!1,message:`Invalid pages parameter: "${q}". Use formats like "1-5", "3", or "10-20". Pages are 1-indexed.`,errorCode:7};if((H.lastPage===1/0?_D6+1:H.lastPage-H.firstPage+1)>_D6)return{result:!1,message:`Page range "${q}" exceeds maximum of ${_D6} pages per request. Please use a smaller range.`,errorCode:8}}let Y=e4(A),z=await K.getAppState();if(PM(Y,z.toolPermissionContext,"read","deny")!==null)return{result:!1,message:"File is in a directory that is denied by your permission settings.",errorCode:1};if(Y.startsWith("\\\\")||Y.startsWith("//"))return{result:!0};let $=Yz6.extname(Y).toLowerCase();if(w4z.has($.slice(1))&&!ID6($))return{result:!1,message:`This tool cannot read binary files. The file appears to be a binary ${$} file. Please use appropriate tools for binary file analysis.`,errorCode:4};if(e7z(Y))return{result:!1,message:`Cannot read '${A}': this device file would block or produce infinite output.`,errorCode:9};return{result:!0}},async call({file_path:A,offset:q=1,limit:K=void 0,pages:Y},z){let{readFileState:w,fileReadingLimits:_}=z,$=_?.maxSizeBytes??_g8,H=_?.maxTokens??wg8(),O=Yz6.extname(A).toLowerCase().slice(1),J=e4(A),j=L1(),D=vG6([J],j);if(D.length>0){for(let X of D)z.dynamicSkillDirTriggers?.add(X);kG6(D).catch(()=>{})}EG6([J],j);try{return await Swq(A,J,J,O,q,K,Y,$,H,w,z)}catch(X){if(X.code==="ENOENT"){let P=q4z(J);if(P)try{return await Swq(A,J,P,O,q,K,Y,$,H,w,z)}catch(T){if(T.code!=="ENOENT")throw T}let W=SN1(J),Z=await ke(J),G=`File does not exist. ${jZ} ${L1()}.`;if(Z)G+=` Did you mean ${Z}?`;else if(W)G+=` Did you mean ${W}?`;throw Error(G)}throw X}},mapToolResultToToolResultBlockParam(A,q){switch(A.type){case"image":return{tool_use_id:q,type:"tool_result",content:[{type:"image",source:{type:"base64",data:A.file.base64,media_type:A.file.type}}]};case"notebook":return So7(A.file.cells,q);case"pdf":return{tool_use_id:q,type:"tool_result",content:`PDF file read: ${A.file.filePath} (${E3(A.file.originalSize)})`};case"parts":return{tool_use_id:q,type:"tool_result",content:`PDF pages extracted: ${A.file.count} page(s) from ${A.file.filePath} (${E3(A.file.originalSize)})`};case"text":{let K;if(A.file.content)K=J4z(A.file)+("");else K=A.file.totalLines===0?"<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>":`<system-reminder>Warning: the file exists but is shorter than the provided offset (${A.file.startLine}). The file has ${A.file.totalLines} lines.</system-reminder>`;return{tool_use_id:q,type:"tool_result",content:K}}}}};D4z=new Set(["claude-opus-4-6"])});function Pv1(A=process.env){let q=A.BASH_DEFAULT_TIMEOUT_MS;if(q){let K=parseInt(q,10);if(!isNaN(K)&&K>0)return K}return 120000}function hwq(A=process.env){let q=A.BASH_MAX_TIMEOUT_MS;if(q){let K=parseInt(q,10);if(!isNaN(K)&&K>0)return Math.max(K,Pv1(A))}return Math.max(600000,Pv1(A))}function Wv1(){return Pv1()}function Zv1(){return hwq()}function M4z(){if(!bA.isSandboxingEnabled())return"";let A=bA.getFsReadConfig(),q=bA.getFsWriteConfig(),K=bA.getNetworkRestrictionConfig(),Y=bA.getAllowUnixSockets(),z=bA.getIgnoreViolations(),w=bA.areUnsandboxedCommandsAllowed(),_={read:A,write:q},$={...K?.allowedHosts&&{allowedHosts:K.allowedHosts},...K?.deniedHosts&&{deniedHosts:K.deniedHosts},...Y&&{allowUnixSockets:Y}},H=[];if(Object.keys(_).length>0)H.push(` - Filesystem: ${F6(_,null,2).split(`
11
+ `).join(`
12
+ `)}`);if(Object.keys($).length>0)H.push(` - Network: ${F6($,null,2).split(`
13
+ `).join(`
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install patch for @tobilu/qmd@2.1.0 — fixes two search-recall issues:
4
+ *
5
+ * 1. Hardcoded per-sub-search limit of 20 in hybridQuery() and structuredSearch()
6
+ * prevents documents ranked 21-40 from entering the RRF fusion pool. Replaced
7
+ * with candidateLimit (default RERANK_CANDIDATE_LIMIT = 40).
8
+ *
9
+ * 2. Dotted version tokens (e.g., "4.7", "v2.0.1") in buildFTS5Query() — the dot
10
+ * is stripped by sanitizeFTS5Term, collapsing "4.7" into "47" which never
11
+ * matches the FTS5 tokens "4" and "7". Added a dotted-token handler that
12
+ * splits on dots and creates a phrase query, matching the hyphenated-token pattern.
13
+ */
14
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
15
+ import { resolve, dirname } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { createRequire } from 'module';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+
21
+ // QMD may be in root and/or web node_modules — patch all copies found.
22
+ const locations = [
23
+ resolve(__dirname, '../node_modules/@tobilu/qmd'),
24
+ resolve(__dirname, '../web/node_modules/@tobilu/qmd'),
25
+ ];
26
+
27
+ // Published-package installs hoist qmd to a PARENT node_modules where neither
28
+ // hardcoded path exists — add wherever Node actually resolves it from. The
29
+ // package exports map blocks './package.json', so resolve the main entry
30
+ // (dist/index.js) and walk up to the package root.
31
+ try {
32
+ const require_ = createRequire(import.meta.url);
33
+ let dir = dirname(require_.resolve('@tobilu/qmd'));
34
+ while (dir !== dirname(dir) && !existsSync(resolve(dir, 'package.json'))) dir = dirname(dir);
35
+ if (!locations.includes(dir)) locations.push(dir);
36
+ } catch { /* not installed at all — the loop below reports it */ }
37
+
38
+ let anyVerified = false;
39
+
40
+ for (const qmdDir of locations) {
41
+ const storeFile = resolve(qmdDir, 'dist/store.js');
42
+ if (!existsSync(storeFile)) continue;
43
+
44
+ // ── Version guard ──
45
+ const pkgFile = resolve(qmdDir, 'package.json');
46
+ let pkg;
47
+ try {
48
+ pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
49
+ } catch (error) {
50
+ throw new Error(`patch-qmd: could not read ${pkgFile}: ${error}`);
51
+ }
52
+ if (pkg.version !== '2.1.0') {
53
+ throw new Error(
54
+ `patch-qmd: expected @tobilu/qmd@2.1.0 but found @${pkg.version} at ${qmdDir}`,
55
+ );
56
+ }
57
+
58
+ const originalSrc = readFileSync(storeFile, 'utf8');
59
+ let src = originalSrc;
60
+ let applied = 0;
61
+ const expected = 6;
62
+
63
+ // ── Fix 1: Replace hardcoded limit 20 with candidateLimit ──
64
+
65
+ // hybridQuery: initial FTS probe
66
+ const hq1 = 'const initialFts = store.searchFTS(query, 20, collection);';
67
+ if (src.includes(hq1)) {
68
+ src = src.replace(hq1, 'const initialFts = store.searchFTS(query, candidateLimit, collection);');
69
+ applied++;
70
+ }
71
+
72
+ // hybridQuery: expanded lex FTS
73
+ const hq2_old = 'const ftsResults = store.searchFTS(q.query, 20, collection);';
74
+ if (src.includes(hq2_old)) {
75
+ src = src.replace(hq2_old, 'const ftsResults = store.searchFTS(q.query, candidateLimit, collection);');
76
+ applied++;
77
+ }
78
+
79
+ // hybridQuery: vec search
80
+ const hq3_old = 'const vecResults = await store.searchVec(vecQueries[i].text, DEFAULT_EMBED_MODEL, 20, collection, undefined, embedding);';
81
+ if (src.includes(hq3_old)) {
82
+ src = src.replace(hq3_old,
83
+ 'const vecResults = await store.searchVec(vecQueries[i].text, DEFAULT_EMBED_MODEL, candidateLimit, collection, undefined, embedding);');
84
+ applied++;
85
+ }
86
+
87
+ // structuredSearch: lex FTS (with coll parameter)
88
+ const ss1_old = 'const ftsResults = store.searchFTS(search.query, 20, coll);';
89
+ if (src.includes(ss1_old)) {
90
+ src = src.replace(ss1_old, 'const ftsResults = store.searchFTS(search.query, candidateLimit, coll);');
91
+ applied++;
92
+ }
93
+
94
+ // structuredSearch: vec search (with coll parameter)
95
+ const ss2_old = 'const vecResults = await store.searchVec(vecSearches[i].query, DEFAULT_EMBED_MODEL, 20, coll, undefined, embedding);';
96
+ if (src.includes(ss2_old)) {
97
+ src = src.replace(ss2_old,
98
+ 'const vecResults = await store.searchVec(vecSearches[i].query, DEFAULT_EMBED_MODEL, candidateLimit, coll, undefined, embedding);');
99
+ applied++;
100
+ }
101
+
102
+ // ── Fix 2: Handle dotted version tokens in buildFTS5Query ──
103
+
104
+ const dottedInsertion = ` else if (/^[\\p{L}\\p{N}][\\p{L}\\p{N}.]*\\.[\\p{L}\\p{N}][\\p{L}\\p{N}.]*$/u.test(term)) {
105
+ const sanitized = term.split('.').map(t => sanitizeFTS5Term(t)).filter(t => t).join(' ');
106
+ if (sanitized) {
107
+ const ftsPhrase = \`"\${sanitized}"\`; // Phrase match
108
+ if (negated) {
109
+ negative.push(ftsPhrase);
110
+ }
111
+ else {
112
+ positive.push(ftsPhrase);
113
+ }
114
+ }
115
+ }`;
116
+
117
+ // Find the closing brace of the hyphenated-token block and insert after it
118
+ const hyphenatedClose = ` }
119
+ }
120
+ else {
121
+ const sanitized = sanitizeFTS5Term(term);`;
122
+
123
+ // Idempotency: check for the actual injected regex code, not a comment string
124
+ if (src.includes(hyphenatedClose) && !src.includes('else if (/^[\\p{L}\\p{N}]')) {
125
+ src = src.replace(
126
+ hyphenatedClose,
127
+ ` }
128
+ }
129
+ // Handle dotted version tokens: 4.7, v2.0.1, 3.14
130
+ // FTS5 tokenizer splits on dots, so "4.7" becomes tokens "4","7".
131
+ // Without this, sanitizeFTS5Term strips the dot \u2192 "47" which never matches.
132
+ ${dottedInsertion}
133
+ else {
134
+ const sanitized = sanitizeFTS5Term(term);`
135
+ );
136
+ applied++;
137
+ }
138
+
139
+ const requiredSnippets = [
140
+ 'store.searchFTS(query, candidateLimit, collection)',
141
+ 'store.searchFTS(q.query, candidateLimit, collection)',
142
+ 'store.searchVec(vecQueries[i].text, DEFAULT_EMBED_MODEL, candidateLimit, collection',
143
+ 'store.searchFTS(search.query, candidateLimit, coll)',
144
+ 'store.searchVec(vecSearches[i].query, DEFAULT_EMBED_MODEL, candidateLimit, coll',
145
+ 'Handle dotted version tokens:',
146
+ "term.split('.').map",
147
+ ];
148
+ const missing = requiredSnippets.filter((snippet) => !src.includes(snippet));
149
+ if (missing.length > 0) {
150
+ throw new Error(
151
+ `patch-qmd: patch contract incomplete at ${qmdDir} `
152
+ + `(${applied}/${expected} replacements applied); missing: ${missing.join(', ')}`,
153
+ );
154
+ }
155
+
156
+ if (src !== originalSrc) {
157
+ writeFileSync(storeFile, src, 'utf8');
158
+ console.log(`patch-qmd: patched ${storeFile} \u2714`);
159
+ } else {
160
+ console.log(`patch-qmd: ${storeFile} \u2014 patch contract verified`);
161
+ }
162
+ anyVerified = true;
163
+ }
164
+
165
+ if (!anyVerified) {
166
+ // Check if we found any QMD at all
167
+ const found = locations.some(d => existsSync(resolve(d, 'dist/store.js')));
168
+ if (!found) {
169
+ console.log('patch-qmd: @tobilu/qmd not installed, skipping');
170
+ }
171
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unified postinstall — routes between two very different install shapes:
4
+ *
5
+ * DEV CHECKOUT (git clone; web/ + src/ present):
6
+ * patch-package && (cd web && npm install) && node scripts/patch-qmd.mjs
7
+ * Strict: any failure fails the install, same as the historical inline chain.
8
+ *
9
+ * PUBLISHED PACKAGE (npm install open-walnut / npx open-walnut):
10
+ * - patch-package is a devDependency and, even if present, resolves targets
11
+ * via ./node_modules/<name> — with npm hoisting the SDK lives in the
12
+ * PARENT node_modules, so it can never find it. Instead we parse our own
13
+ * patch file (single-hunk, single-line change) and apply it directly to
14
+ * the resolved install location.
15
+ * - patch-qmd.mjs handles its own resolution (require-based candidate).
16
+ * Fail-open: a cosmetic patch failure must never break `npm install` for a
17
+ * consumer — warn loudly and continue.
18
+ */
19
+ import { spawnSync } from 'node:child_process';
20
+ import { createRequire } from 'node:module';
21
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
22
+ import { dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const isDevCheckout =
27
+ existsSync(join(root, 'web', 'package.json')) && existsSync(join(root, 'src'));
28
+
29
+ function run(cmd, args, opts = {}) {
30
+ const res = spawnSync(cmd, args, { stdio: 'inherit', cwd: root, ...opts });
31
+ return res.status === 0;
32
+ }
33
+
34
+ if (isDevCheckout) {
35
+ // ── Dev chain (strict) ──
36
+ if (!run('npx', ['patch-package'])) process.exit(1);
37
+ if (!run('npm', ['install'], { cwd: join(root, 'web') })) process.exit(1);
38
+ if (!run(process.execPath, [join(root, 'scripts', 'patch-qmd.mjs')])) process.exit(1);
39
+ process.exit(0);
40
+ }
41
+
42
+ // ── Published-package chain (fail-open) ──
43
+
44
+ function warn(msg) {
45
+ console.warn(`open-walnut postinstall: ${msg}`);
46
+ }
47
+
48
+ /**
49
+ * Apply the claude-agent-sdk reminder patch by parsing the shipped
50
+ * patch-package diff. The patch is one hunk changing one line of the minified
51
+ * cli.js bundle, which makes direct find-and-replace both exact and safe.
52
+ * The .patch file stays the single source of truth — no duplicated strings.
53
+ */
54
+ function applySdkPatch() {
55
+ const patchesDir = join(root, 'patches');
56
+ let patchFile = null;
57
+ try {
58
+ patchFile = readdirSync(patchesDir).find(
59
+ (f) => f.startsWith('@anthropic-ai+claude-agent-sdk+') && f.endsWith('.patch'),
60
+ );
61
+ } catch { /* no patches dir shipped */ }
62
+ if (!patchFile) return warn('SDK patch file not found, skipping');
63
+
64
+ const patchVersion = patchFile.slice('@anthropic-ai+claude-agent-sdk+'.length, -'.patch'.length);
65
+
66
+ // Resolve the installed SDK (may be hoisted to a parent node_modules).
67
+ let sdkDir;
68
+ try {
69
+ const require_ = createRequire(import.meta.url);
70
+ let dir = dirname(require_.resolve('@anthropic-ai/claude-agent-sdk'));
71
+ while (dir !== dirname(dir) && !existsSync(join(dir, 'package.json'))) dir = dirname(dir);
72
+ sdkDir = dir;
73
+ } catch {
74
+ return warn('claude-agent-sdk not resolvable, skipping SDK patch');
75
+ }
76
+
77
+ const installedVersion = JSON.parse(readFileSync(join(sdkDir, 'package.json'), 'utf8')).version;
78
+ if (installedVersion !== patchVersion) {
79
+ return warn(
80
+ `SDK ${installedVersion} != patch target ${patchVersion} — skipping (file reads will carry the SDK's cyber reminder, costing context)`,
81
+ );
82
+ }
83
+
84
+ // Extract the one removed and one added line from the diff.
85
+ const lines = readFileSync(join(patchesDir, patchFile), 'utf8').split('\n');
86
+ const removed = lines.filter((l) => l.startsWith('-') && !l.startsWith('---')).map((l) => l.slice(1));
87
+ const added = lines.filter((l) => l.startsWith('+') && !l.startsWith('+++')).map((l) => l.slice(1));
88
+ if (removed.length !== 1 || added.length !== 1) {
89
+ return warn(`SDK patch shape changed (${removed.length}-/${added.length}+ lines), skipping`);
90
+ }
91
+
92
+ const cliPath = join(sdkDir, 'cli.js');
93
+ let content;
94
+ try {
95
+ content = readFileSync(cliPath, 'utf8');
96
+ } catch {
97
+ return warn(`SDK cli.js not readable at ${cliPath}, skipping`);
98
+ }
99
+ if (content.includes(added[0])) return; // already patched (reinstall/idempotent)
100
+ if (!content.includes(removed[0])) {
101
+ return warn('SDK cli.js does not match the patch context, skipping');
102
+ }
103
+ writeFileSync(cliPath, content.replace(removed[0], added[0]), 'utf8');
104
+ console.log(`open-walnut postinstall: patched claude-agent-sdk@${installedVersion} (reminder disabled)`);
105
+ }
106
+
107
+ try {
108
+ applySdkPatch();
109
+ } catch (err) {
110
+ warn(`SDK patch failed (continuing): ${err instanceof Error ? err.message : err}`);
111
+ }
112
+
113
+ // patch-qmd resolves its own targets (including hoisted installs) and is
114
+ // idempotent; isolate its strict throws so install never fails on it.
115
+ const qmd = spawnSync(process.execPath, [join(root, 'scripts', 'patch-qmd.mjs')], {
116
+ stdio: 'inherit',
117
+ cwd: root,
118
+ });
119
+ if (qmd.status !== 0) warn('qmd search patch failed (continuing — search recall slightly degraded)');
120
+ process.exit(0);