claude-mem 13.27.0 → 13.27.1

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 (29) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/npx-cli/index.js +1 -1
  4. package/openclaw/openclaw.plugin.json +1 -1
  5. package/openclaw/skills/agent-cost-report/CHECKSUMS.txt +6 -6
  6. package/openclaw/skills/agent-cost-report/SKILL.md +1 -0
  7. package/openclaw/skills/agent-cost-report/scripts/acr/render.py +5 -1
  8. package/openclaw/skills/agent-cost-report/scripts/acr/rollup.py +3 -3
  9. package/openclaw/skills/agent-cost-report/scripts/acr/wins.py +64 -4
  10. package/openclaw/skills/agent-cost-report/scripts/acr.py +2 -1
  11. package/openclaw/skills/agent-cost-report/scripts/tests/test_wins.py +44 -2
  12. package/package.json +1 -1
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/.codex-plugin/plugin.json +1 -1
  15. package/plugin/package.json +1 -1
  16. package/plugin/scripts/context-generator.cjs +17 -15
  17. package/plugin/scripts/context-generator.cjs.map +2 -2
  18. package/plugin/scripts/mcp-server.cjs +1 -1
  19. package/plugin/scripts/server-service.cjs +2 -2
  20. package/plugin/scripts/transcript-watcher.cjs +1 -1
  21. package/plugin/scripts/worker-service.cjs +214 -212
  22. package/plugin/scripts/worker-service.cjs.map +2 -2
  23. package/plugin/skills/agent-cost-report/CHECKSUMS.txt +6 -6
  24. package/plugin/skills/agent-cost-report/SKILL.md +1 -0
  25. package/plugin/skills/agent-cost-report/scripts/acr/render.py +5 -1
  26. package/plugin/skills/agent-cost-report/scripts/acr/rollup.py +3 -3
  27. package/plugin/skills/agent-cost-report/scripts/acr/wins.py +64 -4
  28. package/plugin/skills/agent-cost-report/scripts/acr.py +2 -1
  29. package/plugin/skills/agent-cost-report/scripts/tests/test_wins.py +44 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.27.0",
3
+ "version": "13.27.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman",
package/README.md CHANGED
@@ -59,7 +59,7 @@
59
59
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
60
60
  </a>
61
61
  <a href="package.json">
62
- <img src="https://img.shields.io/badge/version-13.26.1-green.svg" alt="Version">
62
+ <img src="https://img.shields.io/badge/version-13.27.1-green.svg" alt="Version">
63
63
  </a>
64
64
  <a href="package.json">
65
65
  <img src="https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg" alt="Node">
@@ -100,7 +100,7 @@ ${o.stack}`:` ${o.message}`;else if(this.getLevel()===0&&typeof o=="object")try{
100
100
  ${m.stack??""}
101
101
  `)}else $n(f+`
102
102
  `)}debug(e,r,i,n){this.log(0,e,r,i,n)}info(e,r,i,n){this.log(1,e,r,i,n)}warn(e,r,i,n){this.log(2,e,r,i,n)}setErrorSink(e){qS=e}error(e,r,i,n){this.log(3,e,r,i,n),this.routeErrorToSink(r,i,n)}routeErrorToSink(e,r,i){try{if(!qS||!(i instanceof Error))return;qS(i)}catch{}}dataIn(e,r,i,n){this.info(e,`\u2192 ${r}`,i,n)}dataOut(e,r,i,n){this.info(e,`\u2190 ${r}`,i,n)}success(e,r,i,n){this.info(e,`\u2713 ${r}`,i,n)}failure(e,r,i,n){this.error(e,`\u2717 ${r}`,i,n)}},w=new BS});import{join as UP}from"path";import{mkdirSync as N8,writeFileSync as L8}from"fs";import{randomUUID as $8}from"crypto";function U8(t){let e=t.DO_NOT_TRACK;return e===void 0||e===""?!1:e!=="0"&&e!=="false"}function VS(t,e){if(U8(t))return{enabled:!1,source:"DO_NOT_TRACK"};let r=t.CLAUDE_MEM_TELEMETRY?.toLowerCase();return r==="0"||r==="false"||r==="off"?{enabled:!1,source:"env"}:r==="1"||r==="true"||r==="on"?{enabled:!0,source:"env"}:e?.enabled===!0?{enabled:!0,source:"config"}:e?.enabled===!1?{enabled:!1,source:"config"}:{enabled:!0,source:"default"}}function JP(t,e){return VS(t,e).enabled}function eu(){return UP(Un(),FP)}function po(){let t;try{t=lr(eu(),null)}catch(e){let r=e instanceof Error?e:new Error(String(e));return w.warn("SYSTEM","Telemetry: corrupt telemetry.json; treating as no recorded consent",void 0,r),null}return!t||typeof t!="object"||typeof t.installId!="string"||t.enabled!==void 0&&typeof t.enabled!="boolean"?null:{enabled:t.enabled,installId:t.installId,decidedAt:typeof t.decidedAt=="string"?t.decidedAt:""}}function Fa(t){let e=Un();N8(e,{recursive:!0}),L8(UP(e,FP),JSON.stringify(t,null,2)+`
103
- `)}function tf(){let t=po();if(t?.installId)return t.installId;let e=$8();return Fa({installId:e,decidedAt:""}),e}var FP,rf=E(()=>{"use strict";He();ls();Oe();FP="telemetry.json"});import F8 from"os";function J8(t){try{if(typeof t!="string"){if(t==null)return"";try{t=String(t)}catch(r){let i=r instanceof Error?r:new Error(String(r));return w.warn("SYSTEM","error-scrub: String() coercion of non-string input failed",void 0,i),""}}let e=t;return e.length>zP?e.slice(0,zP):e}catch{return""}}function z8(t){if(typeof t!="string"||t.length===0)return t??"";let e="";try{e=F8.homedir()||""}catch(i){let n=i instanceof Error?i:new Error(String(i));w.warn("SYSTEM","error-scrub: os.homedir() failed; skipping home-dir redaction",void 0,n),e=""}if(!e)return t;let r=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");try{return t.replace(new RegExp(r,"g"),"~")}catch(i){let n=i instanceof Error?i:new Error(String(i));return w.warn("SYSTEM","error-scrub: home-dir RegExp replace failed; using split/join fallback",void 0,n),t.split(e).join("~")}}function q8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/(?<![~\w])\/(?:[^\s/:"'()]+\/)+([^\s/:"'()]+)/g,"$1").replace(/[A-Za-z]:\\(?:[^\s\\:"'()]+\\)*([^\s\\:"'()]+)/g,"$1").replace(/\\\\[^\s\\:"'()]+(?:\\[^\s\\:"'()]+)*\\([^\s\\:"'()]+)/g,"$1")}function G8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'()]+/g,e=>/^https?:\/\/hooks\.slack\.com\/services\//i.test(e)?e.replace(/^(https?:\/\/hooks\.slack\.com\/services\/)[^\s"'()]+/i,`$1${mn}`):e.replace(/[?#].*$/,"").replace(/^([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+@/,`$1${mn}@`))}function B8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\?[^\s"'()]*/g,"")}function V8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\b(?:access[_-]?token|api[_-]?key|auth(?:orization)?|secret|password|passwd|jwt|session[_-]?id|token)=[^\s"'&]+/gi,e=>{let r=e.indexOf("=");return`${e.slice(0,r+1)}${mn}`})}function W8(t){if(typeof t!="string"||t.length===0)return t??"";let e=t;return e=e.replace(/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g,mn),e=e.replace(/\b[A-Za-z0-9_-]{10,512}\.[A-Za-z0-9_-]{10,512}\.[A-Za-z0-9_-]{10,512}\b/g,mn),e=e.replace(/\b(?:sk|pk|rk|ak|phc|phx|ph|ghp|gho|ghs|xox[bpasr])[-_][A-Za-z0-9_-]{8,512}\b/gi,mn),e=e.replace(/\bBearer\s+[A-Za-z0-9._-]{8,512}\b/gi,mn),e=e.replace(/\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA)[0-9A-Z]{16}\b/g,mn),e=e.replace(/\b[0-9a-fA-F]{24,4096}\b/g,mn),e=e.replace(/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,mn),e=e.replace(/\b(?=[A-Za-z0-9+/_-]{0,4096}\d)[A-Za-z0-9+/_-]{32,4096}={0,2}\b/g,mn),e=e.replace(/\b(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}\b/g,mn),e}function qP(t){let e=J8(t);return e.length===0?"":(e=z8(e),e=G8(e),e=q8(e),e=B8(e),e=V8(e),e=W8(e),e)}var zP,mn,GP=E(()=>{"use strict";Oe();zP=8192,mn="[REDACTED]"});function Z8(t,e){if(!(!t||typeof t!="object"))for(let r of Object.keys(t)){if(!H8.has(r))continue;let i=t[r];if(typeof i=="string"){let n=i.length>BP?i.slice(0,BP):i;e[r]=qP(n)}else(typeof i=="number"&&Number.isFinite(i)||typeof i=="boolean")&&(e[r]=i)}}function VP(t){let e={};try{Z8(t,e)}catch{}return e}var H8,BP,WP=E(()=>{"use strict";GP();H8=new Set(["version","os","os_version","is_wsl","arch","runtime","runtime_version","node_version","duration_ms","outcome","error_category","locale","is_ci","endpoint","ide","provider","runtime_mode","trigger","count","has_summary","is_update","stage","install_method","interactive","bun_version","uv_version","claude_code_version","trial_days","trial_variant","offer_surface","funnel_source","observation_count","session_count","timeline_depth_days","has_session_summary","obs_type_bugfix","obs_type_discovery","obs_type_decision","obs_type_refactor","obs_type_other","tokens_injected","tokens_saved_vs_naive","mode","search_strategy","observation_type","hook","compression_ms","tokens_input","tokens_output","compression_ratio","model","cost_usd","endpoint_class","db_observation_count","db_session_count","db_summary_count","db_project_count","db_size_mb","install_age_days","obs_count_7d","obs_count_30d","days_since_last_obs","result_count","chroma_available","fallback_reason","invalid_output_class","consecutive_invalid_outputs","respawn_triggered","abort_reason","previous_shutdown","previous_uptime_seconds","uptime_seconds","shutdown_reason","process_rss_mb","heap_used_mb","hook_type","error_mode","consecutive_failures","threshold_tripped","limit_window","overage_status","is_using_overage","resets_in_minutes","discovery_tokens","read_tokens","summary_count","prompt_count","project_count","backfilled","first_active_date","session_completed_count","session_failed_count","sessions_claude_count","sessions_codex_count","sessions_gemini_count","sessions_other_platform_count","subagent_obs_count","total_tokens_input","total_tokens_output","total_cost_usd","avg_duration_ms","avg_compression_ms","outcomes_ok","outcomes_error","outcomes_aborted","outcomes_invalid_output","top_model","observed_model","observed_billing","window_start_ts","rollup_reason","window_seq","total_tokens","avg_tokens","skill_id","skill_source","skill_trigger","observations_created","total_observations_injected","total_tokens_saved_vs_naive"]),BP=200});import HP from"os";function ZP(){return process.env.CLAUDE_MEM_TELEMETRY_KEY||X8}function YP(){return process.env.CLAUDE_MEM_TELEMETRY_HOST||Q8}function XP(t){let e={};for(let r of eX)t[r]!==void 0&&(e[r]=t[r]);return e}function tX(){if(process.platform!=="linux")return!1;try{return!!process.env.WSL_DISTRO_NAME||HP.release().toLowerCase().includes("microsoft")}catch(t){let e=t instanceof Error?t:new Error(String(t));return w.warn("SYSTEM","Telemetry: WSL detection failed; reporting is_wsl=false",void 0,e),!1}}function QP(){return{version:Y8,os:process.platform,os_version:HP.release(),is_wsl:tX(),arch:process.arch,runtime:process.versions.bun?"bun":"node",runtime_version:process.versions.bun??process.versions.node,node_version:process.versions.node,is_ci:!!process.env.CI,locale:Intl.DateTimeFormat().resolvedOptions().locale}}var Y8,X8,Q8,eX,eM=E(()=>{"use strict";Oe();Y8="13.27.0",X8="phc_BKJAeNbpj932N9qEiU6qhutZEiu6LLfRpXfTbLM9MLaG",Q8="https://us.i.posthog.com";eX=["version","os","os_version","is_wsl","arch","runtime","locale","ide","provider","runtime_mode","install_method","claude_code_version","first_active_date","db_observation_count","db_session_count","db_summary_count","db_project_count","db_size_mb","install_age_days","obs_count_7d","obs_count_30d","days_since_last_obs"]});async function oi(t,e,r){try{if(!JP(process.env,po()))return;let i=VP({...QP(),...e??{}});if(r?.person?i.$set=XP(i):i.$process_person_profile=!1,process.env.CLAUDE_MEM_TELEMETRY_DEBUG==="1"){process.stderr.write("[telemetry] "+JSON.stringify({event:t,properties:i})+`
103
+ `)}function tf(){let t=po();if(t?.installId)return t.installId;let e=$8();return Fa({installId:e,decidedAt:""}),e}var FP,rf=E(()=>{"use strict";He();ls();Oe();FP="telemetry.json"});import F8 from"os";function J8(t){try{if(typeof t!="string"){if(t==null)return"";try{t=String(t)}catch(r){let i=r instanceof Error?r:new Error(String(r));return w.warn("SYSTEM","error-scrub: String() coercion of non-string input failed",void 0,i),""}}let e=t;return e.length>zP?e.slice(0,zP):e}catch{return""}}function z8(t){if(typeof t!="string"||t.length===0)return t??"";let e="";try{e=F8.homedir()||""}catch(i){let n=i instanceof Error?i:new Error(String(i));w.warn("SYSTEM","error-scrub: os.homedir() failed; skipping home-dir redaction",void 0,n),e=""}if(!e)return t;let r=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");try{return t.replace(new RegExp(r,"g"),"~")}catch(i){let n=i instanceof Error?i:new Error(String(i));return w.warn("SYSTEM","error-scrub: home-dir RegExp replace failed; using split/join fallback",void 0,n),t.split(e).join("~")}}function q8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/(?<![~\w])\/(?:[^\s/:"'()]+\/)+([^\s/:"'()]+)/g,"$1").replace(/[A-Za-z]:\\(?:[^\s\\:"'()]+\\)*([^\s\\:"'()]+)/g,"$1").replace(/\\\\[^\s\\:"'()]+(?:\\[^\s\\:"'()]+)*\\([^\s\\:"'()]+)/g,"$1")}function G8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'()]+/g,e=>/^https?:\/\/hooks\.slack\.com\/services\//i.test(e)?e.replace(/^(https?:\/\/hooks\.slack\.com\/services\/)[^\s"'()]+/i,`$1${mn}`):e.replace(/[?#].*$/,"").replace(/^([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+@/,`$1${mn}@`))}function B8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\?[^\s"'()]*/g,"")}function V8(t){return typeof t!="string"||t.length===0?t??"":t.replace(/\b(?:access[_-]?token|api[_-]?key|auth(?:orization)?|secret|password|passwd|jwt|session[_-]?id|token)=[^\s"'&]+/gi,e=>{let r=e.indexOf("=");return`${e.slice(0,r+1)}${mn}`})}function W8(t){if(typeof t!="string"||t.length===0)return t??"";let e=t;return e=e.replace(/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g,mn),e=e.replace(/\b[A-Za-z0-9_-]{10,512}\.[A-Za-z0-9_-]{10,512}\.[A-Za-z0-9_-]{10,512}\b/g,mn),e=e.replace(/\b(?:sk|pk|rk|ak|phc|phx|ph|ghp|gho|ghs|xox[bpasr])[-_][A-Za-z0-9_-]{8,512}\b/gi,mn),e=e.replace(/\bBearer\s+[A-Za-z0-9._-]{8,512}\b/gi,mn),e=e.replace(/\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA)[0-9A-Z]{16}\b/g,mn),e=e.replace(/\b[0-9a-fA-F]{24,4096}\b/g,mn),e=e.replace(/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,mn),e=e.replace(/\b(?=[A-Za-z0-9+/_-]{0,4096}\d)[A-Za-z0-9+/_-]{32,4096}={0,2}\b/g,mn),e=e.replace(/\b(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}\b/g,mn),e}function qP(t){let e=J8(t);return e.length===0?"":(e=z8(e),e=G8(e),e=q8(e),e=B8(e),e=V8(e),e=W8(e),e)}var zP,mn,GP=E(()=>{"use strict";Oe();zP=8192,mn="[REDACTED]"});function Z8(t,e){if(!(!t||typeof t!="object"))for(let r of Object.keys(t)){if(!H8.has(r))continue;let i=t[r];if(typeof i=="string"){let n=i.length>BP?i.slice(0,BP):i;e[r]=qP(n)}else(typeof i=="number"&&Number.isFinite(i)||typeof i=="boolean")&&(e[r]=i)}}function VP(t){let e={};try{Z8(t,e)}catch{}return e}var H8,BP,WP=E(()=>{"use strict";GP();H8=new Set(["version","os","os_version","is_wsl","arch","runtime","runtime_version","node_version","duration_ms","outcome","error_category","locale","is_ci","endpoint","ide","provider","runtime_mode","trigger","count","has_summary","is_update","stage","install_method","interactive","bun_version","uv_version","claude_code_version","trial_days","trial_variant","offer_surface","funnel_source","observation_count","session_count","timeline_depth_days","has_session_summary","obs_type_bugfix","obs_type_discovery","obs_type_decision","obs_type_refactor","obs_type_other","tokens_injected","tokens_saved_vs_naive","mode","search_strategy","observation_type","hook","compression_ms","tokens_input","tokens_output","compression_ratio","model","cost_usd","endpoint_class","db_observation_count","db_session_count","db_summary_count","db_project_count","db_size_mb","install_age_days","obs_count_7d","obs_count_30d","days_since_last_obs","result_count","chroma_available","fallback_reason","invalid_output_class","consecutive_invalid_outputs","respawn_triggered","abort_reason","previous_shutdown","previous_uptime_seconds","uptime_seconds","shutdown_reason","process_rss_mb","heap_used_mb","hook_type","error_mode","consecutive_failures","threshold_tripped","limit_window","overage_status","is_using_overage","resets_in_minutes","discovery_tokens","read_tokens","summary_count","prompt_count","project_count","backfilled","first_active_date","session_completed_count","session_failed_count","sessions_claude_count","sessions_codex_count","sessions_gemini_count","sessions_other_platform_count","subagent_obs_count","total_tokens_input","total_tokens_output","total_cost_usd","avg_duration_ms","avg_compression_ms","outcomes_ok","outcomes_error","outcomes_aborted","outcomes_invalid_output","top_model","observed_model","observed_billing","window_start_ts","rollup_reason","window_seq","total_tokens","avg_tokens","skill_id","skill_source","skill_trigger","observations_created","total_observations_injected","total_tokens_saved_vs_naive"]),BP=200});import HP from"os";function ZP(){return process.env.CLAUDE_MEM_TELEMETRY_KEY||X8}function YP(){return process.env.CLAUDE_MEM_TELEMETRY_HOST||Q8}function XP(t){let e={};for(let r of eX)t[r]!==void 0&&(e[r]=t[r]);return e}function tX(){if(process.platform!=="linux")return!1;try{return!!process.env.WSL_DISTRO_NAME||HP.release().toLowerCase().includes("microsoft")}catch(t){let e=t instanceof Error?t:new Error(String(t));return w.warn("SYSTEM","Telemetry: WSL detection failed; reporting is_wsl=false",void 0,e),!1}}function QP(){return{version:Y8,os:process.platform,os_version:HP.release(),is_wsl:tX(),arch:process.arch,runtime:process.versions.bun?"bun":"node",runtime_version:process.versions.bun??process.versions.node,node_version:process.versions.node,is_ci:!!process.env.CI,locale:Intl.DateTimeFormat().resolvedOptions().locale}}var Y8,X8,Q8,eX,eM=E(()=>{"use strict";Oe();Y8="13.27.1",X8="phc_BKJAeNbpj932N9qEiU6qhutZEiu6LLfRpXfTbLM9MLaG",Q8="https://us.i.posthog.com";eX=["version","os","os_version","is_wsl","arch","runtime","locale","ide","provider","runtime_mode","install_method","claude_code_version","first_active_date","db_observation_count","db_session_count","db_summary_count","db_project_count","db_size_mb","install_age_days","obs_count_7d","obs_count_30d","days_since_last_obs"]});async function oi(t,e,r){try{if(!JP(process.env,po()))return;let i=VP({...QP(),...e??{}});if(r?.person?i.$set=XP(i):i.$process_person_profile=!1,process.env.CLAUDE_MEM_TELEMETRY_DEBUG==="1"){process.stderr.write("[telemetry] "+JSON.stringify({event:t,properties:i})+`
104
104
  `);return}let n=ZP(),o=new AbortController,s=setTimeout(()=>o.abort(),rX);try{await nX(n,t,i,o.signal)}finally{clearTimeout(s)}}catch{}}async function nX(t,e,r,i){await fetch(`${YP()}/capture/`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({api_key:t,event:e,distinct_id:tf(),properties:r}),signal:i})}var rX,nf=E(()=>{"use strict";rf();WP();eM();rX=2e3});import{spawn as iX,spawnSync as tM}from"node:child_process";import{dirname as oX,extname as tu,join as sX}from"node:path";function si(t,e,r){return iX(t,e??[],{windowsHide:!0,...r})}function lX(t){return`"${t.replace(/"/g,'""')}"`}function ZS(t){if(process.platform!=="win32")return[];try{let e=tM("where",[t],{encoding:"utf-8",stdio:["ignore","pipe","ignore"],windowsHide:!0});return e.status!==0||!e.stdout.trim()?[]:e.stdout.split(/\r?\n/).map(r=>r.trim()).filter(Boolean)}catch{return[]}}function dX(t,e){let r=t.find(n=>WS.has(tu(n).toLowerCase()));if(r)return r;let i=t.find(n=>HS.has(tu(n).toLowerCase()));if(i&&e){let n=e(i);if(n&&WS.has(tu(n).toLowerCase()))return n}return i??t.find(n=>aX.has(tu(n).toLowerCase()))??t[0]??null}function pX(t,e,r=uX){if(!/[\\/]volta[\\/]bin[\\/]/i.test(e))return null;try{let i=r(sX(oX(e),"volta.exe"),["which",t],{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:cX,windowsHide:!0});return i.status!==0||!i.stdout.trim()?null:i.stdout.split(/\r?\n/).map(n=>n.trim()).find(Boolean)??null}catch{return null}}function Ja(t){return dX(ZS(t),e=>pX(t,e))}function za(t,e,r,i=process.platform){let n={...i==="win32"?{windowsHide:!0}:{},...r};if(i==="win32"&&HS.has(tu(t).toLowerCase())){let o=[t,...e].map(lX).join(" ");return{command:process.env.ComSpec??"cmd.exe",args:["/d","/s","/c",`"${o}"`],options:{...n,windowsVerbatimArguments:!0}}}return{command:t,args:[...e],options:n}}var HS,WS,aX,cX,uX,ai=E(()=>{"use strict";HS=new Set([".cmd",".bat"]),WS=new Set([".exe",".com"]),aX=new Set([...WS,...HS]);cX=5e3,uX=(t,e,r)=>tM(t,e,r)});function qa(t){return process.platform==="win32"?Math.round(t*qr.WINDOWS_MULTIPLIER):t}var qr,of=E(()=>{"use strict";qr={HEALTH_CHECK:3e3,API_REQUEST:3e4,HOOK_READINESS_WAIT:1e4,POST_SPAWN_WAIT:15e3,READINESS_WAIT:3e4,PORT_IN_USE_WAIT:3e3,POWERSHELL_COMMAND:1e4,WINDOWS_MULTIPLIER:1.5}});var iM={};De(iM,{SettingsDefaultsManager:()=>Le});import{readFileSync as fX,existsSync as mX}from"fs";import{join as rM}from"path";import{homedir as nM,hostname as hX}from"os";function vX(t){if(typeof t!="string")return null;let e=t.trim();if(e.length===0)return null;try{if(new URL(e).hostname===gX)return bX}catch{return null}return null}var yX,gX,bX,Le,Dr=E(()=>{"use strict";of();Rr();yX="security_alert",gX="sync-hub.black-pond-afbb.workers.dev",bX="https://sync.cmem.ai";Le=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5-20251001",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:String(37700+(process.getuid?.()??77)%100),CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_API_TIMEOUT_MS:String(qa(qr.API_REQUEST)),CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_CLAUDE_AUTH_METHOD:"subscription",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-flash-latest",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_BASE_URL:"",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:rM(nM(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_CLAUDE_CONFIG_DIR:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"false",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"false",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"false",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:"",CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:"",CLAUDE_MEM_CONTEXT_FULL_COUNT:"0",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false",CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT:"true",CLAUDE_MEM_WELCOME_HINT_ENABLED:"true",CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED:"false",CLAUDE_MEM_FOLDER_USE_LOCAL_MD:"false",CLAUDE_MEM_TRANSCRIPTS_ENABLED:"true",CLAUDE_MEM_TRANSCRIPTS_CONFIG_PATH:rM(nM(),".claude-mem","transcript-watch.json"),CLAUDE_MEM_CODEX_TRANSCRIPT_INGESTION:"false",CLAUDE_MEM_MAX_CONCURRENT_AGENTS:"2",CLAUDE_MEM_OBSERVER_MAX_CONVERSATION_CHARS:"400000",CLAUDE_MEM_HOOK_FAIL_LOUD_THRESHOLD:"3",CLAUDE_MEM_EXCLUDED_PROJECTS:"",CLAUDE_MEM_FOLDER_MD_EXCLUDE:"[]",CLAUDE_MEM_FOLDER_MD_SKELETON_DENYLIST:"[]",CLAUDE_MEM_SEMANTIC_INJECT:"false",CLAUDE_MEM_SEMANTIC_INJECT_LIMIT:"5",CLAUDE_MEM_TIER_ROUTING_ENABLED:"true",CLAUDE_MEM_TIER_SIMPLE_MODEL:"haiku",CLAUDE_MEM_TIER_SUMMARY_MODEL:"",CLAUDE_MEM_TIER_FAST_MODEL:"haiku",CLAUDE_MEM_TIER_SMART_MODEL:"sonnet",CLAUDE_MEM_CHROMA_ENABLED:"true",CLAUDE_MEM_CHROMA_MODE:"local",CLAUDE_MEM_CHROMA_HOST:"127.0.0.1",CLAUDE_MEM_CHROMA_PORT:"8000",CLAUDE_MEM_CHROMA_SSL:"false",CLAUDE_MEM_CHROMA_API_KEY:"",CLAUDE_MEM_CHROMA_TENANT:"default_tenant",CLAUDE_MEM_CHROMA_DATABASE:"default_database",CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS:"120000",CLAUDE_MEM_CHROMA_MAX_PENDING_MUTATIONS:"5000",CLAUDE_MEM_CLOUD_SYNC_TOKEN:"",CLAUDE_MEM_CLOUD_SYNC_USER_ID:"",CLAUDE_MEM_CLOUD_SYNC_HUB_URL:"",CLAUDE_MEM_CLOUD_SYNC_DEVICE_ID:"",CLAUDE_MEM_CLOUD_SYNC_DEVICE_NAME:hX(),CLAUDE_MEM_CLOUD_SYNC_WS:"true",CLAUDE_MEM_CLOUD_SYNC_CONTENT_BATCH_SIZE:"40",CLAUDE_MEM_CLOUD_SYNC_REQUEST_TIMEOUT_MS:"90000",CLAUDE_MEM_LLM_TIMEOUT_MS:"30000",CLAUDE_MEM_TV_TOKEN:"",CLAUDE_MEM_PRO_TRIAL_EMAIL:"",CLAUDE_MEM_PRO_TRIAL_AT:"",CLAUDE_MEM_PRO_TRIAL_STATE:"",CLAUDE_MEM_PRO_TRIAL_ENDS_AT:"",CLAUDE_MEM_PRO_PLAN:"",CLAUDE_MEM_PRO_FALLBACK_AT:"",CLAUDE_MEM_PRO_MEMORY_KEY:"",CLAUDE_MEM_PRO_MEMORY_BASE_URL:"",CLAUDE_MEM_PRO_MEMORY_MODEL:"",CLAUDE_MEM_TELEGRAM_ENABLED:"true",CLAUDE_MEM_TELEGRAM_BOT_TOKEN:"",CLAUDE_MEM_TELEGRAM_CHAT_ID:"",CLAUDE_MEM_TELEGRAM_WRAPUPS_ENABLED:"true",CLAUDE_MEM_TELEGRAM_OBSERVATION_ALERTS_ENABLED:"false",CLAUDE_MEM_TELEGRAM_WRAPUP_ROUTES:"{}",CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES:"security_alert,sensitive",CLAUDE_MEM_TELEGRAM_TRIGGER_CONCEPTS:"",CLAUDE_MEM_GROK_BOT_AWARENESS_ENABLED:"true",CLAUDE_MEM_GROK_BOT_AWARENESS_AGENT_IDS:"521e962d-2ec3-4488-bfbc-54d5209ce118,95601360-61f7-4fd9-bb3a-2c976b2b85c0",CLAUDE_MEM_GROK_BOT_AWARENESS_TRIGGER_TYPES:"decision,bugfix,security_alert,sensitive",CLAUDE_MEM_GROK_BOT_AWARENESS_TRIGGER_CONCEPTS:"",CLAUDE_MEM_GROK_BOT_INJECT_ENABLED:"true",CLAUDE_MEM_GROK_BOT_INJECT_AGENT_IDS:"*",CLAUDE_MEM_GROK_BOT_INJECT_TIER:"episode",CLAUDE_MEM_GROK_BOT_INJECT_WINDOW:"80",CLAUDE_MEM_GROK_BOT_INJECT_FALLBACK:"house",CLAUDE_MEM_GROK_BOT_INJECT_PLATFORM_SOURCE:"",CLAUDE_MEM_GROK_BOT_INJECT_PROJECTS_BY_AGENT:"",CLAUDE_MEM_GROK_BOT_INJECT_MAX_LINE_CHARS:"160",CLAUDE_MEM_GROK_BOT_INJECT_DEBOUNCE_MS:"1500",CLAUDE_MEM_GROK_BOT_INJECT_STANDING_LINE:"",CLAUDE_MEM_CCS_ALIGN_ENABLED:"true",CLAUDE_MEM_CCS_ALIGN_VIEWER_IDS:"ccs-align",CLAUDE_MEM_CCS_ALIGN_TRIGGER_TYPES:"decision,bugfix,security_alert,sensitive",CLAUDE_MEM_CCS_ALIGN_PATCH_SHADOWS:"false",CLAUDE_MEM_QUEUE_ENGINE:"sqlite",CLAUDE_MEM_REDIS_URL:"",CLAUDE_MEM_REDIS_HOST:"127.0.0.1",CLAUDE_MEM_REDIS_PORT:"6379",CLAUDE_MEM_REDIS_MODE:"external",CLAUDE_MEM_QUEUE_REDIS_PREFIX:`claude_mem_${process.env.CLAUDE_MEM_WORKER_PORT??String(37700+(process.getuid?.()??77)%100)}`,CLAUDE_MEM_AUTH_MODE:"api-key",CLAUDE_MEM_RUNTIME:"worker",CLAUDE_MEM_SERVER_URL:`http://127.0.0.1:${process.env.CLAUDE_MEM_SERVER_PORT??String(37877+(process.getuid?.()??77)%100)}`,CLAUDE_MEM_SERVER_API_KEY:"",CLAUDE_MEM_SERVER_PROJECT_ID:"",CLAUDE_MEM_SERVER_BETA_URL:`http://127.0.0.1:${process.env.CLAUDE_MEM_SERVER_PORT??String(37877+(process.getuid?.()??77)%100)}`,CLAUDE_MEM_SERVER_BETA_API_KEY:"",CLAUDE_MEM_SERVER_BETA_PROJECT_ID:""};static getAllDefaults(){return{...this.DEFAULTS}}static get(e){let r=process.env[e]??this.DEFAULTS[e];return e==="CLAUDE_MEM_WORKER_HOST"?this.normalizeWorkerHost(r):r}static normalizeWorkerHost(e){return e==="localhost"?"127.0.0.1":e}static finalizeSettings(e,r){let i=r?this.applyEnvOverrides(e):e;return i.CLAUDE_MEM_WORKER_HOST=this.normalizeWorkerHost(i.CLAUDE_MEM_WORKER_HOST),i}static getInt(e){let r=this.get(e);return parseInt(r,10)}static applyEnvOverrides(e){let r={...e};for(let i of Object.keys(this.DEFAULTS))process.env[i]!==void 0&&(r[i]=process.env[i]);return r}static loadFromFile(e,r=!0){try{if(!mX(e)){let u=this.getAllDefaults();try{lt(e,u),console.warn("[SETTINGS] Created settings file with defaults:",e)}catch(d){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",e,d instanceof Error?d.message:String(d))}return this.finalizeSettings(u,r)}let i=fX(e,"utf-8"),n=Et(i),o=n,s=n.env&&typeof n.env=="object"&&!Array.isArray(n.env),a=s&&Object.keys(n).some(u=>u!=="env");if(s&&(o=n.env,!a))try{lt(e,o),console.warn("[SETTINGS] Migrated settings file from nested to flat schema:",e)}catch(u){console.warn("[SETTINGS] Failed to auto-migrate settings file:",e,u instanceof Error?u.message:String(u))}if(o.CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES===yX){o={...o,CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES:this.DEFAULTS.CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES};try{lt(e,a?{...n,env:o}:o),console.warn("[SETTINGS] Migrated Telegram trigger types off the legacy default:",e)}catch(u){console.warn("[SETTINGS] Failed to migrate Telegram trigger types:",e,u instanceof Error?u.message:String(u))}}let l=vX(o.CLAUDE_MEM_CLOUD_SYNC_HUB_URL);if(l!==null){o={...o,CLAUDE_MEM_CLOUD_SYNC_HUB_URL:l};try{lt(e,a?{...n,env:o}:o),console.warn("[SETTINGS] Migrated cloud sync hub URL off the legacy workers.dev host:",e)}catch(u){console.warn("[SETTINGS] Failed to migrate cloud sync hub URL:",e,u instanceof Error?u.message:String(u))}}let c={...this.DEFAULTS};for(let u of Object.keys(this.DEFAULTS))o[u]!==void 0&&(c[u]=o[u]);return this.finalizeSettings(c,r)}catch(i){console.warn("[SETTINGS] Failed to load settings, using defaults:",e,i instanceof Error?i.message:String(i));let n=this.getAllDefaults();return this.finalizeSettings(n,r)}}}});import{execFile as SX}from"child_process";import{promisify as EX}from"util";import{createHash as LIe}from"crypto";import{existsSync as UIe,readFileSync as FIe,writeFileSync as JIe,mkdirSync as zIe,unlinkSync as qIe}from"fs";import{userInfo as BIe}from"os";import{join as WIe}from"path";var XIe,oM=E(()=>{"use strict";He();Dr();Oe();XIe=EX(SX)});import{existsSync as YS,readFileSync as aM,writeFileSync as wX,mkdirSync as _X,chmodSync as sM}from"fs";import{parseEnv as kX}from"util";import{basename as axe}from"path";function lM(){return process.env.CLAUDE_MEM_ENV_FILE??Be.envFile()}function uM(t){return kX(t)}function IX(t){let e=["# claude-mem credentials","# This file stores keys and gateway settings for the claude-mem memory agent","# Edit this file or use claude-mem settings to configure",""];for(let[r,i]of Object.entries(t))if(i){let n=/[\s#=]/.test(i);e.push(`${r}=${n?`"${i}"`:i}`)}return e.join(`
105
105
  `)+`
106
106
  `}function dM(){let t=lM();if(!YS(t))return{};try{let e=aM(t,"utf-8"),r=uM(e),i={};for(let n of cM)r[n]&&(i[n]=r[n]);return i}catch(e){return w.warn("ENV","Failed to load .env file",{path:t},e instanceof Error?e:new Error(String(e))),{}}}function pM(t){let e=lM(),r={};try{YS(Be.dataDir())||_X(Be.dataDir(),{recursive:!0,mode:448}),sM(Be.dataDir(),448),r=YS(e)?uM(aM(e,"utf-8")):{}}catch(n){let o=n instanceof Error?n:new Error(String(n));throw w.error("ENV","Failed to set up env directory or read existing env",{},o),o}let i={...r};for(let n of cM){let o=t[n];o!==void 0&&(o?i[n]=o:delete i[n])}try{wX(e,IX(i),{encoding:"utf-8",mode:384}),sM(e,384)}catch(n){throw w.error("ENV","Failed to save .env file",{path:e},n instanceof Error?n:new Error(String(n))),n}}var cM,fM=E(()=>{"use strict";Oe();He();Dr();oM();cM=["ANTHROPIC_API_KEY","ANTHROPIC_BASE_URL","ANTHROPIC_AUTH_TOKEN","GEMINI_API_KEY","OPENROUTER_API_KEY"]});function Fn(t=process.env){let e={};for(let[r,i]of Object.entries(t))if(i!==void 0){if(AX.has(r)){e[r]=i;continue}TX.has(r)||xX.some(n=>r.startsWith(n))||(e[r]=i)}return e}var xX,TX,CX,AX,ru=E(()=>{"use strict";xX=["CLAUDECODE_","CLAUDE_CODE_"],TX=new Set(["CLAUDECODE","CLAUDE_CODE_SESSION","CLAUDE_CODE_ENTRYPOINT","MCP_SESSION_ID"]),CX=new Set(["HTTP_PROXY","HTTPS_PROXY","ALL_PROXY","NO_PROXY","http_proxy","https_proxy","all_proxy","no_proxy","npm_config_proxy","npm_config_https_proxy","npm_config_noproxy"]),AX=new Set(["CLAUDE_CODE_OAUTH_TOKEN","CLAUDE_CODE_GIT_BASH_PATH","CLAUDE_CODE_USE_BEDROCK","CLAUDE_CODE_USE_VERTEX","CLAUDE_CODE_USE_FOUNDRY","CLAUDE_CODE_SKIP_BEDROCK_AUTH","CLAUDE_CODE_SKIP_VERTEX_AUTH","CLAUDE_CODE_SKIP_FOUNDRY_AUTH","ANTHROPIC_BEDROCK_BASE_URL","AWS_REGION","AWS_PROFILE","AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_SESSION_TOKEN","ANTHROPIC_VERTEX_PROJECT_ID","CLOUD_ML_REGION","GOOGLE_APPLICATION_CREDENTIALS",...CX])});import{spawnSync as hM}from"child_process";import{readFileSync as OX}from"fs";function DX(t){XS+=1;let e=hM("powershell.exe",["-NoProfile","-NonInteractive","-Command",`(Get-CimInstance Win32_Process -Filter "ProcessId=${t}").CreationDate.ToString('yyyyMMddHHmmss.ffffff')`],{encoding:"utf-8",timeout:5e3,windowsHide:!0,env:{...Fn(process.env),LC_ALL:"C",LANG:"C"}});if(e.status===0){let r=e.stdout.trim();return r.length>0?r:null}return null}function PX(t,e=!1){if(!e){let i=mM.get(t);if(i&&Date.now()-i.capturedAtMs<RX)return i.token}let r=null;try{r=DX(t)}catch(i){w.debug("SYSTEM","captureProcessStartToken: powershell CIM lookup failed",{pid:t,error:i instanceof Error?i.message:String(i)}),r=null}return mM.set(t,{token:r,capturedAtMs:Date.now()}),r}function yM(t,e){if(!Number.isInteger(t)||t<=0)return null;if(process.platform==="linux"){XS+=1;try{let r=OX(`/proc/${t}/stat`,"utf-8"),i=r.lastIndexOf(") ");if(i<0)return null;let o=r.slice(i+2).split(" ")[19];return o&&/^\d+$/.test(o)?o:null}catch(r){return w.debug("SYSTEM","captureProcessStartToken: /proc read failed",{pid:t,error:r instanceof Error?r.message:String(r)}),null}}if(process.platform==="win32")return PX(t,e);XS+=1;try{let r=hM("ps",["-p",String(t),"-o","lstart="],{encoding:"utf-8",timeout:2e3,env:{...Fn(process.env),LC_ALL:"C",LANG:"C"}});if(r.status!==0)return null;let i=r.stdout.trim();return i.length>0?i:null}catch(r){return w.debug("SYSTEM","captureProcessStartToken: ps exec failed",{pid:t,error:r instanceof Error?r.message:String(r)}),null}}function fo(t){return yM(t,!1)}function Di(t,e){if(e===null)return!0;let r=yM(t,!0);return r===null?!0:r===e}var RX,mM,XS,QS=E(()=>{"use strict";Oe();ru();RX=5e3,mM=new Map,XS=0});import{execFile as MX}from"child_process";import{readdirSync as KX,readFileSync as jX}from"fs";import{promisify as NX}from"util";async function Jn(t,e={}){let r=e.signalMode==="immediate",i=e.expectedStartToken!==void 0?e.expectedStartToken:fo(t),n=()=>Di(t,i);if(!n()){w.warn("PROCESS","Skipping tree-kill: root PID was reused since it was captured",{pid:t});return}if(w.debug("PROCESS",`Killing process tree rooted at PID ${t}`,{immediate:r}),process.platform==="win32"){try{await eE("taskkill",["/PID",String(t),"/T","/F"],{timeout:5e3,windowsHide:!0})}catch(o){let s=o.code,a=String(o.stderr??""),l=o instanceof Error?o.message:String(o);if(s===LX||$X.test(`${a} ${l}`)){w.debug("PROCESS","taskkill reported the process was already gone",{pid:t});return}throw new sf(t,`taskkill failed for PID ${t} (exit ${String(s)}): ${a.trim()||l}`,{cause:o})}return}try{let o=r?"SIGKILL":"SIGTERM",s=await af(t);for(let c of s){if(!Di(c.pid,c.startToken)){w.debug("PROCESS",`Skipping ${o}: descendant PID was reused since enumeration`,{pid:c.pid,rootPid:t});continue}try{process.kill(c.pid,o)}catch{}}if(n())try{process.kill(t,o)}catch(c){let u=c instanceof Error?c:new Error(String(c)),d=u.code;d!=="ESRCH"&&w.debug("PROCESS",`Failed to ${o} PID ${t}`,{code:d},u)}else w.warn("PROCESS","Skipping root signal: PID was reused during descendant enumeration",{pid:t,signal:o});r||await new Promise(c=>setTimeout(c,500));let a=n()?await af(t):[],l=new Map;for(let c of s)l.set(c.pid,c.startToken);for(let c of a)l.set(c.pid,c.startToken);for(let[c,u]of l){if(!Di(c,u)){w.debug("PROCESS","Skipping SIGKILL: descendant PID was reused since the scan",{pid:c,rootPid:t});continue}try{process.kill(c,"SIGKILL")}catch{}}if(!r&&n())try{process.kill(t,"SIGKILL")}catch{}}catch(o){throw new sf(t,`tree-kill failed for PID ${t}: ${o instanceof Error?o.message:String(o)}`,{cause:o})}}async function UX(){return process.platform==="win32"?zX():process.platform==="linux"?FX():JX()}function FX(){let t=[],e;try{e=KX("/proc")}catch(r){return w.warn("PROCESS","Cannot read /proc \u2014 tree-kill degrades to a single-PID kill",{error:r instanceof Error?r.message:String(r)}),t}for(let r of e){if(!/^\d+$/.test(r))continue;let i;try{i=jX(`/proc/${r}/stat`,"utf-8")}catch{continue}let n=i.lastIndexOf(") ");if(n<0)continue;let o=i.slice(n+2).split(" "),s=Number.parseInt(o[1]??"",10),a=o[19];Number.isFinite(s)&&t.push({pid:Number.parseInt(r,10),ppid:s,startToken:a&&/^\d+$/.test(a)?a:null})}return t}async function JX(){let t;try{t=(await eE("ps",["-eo","pid=,ppid=,lstart="],{timeout:1e4,maxBuffer:33554432,env:{...process.env,LC_ALL:"C",LANG:"C"}})).stdout}catch(r){return w.warn("PROCESS","Cannot enumerate the process table \u2014 tree-kill degrades to a single-PID kill",{error:r instanceof Error?r.message:String(r)}),[]}let e=[];for(let r of t.split(`
@@ -3,7 +3,7 @@
3
3
  "name": "Claude-Mem (Persistent Memory)",
4
4
  "description": "OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
5
5
  "kind": "memory",
6
- "version": "13.27.0",
6
+ "version": "13.27.1",
7
7
  "license": "Apache-2.0",
8
8
  "author": "thedotmack",
9
9
  "homepage": "https://claude-mem.ai",
@@ -1,5 +1,5 @@
1
- 8f7afa104e956d318696610358d0b55c032397ed5232cbac1b064ed43429f09e SKILL.md
2
- a0bc9b77b1895208cd63587b1524f16e203a5824b27786cb6ff18a2a260f234c scripts/acr.py
1
+ a8388ef9d748761e8daddf0d3dd4889d7dc67688c7d7d65bd003becd7131b613 SKILL.md
2
+ 8cf92dc443325efa5f2239d47582607ea38830396defee45669fb11fe76fc573 scripts/acr.py
3
3
  bc4fc2a17a1b2bd38422000c907ec07bf1deb1008c7b1bfcf45b2e41364da3e4 scripts/acr/__init__.py
4
4
  a5a3394119fca6a376843826c29cec40e6edbd3085f22024bfdad27601ff8b85 scripts/acr/behavior.py
5
5
  f72d82461e2c02584bcc3f4d7a05fd18165e5b3d255c0333c7763599daa5adaa scripts/acr/classify.py
@@ -12,15 +12,15 @@ f617e2c75cd65f4415a7ae6107d03681bc85365684ebf777b594f074c87e231c scripts/acr/mi
12
12
  3ae9141248b97d27a87c7ac9526db0a663f90924448126e4b2cab5c090a4f3a3 scripts/acr/patterns.py
13
13
  9bc988081b4e48f252a9b47ddd6f26bf05766bd5e41495736d1f29c3db833b6f scripts/acr/period.py
14
14
  5235c50c24012738321cf441d61dd8987be398b5939af8c9fd038f49417c261b scripts/acr/prices.py
15
- 53dc7888ba105e67e58b719c25f0a7b96402d4ae9dbc9a29362745064105590d scripts/acr/render.py
16
- a96306368d051a24be5bbfa516ae7f7448b10a01176548671de142068bf444be scripts/acr/rollup.py
15
+ 64b71efe36cdfb846a11680e3b61873110442395004e8dc2e571245634bcc96b scripts/acr/render.py
16
+ 652ff78b6e470664817d83b802e452abb08f97175650f7d78855719806307824 scripts/acr/rollup.py
17
17
  6637b40b8171127a9d76cf9a971412f6e0ee351246e1944070cd63d36d2bd3fb scripts/acr/rules.json
18
18
  2c57f2e41221be84ce08d5146cdff6d5016119c120fc43505afe7274a18d0012 scripts/acr/rules.py
19
19
  6328e919ab8e09535129b4f3845fc2e0d0ab50d0beafbc3042f57bee07ba9db2 scripts/acr/sample.py
20
20
  d329883df29e52e66312ef0829a17521adb832c4d267a69957aba10235b34679 scripts/acr/snapshot.py
21
21
  0d03c8ed1c8076ec59dc8f6c1241ca52f5d2ecf83f47792d8dfb20a1c8737a81 scripts/acr/sync.py
22
22
  05155c3c9d0f061484d933839189a7b14d2b56bab603ef41a0e0e141be4c8848 scripts/acr/transcripts.py
23
- a2ce04c2409982bf38427bcf8f4fa40cf119d289159d67dea5d2c5966066b6c1 scripts/acr/wins.py
23
+ e9d4a37c31d6ca9d0c403cbe76bb4e2a087d5808cd4d038315bf5750cfe53e5c scripts/acr/wins.py
24
24
  34f188a792da085d50a0fd608836ca03395a01978229aad90fb7901be753e655 scripts/tests/_db.py
25
25
  1baa143bea04326b7096a0654f91c22255d1f131ed23af454395b9d3c959f78d scripts/tests/_paths.py
26
26
  a2cc21046a1e79c3977e5bf22ca75f67c0c77d4e725ef33663ac62b289688e7e scripts/tests/test_behavior.py
@@ -34,7 +34,7 @@ ccbb9887b02d145359a42cfb3919850d4e4b7b538b9017caa7cc4312a94b85cd scripts/tests/
34
34
  323379622ffc8984d3bf2d3710fb1cc8284fd7174ed7e6725dbf5613bea5cdd4 scripts/tests/test_snapshot.py
35
35
  ca1b30d767ddf668bf9ace78e76aa4592eb299a09c1f5f9049e089f610d1ba11 scripts/tests/test_sync.py
36
36
  2c39ce509e7293e95ffa81f766f46792c76a0497f491806d797e9ec76a822d52 scripts/tests/test_transcripts.py
37
- 96acc8b6531383951b347a816ef511765fa24564425ad0bd9ee0ad27b18d7e9d scripts/tests/test_wins.py
37
+ f0d9bd9b031876277addd413b05efb5ec762932254b24975e2bc16b93562dbc7 scripts/tests/test_wins.py
38
38
  e18dd87adc7439dbc3485328eb39bdf8c72231713412a7d34f095007e939576f scripts/tests/fixtures/make_report_fixtures.py
39
39
  f89b3a779a195670f381c40ec2fcda56b049e14c3cefa0483d583fc11209ab06 scripts/tests/fixtures/prices_sample.json
40
40
  2763df8beaebc33e037f0c618b3e1d0e95747f277fa41f139cd5630cf9901226 scripts/tests/fixtures/report-1day.json
@@ -223,6 +223,7 @@ Each of these needs Alex's explicit go; the report shows the honest fallback unt
223
223
  - **OpenRouter key (needs Alex)** — a regular inference `OPENROUTER_API_KEY` supplied as an environment variable through the house's secure secret flow; never a management or provisioning key, never read from a settings file, never printed or written to any output. The per-key endpoint gives UTC day/week/month snapshots, so the figure is shown with its bucket label and switches the hero to MEASURED only when the bucket fully covers the report window. A range-by-day source (`/api/v1/activity`) exists but needs a management key, so it is not implemented (G5).
224
224
  - **Mac transcripts (needs Alex)** — sessions from Alex's Mac have no transcript on the box, so their cost is EXTRAPOLATED (low confidence) until a device export is merged. Either Alex runs, on the Mac, from a plain checkout of this skill (system `python3` 3.9+): `python3 scripts/acr.py collect --export-device mac --start YYYY-MM-DD --end YYYY-MM-DD --out ~/acr-export` and shares `~/acr-export/device-usage-mac.json` (ids, timestamps, token counts, model names; no prompt or observation text, no settings, no paths); or, only after Alex's explicit go for that specific run (window, machine, destination named), an orchestrating agent runs the same command on the registered Mac through the house's registered-machine tooling and copies only `device-usage-mac.json` to the box. A go for one run is not a go for the next (G6 covered Sep 18–26 only). Merge with `--device-usage`; joined sessions flip to `estimated_usage` with `device: mac`.
225
225
  - **Grok Bot (ALWAYS unavailable, no seat count)** — Grok Bot is Cursor's cloud agent. Cursor shows its weekly usage only on the plan screen; there is no API or export, and the house has no xAI key for it. The report says exactly "Grok Bot usage: unavailable" (G4): no dollars, no guessed per-seat price, no seat count. A figure Alex supplies by hand would be entered as `measured_manual` with `entered_by: Alex` and the date, never inferred; that would be a new decision.
226
+ - **Wins from GitHub (read-only)** — besides box transcripts and ship observations, `rollup` lists merged PRs with `gh pr list --state merged` for the repos in `--wins-repo` (default `thedotmack/claude-mem`), filtered by merge time inside the PT window and deduplicated by repo and PR number. When `gh` is missing, unauthenticated or rate-limited, that source reads "unavailable" in Details and the report still completes; it is never a zero. `--no-gh` turns every gh call off.
226
227
  - **Win cost (unmeasured until R4)** — sessions are not linked to PRs today, so every win reads "unmeasured (session not linked to PR)". Once commits carry a `Claude-Session: <content_session_id>` trailer (G13: trailer only, bare id), PR and publish wins get `ESTIMATED · session-linked` costs. There is no fallback estimate (G11).
227
228
  - **Merge to main, npm publish, `/version-bump`** — each needs Alex's separate go. Pushing the work branch and opening the PR are routine.
228
229
 
@@ -395,12 +395,16 @@ def details(d, open_):
395
395
  out = [f'<details id="details" {"open" if open_ else ""}><summary>Details &amp; evidence</summary>']
396
396
  # wins (3.8) and mistakes by day, first so fragment links land near the top
397
397
  out.append("<h4>Wins</h4>")
398
+ gs = (d["wins"].get("sources") or {}).get("github_merged_prs")
399
+ if gs:
400
+ rep = "; ".join(f'{esc(r)}: {esc(v["status"])}' + (f' ({v["found"]} found)' if v.get("found") is not None else "") for r, v in (gs.get("repos") or {}).items())
401
+ out.append(f'<div class="muted">GitHub merged PRs (read-only gh pr list): {esc(gs["status"])} · {rep}</div>')
398
402
  for w in d["wins"].get("items", []):
399
403
  cost = "unmeasured (session not linked to PR)" if w.get("usd") is None else f'≈{usd2(w["usd"])} {tag("extra") if w.get("cost_basis") == "extrapolated" else tag("est")} · {esc(w.get("cost_basis") or "")}'
400
404
  tok = w.get("tokens"); tok_s = ", ".join(f"{k} {v:,}" for k, v in tok.items()) if tok else "—"
401
405
  out.append(f'<div id="win-{esc(w["win_id"])}"><b>{esc(w["win_id"])}</b> · {esc(w["kind"])} · {esc(w["title"])} · {esc(w.get("ts_pt") or "")}'
402
406
  + (f' · <a href="{esc(w["url"])}">{esc(w["url"])}</a>' if w.get("url") else "")
403
- + f'<br><span class="muted">cost {cost} · sessions {esc(", ".join(w.get("sessions") or []) or "—")} · tokens {tok_s} · attribution {esc(d["wins"].get("attribution_method") or "none")} · evidence {esc(", ".join(map(str, w.get("evidence_ids") or [])) or "—")}</span></div>')
407
+ + f'<br><span class="muted">sources {esc(", ".join(w.get("sources") or []))} · cost {cost} · sessions {esc(", ".join(w.get("sessions") or []) or "—")} · tokens {tok_s} · attribution {esc(d["wins"].get("attribution_method") or "none")} · evidence {esc(", ".join(map(str, w.get("evidence_ids") or [])) or "—")}</span></div>')
404
408
  if not d["wins"].get("items"): out.append('<div class="muted">No wins found (merged PR, published version, or praise).</div>')
405
409
  if d["wins"].get("dropped"): out.append(f'<div class="muted">{len(d["wins"]["dropped"])} candidate(s) dropped: ' + esc("; ".join(str(x.get("reason")) for x in d["wins"]["dropped"][:6])) + "</div>")
406
410
  out.append("<h4>Mistakes by day</h4>")
@@ -55,7 +55,7 @@ def waste_split(li):
55
55
  li["productive_cost"] = round(c - li["wasted_cost"] - li["recovery_cost"], 2)
56
56
 
57
57
 
58
- def build(usage, prices, db, scope, window_block, now=None, use_gh=True, gh=wins.gh_pr_view, remote=wins.git_remote, behavior=True, classify=None, rules_dir=None, glob_pattern=None, device_exports=(), precision=None):
58
+ def build(usage, prices, db, scope, window_block, now=None, use_gh=True, gh=wins.gh_pr_view, remote=wins.git_remote, behavior=True, classify=None, rules_dir=None, glob_pattern=None, device_exports=(), precision=None, wins_repos=wins.WINS_REPOS, gh_list=wins.gh_merged_prs):
59
59
  now = now or period.now_pt(); pricer = costs.Pricer(prices)
60
60
  sess = evidence.load_sessions(db, scope)
61
61
  ev, dev_labels, counts, obs_unpriced = evidence.load_evidence(db, scope, sess, pricer.observer_input_rate)
@@ -175,7 +175,7 @@ def build(usage, prices, db, scope, window_block, now=None, use_gh=True, gh=wins
175
175
  rows_by_cs = collections.defaultdict(list)
176
176
  for r in rows: rows_by_cs[r["session"]].append(r)
177
177
  files = sorted({r["file"] for r in rows if r.get("file")})
178
- wins_block, wins_by_day = wins.build(files, scope.S, scope.E, ship_obs, sessions_by_cs, rows_by_cs, ratio, days, gh=gh, remote=remote, use_gh=use_gh)
178
+ wins_block, wins_by_day = wins.build(files, scope.S, scope.E, ship_obs, sessions_by_cs, rows_by_cs, ratio, days, gh=gh, remote=remote, use_gh=use_gh, repos=tuple(wins_repos), gh_list=gh_list)
179
179
  if wins_block["cost_status"] == "session_linked": # spend not tied to any win, only once at least one win cost exists (2.8)
180
180
  wins_block["unattributed_usd"] = round(max(spend["total_estimate_usd"] - wins_block["total_attributed_usd"], 0.0), 2)
181
181
  # ---- Phase 2B: behavior pass, mistakes line, mistakes timeline (one union set) ----
@@ -336,7 +336,7 @@ def run_rollup(args):
336
336
  with open(args.precision) as fh: precision = json.load(fh)
337
337
  cl = dict(enabled=True, cap_usd=getattr(args, "classify_budget", None) or mistakes.classify_mod.CAP_USD, model=getattr(args, "classify_model", None)) if getattr(args, "classify", False) else None
338
338
  report, evid, review = build(usage, prices, db, scope, window_block, use_gh=not getattr(args, "no_gh", False),
339
- behavior=not getattr(args, "no_behavior", False), classify=cl, rules_dir=getattr(args, "rules_dir", None), device_exports=exports, precision=precision)
339
+ behavior=not getattr(args, "no_behavior", False), classify=cl, rules_dir=getattr(args, "rules_dir", None), device_exports=exports, precision=precision, wins_repos=tuple(getattr(args, "wins_repo", None) or wins.WINS_REPOS))
340
340
  finally:
341
341
  db.close()
342
342
  mp = getattr(args, "measured", None) or os.path.join(args.out, "measured.json")
@@ -131,6 +131,55 @@ def gh_pr_view(number, repo):
131
131
  except Exception: return None
132
132
 
133
133
 
134
+ # ---- read-only merged-PR source (GitHub) ----
135
+ WINS_REPOS = ("thedotmack/claude-mem",) # default repos for the merged-PR list; override with `rollup --wins-repo`
136
+ GH_SOURCE = "github_merged_prs"
137
+ GH_LIST_LIMIT = 200
138
+
139
+
140
+ def _gh_status(returncode, stderr):
141
+ """Map a failed `gh` call to an honest status; never a number."""
142
+ t = (stderr or "").lower()
143
+ if "rate limit" in t or "api rate limit" in t or "secondary rate" in t: return "rate-limited"
144
+ if "auth login" in t or "authentication" in t or "not logged in" in t or "http 401" in t or "bad credentials" in t: return "unauthenticated"
145
+ return f"gh error (exit {returncode})"
146
+
147
+
148
+ def gh_merged_prs(repo, s_ms, e_ms, run=subprocess.run):
149
+ """Read-only: gh pr list --repo <repo> --state merged --json number,title,mergedAt,url, searched by merge date
150
+ (UTC days covering the PT window) and then filtered by mergedAt inside [s_ms, e_ms). Returns (rows, status)
151
+ with status "ok" or a reason: "gh missing" | "unauthenticated" | "rate-limited" | "gh error (exit n)"."""
152
+ a = dt.datetime.fromtimestamp(s_ms / 1000, dt.timezone.utc).date() - dt.timedelta(days=1)
153
+ b = dt.datetime.fromtimestamp(e_ms / 1000, dt.timezone.utc).date() + dt.timedelta(days=1)
154
+ cmd = ["gh", "pr", "list", "--repo", repo, "--state", "merged", "--limit", str(GH_LIST_LIMIT), "--json", "number,title,mergedAt,url",
155
+ "--search", f"merged:{a.isoformat()}..{b.isoformat()}"]
156
+ try:
157
+ r = run(cmd, capture_output=True, text=True, timeout=40)
158
+ except FileNotFoundError: return [], "gh missing"
159
+ except (subprocess.TimeoutExpired, OSError) as ex: return [], f"gh error ({type(ex).__name__})"
160
+ if r.returncode != 0: return [], _gh_status(r.returncode, r.stderr)
161
+ try: rows = json.loads(r.stdout or "[]")
162
+ except ValueError: return [], "gh error (unparseable output)"
163
+ out = []
164
+ for x in rows:
165
+ if not x.get("mergedAt") or not x.get("number"): continue
166
+ ms = _ts_ms(x["mergedAt"])
167
+ if s_ms <= ms < e_ms: out.append(dict(number=int(x["number"]), title=x.get("title") or "", url=x.get("url"), ts_ms=ms))
168
+ return out, "ok"
169
+
170
+
171
+ def gh_list_candidates(repos, s_ms, e_ms, lister=gh_merged_prs):
172
+ """Candidates from the merged-PR list per repo plus a per-repo status for the report."""
173
+ cands = []; status = {}
174
+ for repo in repos:
175
+ rows, st = lister(repo, s_ms, e_ms)
176
+ status[repo] = dict(status=st, found=len(rows) if st == "ok" else None)
177
+ for x in rows:
178
+ cands.append(dict(kind="pr", source=GH_SOURCE, repo=repo, number=x["number"], title=x["title"], url=x["url"], ts_ms=x["ts_ms"],
179
+ project=repo.split("/")[-1], sessions=[], evidence_ids=[], gh_listed=True))
180
+ return cands, status
181
+
182
+
134
183
  def session_trailers(message):
135
184
  """Bare content_session_ids named by `Claude-Session:` trailers in one commit message (G13).
136
185
  Non-bare values (URLs, ids with spaces or parentheses, anything else) are rejected; `Session:`
@@ -182,7 +231,8 @@ def _key(c, dropped):
182
231
  dropped.append(dict(source=c["source"], reason="tag push without an identifiable tag", cmd=c.get("cmd"))); return None
183
232
 
184
233
 
185
- def build(files, s_ms, e_ms, ship_obs, sessions_by_cs, rows_by_cs, ratio, days, gh=gh_pr_view, remote=git_remote, use_gh=True):
234
+ def build(files, s_ms, e_ms, ship_obs, sessions_by_cs, rows_by_cs, ratio, days, gh=gh_pr_view, remote=git_remote, use_gh=True,
235
+ repos=WINS_REPOS, gh_list=gh_merged_prs):
186
236
  """Returns (wins_block, wins_by_day). sessions_by_cs: cs -> dict(project, memory_session_id, has_transcript,
187
237
  observer_tokens, cwd); rows_by_cs: cs -> usage rows with ts_ms and x1e6 (None when unpriced)."""
188
238
  repo_cache = {}
@@ -202,6 +252,10 @@ def build(files, s_ms, e_ms, ship_obs, sessions_by_cs, rows_by_cs, ratio, days,
202
252
  if c["kind"] == "pr" and not c.get("repo"): c["repo"] = (remote(c["cwd"]) if remote and c.get("cwd") else None) or project_repo(c["project"])
203
253
  cands.append(c)
204
254
  cands += list(observation_candidates(ship_obs, project_repo))
255
+ gh_status = {}
256
+ if use_gh and gh_list and s_ms is not None and e_ms is not None:
257
+ gh_cands, gh_status = gh_list_candidates(repos, s_ms, e_ms, lister=gh_list); cands += gh_cands
258
+ elif repos: gh_status = {r: dict(status="unavailable (gh disabled for this run)", found=None) for r in repos}
205
259
  merged = collections.OrderedDict()
206
260
  for c in cands:
207
261
  key = _key(c, dropped)
@@ -210,16 +264,18 @@ def build(files, s_ms, e_ms, ship_obs, sessions_by_cs, rows_by_cs, ratio, days,
210
264
  if w is None:
211
265
  w = merged[key] = dict(win_id=None, kind=c["kind"], key=key, title=c.get("title") or key, url=None, ts_ms=c["ts_ms"],
212
266
  project=c.get("project"), repo=c.get("repo"), number=c.get("number"), evidence_ids=[], sessions=[],
213
- sources=[], confirmed=False, linked_sessions=[])
267
+ sources=[], confirmed=False, linked_sessions=[], gh_listed=False)
214
268
  for eid in c.get("evidence_ids", []):
215
269
  if eid not in w["evidence_ids"]: w["evidence_ids"].append(eid)
216
270
  for sid in c.get("sessions", []):
217
271
  if sid and sid not in w["sessions"]: w["sessions"].append(sid)
218
272
  if c["source"] not in w["sources"]: w["sources"].append(c["source"])
219
- if c["ts_ms"] and (w["ts_ms"] is None or c["ts_ms"] < w["ts_ms"]): w["ts_ms"] = c["ts_ms"]
273
+ if c.get("gh_listed"): # the registry's own merge time, title and url win over transcript guesses
274
+ w["gh_listed"] = True; w["confirmed"] = True; w["ts_ms"] = c["ts_ms"]; w["url"] = w["url"] or c.get("url"); w["title"] = c.get("title") or w["title"]
275
+ elif c["ts_ms"] and (w["ts_ms"] is None or c["ts_ms"] < w["ts_ms"]): w["ts_ms"] = c["ts_ms"]
220
276
  items = []
221
277
  for key, w in merged.items():
222
- if w["kind"] == "pr" and use_gh and gh and w.get("repo") and w.get("number"):
278
+ if w["kind"] == "pr" and use_gh and gh and w.get("repo") and w.get("number") and not (w["gh_listed"] and w["sources"] == [GH_SOURCE]): # listed-only wins need no second call
223
279
  info = gh(w["number"], w["repo"])
224
280
  if info is not None:
225
281
  if not info.get("mergedAt"): dropped.append(dict(key=key, reason="gh pr view: not merged")); continue
@@ -249,6 +305,10 @@ def build(files, s_ms, e_ms, ship_obs, sessions_by_cs, rows_by_cs, ratio, days,
249
305
  total = round(sum(w["usd"] for w in items if w["usd"] is not None), 2) if linked else None
250
306
  block = dict(items=out, cost_status="session_linked" if linked else "unmeasured", attribution_method=ATTRIBUTION if linked else None,
251
307
  unattributed_usd=None, total_attributed_usd=total, praise_n=len(praise), dropped=dropped, gh_unconfirmed=gh_unconfirmed,
308
+ sources=dict(transcripts="box transcripts (gh pr merge / npm publish / release / tag push)", ship_observations="claude-mem ship observations",
309
+ **{GH_SOURCE: dict(label="GitHub merged PRs (read-only gh pr list)", repos={r: v for r, v in gh_status.items()},
310
+ status=("ok" if gh_status and all(v["status"] == "ok" for v in gh_status.values()) else "unavailable" if gh_status else "off"),
311
+ found=sum(v["found"] or 0 for v in gh_status.values()) if gh_status else 0)}),
252
312
  note="finished line items (shipped/completed) are outcomes, not wins (G12); praise detection is a Phase 2B hook")
253
313
  by_day = [dict(day_pt=d, count=sum(1 for w in out if w["day_pt"] == d),
254
314
  usd=(round(sum(w["usd"] for w in out if w["day_pt"] == d and w["usd"] is not None), 2) if linked else None),
@@ -148,7 +148,8 @@ def build_parser():
148
148
  r.add_argument("--out", required=True, metavar="DIR", help="directory holding usage.json and prices.json; outputs land here")
149
149
  r.add_argument("--prices", metavar="FILE", help="price table to use instead of DIR/prices.json")
150
150
  r.add_argument("--db", metavar="FILE", help=argparse.SUPPRESS)
151
- r.add_argument("--no-gh", action="store_true", help="skip the read-only `gh pr view` confirmation of merged-PR wins")
151
+ r.add_argument("--no-gh", action="store_true", help="skip every gh call: the merged-PR list source and the per-PR confirmation (wins from GitHub show as unavailable)")
152
+ r.add_argument("--wins-repo", action="append", metavar="OWNER/REPO", help="repo(s) for the read-only merged-PR wins source (default: thedotmack/claude-mem; repeatable)")
152
153
  r.add_argument("--no-behavior", action="store_true", help="skip the Phase 2B behavior pass (no mistakes line)")
153
154
  r.add_argument("--classify", action="store_true", help="run the optional classifier on unsettled candidates (off by default, G8)")
154
155
  r.add_argument("--classify-budget", type=float, metavar="USD", help="hard cap per run for --classify (default $2.00, G8)")
@@ -102,8 +102,9 @@ class Build(Transcripts):
102
102
  def ship(self, n=4125, ms=S + 6 * H):
103
103
  return [dict(id=9, title=f"PR #{n} merged to main: x", ts_ms=ms, project="claude-mem", memory_session_id="m1", content_session_id="cs1")]
104
104
 
105
- def build(self, files=(), ship_obs=(), rows=None, gh=None, ratio=None, remote=lambda cwd: "o/r"):
106
- return wins.build(list(files), S, E, list(ship_obs), self.sessions(), rows or {}, ratio, DAYS, gh=gh or fake_gh({}), remote=remote, use_gh=gh is not None)
105
+ def build(self, files=(), ship_obs=(), rows=None, gh=None, ratio=None, remote=lambda cwd: "o/r", gh_list=None, repos=("o/r",)):
106
+ return wins.build(list(files), S, E, list(ship_obs), self.sessions(), rows or {}, ratio, DAYS, gh=gh or fake_gh({}), remote=remote,
107
+ use_gh=(gh is not None or gh_list is not None), repos=repos, gh_list=gh_list)
107
108
 
108
109
  def test_no_link_means_every_usd_null(self):
109
110
  f = self.write("a.jsonl", tool_lines("cs1", S + 2 * H, "gh pr merge 5 --repo o/r"))
@@ -170,6 +171,47 @@ class Build(Transcripts):
170
171
  block, by_day = self.build([], []) # completed sessions exist in sessions(), but no merge/publish evidence
171
172
  self.assertEqual(block["items"], []); self.assertEqual([d["count"] for d in by_day], [0, 0, 0])
172
173
 
174
+ # ---- read-only GitHub merged-PR source ----
175
+ def test_gh_list_adds_wins_dedupes_and_labels_source(self):
176
+ f = self.write("a.jsonl", tool_lines("cs1", S + 2 * H, "gh pr merge 4125 --repo o/r"))
177
+ lister = lambda repo, s, e: ([dict(number=4125, title="from github", url="https://github.com/o/r/pull/4125", ts_ms=S + 3 * H),
178
+ dict(number=4200, title="only on github", url="https://github.com/o/r/pull/4200", ts_ms=S + 30 * H)], "ok")
179
+ block, by_day = self.build([f], self.ship(4125), gh_list=lister)
180
+ keys = {i["key"]: i for i in block["items"]}
181
+ self.assertEqual(sorted(keys), ["o/r#4125", "o/r#4200"]) # 4125 counted once across three sources
182
+ self.assertEqual(set(keys["o/r#4125"]["sources"]), {"transcript", "ship_observation", wins.GH_SOURCE}); self.assertTrue(keys["o/r#4125"]["confirmed"])
183
+ self.assertEqual((keys["o/r#4200"]["sources"], keys["o/r#4200"]["title"], keys["o/r#4200"]["usd"], keys["o/r#4200"]["cost_status"]), ([wins.GH_SOURCE], "only on github", None, "unmeasured"))
184
+ src = block["sources"][wins.GH_SOURCE]; self.assertEqual((src["status"], src["found"], src["repos"]["o/r"]["status"]), ("ok", 2, "ok"))
185
+ self.assertEqual([d["count"] for d in by_day], [1, 1, 0])
186
+
187
+ def test_gh_list_unavailable_never_zero_and_report_completes(self):
188
+ f = self.write("a.jsonl", tool_lines("cs1", S + 2 * H, "gh pr merge 4125 --repo o/r"))
189
+ for reason in ("gh missing", "unauthenticated", "rate-limited"):
190
+ block, _ = self.build([f], gh_list=lambda repo, s, e, r=reason: ([], r))
191
+ src = block["sources"][wins.GH_SOURCE]
192
+ self.assertEqual((src["status"], src["repos"]["o/r"]["status"], src["repos"]["o/r"]["found"]), ("unavailable", reason, None), reason)
193
+ self.assertEqual(len(block["items"]), 1) # the other sources still ship
194
+ block, _ = self.build([f], gh=fake_gh({}), gh_list=None)
195
+ self.assertEqual(block["sources"][wins.GH_SOURCE]["repos"]["o/r"]["status"], "unavailable (gh disabled for this run)")
196
+
197
+ def test_gh_merged_prs_window_edges_and_failures(self):
198
+ rows = [dict(number=1, title="a", url="u1", mergedAt=iso(S)), dict(number=2, title="b", url="u2", mergedAt=iso(E)), dict(number=3, title="c", url="u3", mergedAt=iso(E - 1000)), dict(number=4, title="no merge", url="u4", mergedAt=None)]
199
+ ok = lambda cmd, **k: type("R", (), dict(returncode=0, stdout=json.dumps(rows), stderr=""))()
200
+ out, st = wins.gh_merged_prs("o/r", S, E, run=ok)
201
+ self.assertEqual((st, [x["number"] for x in out]), ("ok", [1, 3])) # start inclusive, end exclusive, unmerged dropped
202
+ def missing(cmd, **k): raise FileNotFoundError("gh")
203
+ self.assertEqual(wins.gh_merged_prs("o/r", S, E, run=missing), ([], "gh missing"))
204
+ auth = lambda cmd, **k: type("R", (), dict(returncode=4, stdout="", stderr="To get started with GitHub CLI, please run: gh auth login"))()
205
+ self.assertEqual(wins.gh_merged_prs("o/r", S, E, run=auth)[1], "unauthenticated")
206
+ rl = lambda cmd, **k: type("R", (), dict(returncode=1, stdout="", stderr="GraphQL: API rate limit exceeded for user"))()
207
+ self.assertEqual(wins.gh_merged_prs("o/r", S, E, run=rl)[1], "rate-limited")
208
+ junk = lambda cmd, **k: type("R", (), dict(returncode=0, stdout="not json", stderr=""))()
209
+ self.assertEqual(wins.gh_merged_prs("o/r", S, E, run=junk)[1], "gh error (unparseable output)")
210
+ seen = []
211
+ def spy(cmd, **k): seen.append(cmd); return ok(cmd)
212
+ wins.gh_merged_prs("o/r", S, E, run=spy)
213
+ self.assertEqual(seen[0][:6], ["gh", "pr", "list", "--repo", "o/r", "--state"]); self.assertEqual(seen[0][1:3], ["pr", "list"]); self.assertNotIn("merge", seen[0][2]) # read-only: list, never merge
214
+
173
215
  def test_publish_from_transcript_and_observation_dedup(self):
174
216
  f = self.write("a.jsonl", tool_lines("cs1", S + 2 * H, "npm publish", "npm notice Publishing to https://registry.npmjs.org/\n+ claude-mem@13.25.3"))
175
217
  obs = [dict(id=3, title="claude-mem 13.25.3 published to npm", ts_ms=S + 3 * H, project="claude-mem/wt", memory_session_id="m1", content_session_id="cs1")]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.27.0",
3
+ "version": "13.27.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.27.0",
3
+ "version": "13.27.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.27.0",
3
+ "version": "13.27.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-plugin",
3
- "version": "13.27.0",
3
+ "version": "13.27.1",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for claude-mem bundled hooks",
6
6
  "type": "module",