@wukazis/euphony 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ structured chat data and Codex sessions in the browser.
37
37
  | Filtering and focus mode | Filters datasets with JMESPath and narrows visible messages by role, recipient, or content type. |
38
38
  | Grid and editor modes | Supports dataset skimming in grid view and direct JSONL editing in editor mode. |
39
39
  | Harmony token rendering | Shows Harmony renderer output, token IDs, decoded tokens, and rendered display strings. |
40
- | Local Codex sessions index | Scans local Codex history under `~/.codex/sessions`, shows a lightweight session list, and opens full sessions on demand. |
40
+ | Local Codex sessions index | Scans local Codex history under `~/.codex/sessions`, supports keyword search, and opens full sessions on demand. |
41
41
  | Embeddable web components | Ships reusable custom elements for integrating the viewer into other web apps in any framework (e.g., React, Svelte, Vue). |
42
42
 
43
43
  ## Get Started
@@ -160,13 +160,33 @@ To open the local Codex sessions index directly:
160
160
  http://127.0.0.1:8020/?path=codex%3Asessions
161
161
  ```
162
162
 
163
+ To open the local Claude Code sessions index directly:
164
+
165
+ ```text
166
+ http://127.0.0.1:8020/?path=claude%3Asessions
167
+ ```
168
+
169
+ To open the Codex token usage overview directly:
170
+
171
+ ```text
172
+ http://127.0.0.1:8020/?path=codex%3Ausage
173
+ ```
174
+
163
175
  The `codex:sessions` path scans:
164
176
 
165
177
  ```text
166
178
  ~/.codex/sessions/**/*.jsonl
167
179
  ```
168
180
 
169
- Each session is shown as a lightweight row with its session path, first user message, relative time, and event count. Click a row to load the full session. On a full session page, use the `Sessions` button in the toolbar to return to the index.
181
+ The `claude:sessions` path scans:
182
+
183
+ ```text
184
+ ~/.claude/projects/**/*.jsonl
185
+ ```
186
+
187
+ Each Codex or Claude session is shown as a lightweight row with its session path, first user message, relative time, compact total token count, and event count. Use the search box to filter by session path or first user message. Click a row to load the full session. On a full session page, use the `Sessions` button in the toolbar to return to the index.
188
+
189
+ The `codex:usage` path reads each Codex session's latest `total_token_usage` and groups it into selectable ranges: `7d`, `30d`, and `1y`. The usage page includes a line chart with hover tooltips and compact token labels, for example `45,034 tokens` becomes `45k tokens` and million-scale counts become `m tokens`.
170
190
 
171
191
  To read Codex sessions from another directory:
172
192
 
@@ -174,6 +194,12 @@ To read Codex sessions from another directory:
174
194
  CODEX_SESSIONS_DIR=/path/to/sessions pnpm start
175
195
  ```
176
196
 
197
+ To read Claude Code sessions from another directory:
198
+
199
+ ```bash
200
+ CLAUDE_PROJECTS_DIR=/path/to/projects pnpm start
201
+ ```
202
+
177
203
  If you want backend translation, set either `OPENAI_API_KEY` or `OPEN_AI_API_KEY` before starting the backend.
178
204
 
179
205
  To use another host or port:
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";const f=new Set(["session_meta","response_item","event_msg","turn_context","compacted"]),p=new Set(["message","function_call","function_call_output","custom_tool_call","custom_tool_call_output","reasoning"]),y=new Set(["user_message","agent_message","agent_reasoning","context_compacted","turn_aborted","token_count"]),d=new Set(["user","assistant","system","summary","permission-mode","file-history-snapshot","last-prompt","ai-title"]),i=t=>typeof t=="object"&&t!==null,u=t=>{try{return JSON.parse(t)}catch{return null}},m=t=>{const e=[];for(const s of t){if(typeof s=="string"){const o=u(s);i(o)&&e.push(o);continue}i(s)&&e.push(s)}return e},g=t=>{const e=[];for(const s of t){if(typeof s=="string"){const o=u(s);i(o)&&e.push(o);continue}i(s)&&e.push(s)}return e},h=t=>{if(typeof t.type!="string")return!1;if(f.has(t.type)){if(t.type==="response_item"){const e=t.payload&&typeof t.payload.type=="string"?t.payload.type:null;return e?p.has(e):!0}if(t.type==="event_msg"){const e=t.payload&&typeof t.payload.type=="string"?t.payload.type:null;return e?y.has(e):!0}return!0}return t.type.startsWith("response_")||t.type.startsWith("event_")},_=t=>typeof t.type!="string"||!d.has(t.type)?!1:t.type==="user"||t.type==="assistant"?i(t.message):typeof t.sessionId=="string"||i(t.snapshot),S=t=>{if(!Array.isArray(t)||t.length===0)return!1;const e=m(t).filter(h);return e.length===0||e.length/t.length<.6?!1:e.some(o=>o.type==="session_meta")?!0:e.filter(o=>f.has(o.type??"")).length/e.length>=.6},N=t=>{if(!Array.isArray(t)||t.length===0)return!1;const e=g(t).filter(_);return e.length===0||e.filter(o=>o.type==="user"||o.type==="assistant").length===0?!1:e.length/t.length>=.4},E=t=>S(t)||N(t),l=t=>typeof t!="object"||t===null?!1:"messages"in t&&Array.isArray(t.messages),O=t=>{let e=null;if(t.length>0&&typeof t[0]=="object"&&!l(t[0])&&(e=t),t.length>0&&typeof t[0]=="string"){let s=!1;try{const o=JSON.parse(t[0]);l(o)&&(s=!0)}catch{s=!0}if(!s){e=[];for(const o of t){const n=JSON.parse(o);e.push(n)}}}if(e!==null){let s=null,o=!1;for(const n in e[0])if(typeof e[0][n]=="string")try{const r=JSON.parse(e[0][n]);if(l(r)){s=n,o=!0;break}}catch{continue}else if(l(e[0][n])){s=n;break}if(s!==null){const n=[];for(const r of e){const a=o?JSON.parse(r[s]):r[s];a.metadata??={};for(const c in r)c!==s&&(a.metadata[`euphonyTransformed-${c}`]=r[c]);n.push(a)}return n}}return null},v=t=>{const e=[];for(const[s,o]of t.entries())if(typeof o=="string"){const n=JSON.parse(o);let r=o;n.conversation_id!==void 0&&n.id===void 0&&(n.id=n.conversation_id,r=JSON.stringify(n)),t[s]=r,e.push(Array.isArray(n.messages))}else{const n=o;n.conversation_id!==void 0&&n.id===void 0&&(n.id=n.conversation_id),t[s]=n,e.push(Array.isArray(n.messages))}return e.every(Boolean)},T=t=>{const e=[];try{const s=JSON.parse(t);return e.push(s),e}catch{for(const o of t.split(`
2
+ `))try{e.push(JSON.parse(o))}catch{}}return e},J=t=>{let e=T(t);if(e.length===0)throw new Error("Failed to read any JSON or JSONL data.");if(E(e))return{dataType:"codex",codexSessionData:e};const s=O(e);if(s&&(e=s),!v(e))return{dataType:"json",jsonData:e};const o=[];for(const n of e)typeof n=="string"?o.push(JSON.parse(n)):o.push(n);return{dataType:"conversation",conversationData:o}};self.onmessage=async t=>{if(t.data.command!=="startParseData"){console.error("Worker: unknown message",t.data.command);return}const{requestID:e,sourceName:s,sourceText:o,sourceFile:n}=t.data.payload;try{const r=o??await n?.text();if(r===void 0)throw new Error("No source text or file was provided.");const a=J(r),c={command:"finishParseData",payload:{requestID:e,sourceName:s,...a}};postMessage(c)}catch(r){const a={command:"error",payload:{requestID:e,sourceName:s,message:r instanceof Error?r.message:String(r)}};postMessage(a)}}})();