@hienlh/ppm 0.7.8 → 0.7.10

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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CONTRIBUTING.md +46 -0
  3. package/LICENSE +21 -0
  4. package/README.md +34 -1
  5. package/bun.lock +1 -0
  6. package/dist/web/assets/ai-settings-section-BxCMGg-I.js +1 -0
  7. package/dist/web/assets/chat-tab-R_8ZfOG8.js +7 -0
  8. package/dist/web/assets/{code-editor-1FNaZKfA.js → code-editor-BbhIHbts.js} +1 -1
  9. package/dist/web/assets/{database-viewer-Hso-EwQH.js → database-viewer-BJYmlnr2.js} +1 -1
  10. package/dist/web/assets/{diff-viewer-BG2UNjTZ.js → diff-viewer-CS-wesGq.js} +1 -1
  11. package/dist/web/assets/{git-graph-DK_yDfWe.js → git-graph-B9eaNltz.js} +1 -1
  12. package/dist/web/assets/index-qElHXk-7.js +28 -0
  13. package/dist/web/assets/index-sMxUHxFZ.css +2 -0
  14. package/dist/web/assets/input-CVIzrYsH.js +41 -0
  15. package/dist/web/assets/keybindings-store-DrBQMVKg.js +1 -0
  16. package/dist/web/assets/{markdown-renderer-Xe_wjdJH.js → markdown-renderer-DpIu7iOT.js} +1 -1
  17. package/dist/web/assets/{postgres-viewer-CguN1z3q.js → postgres-viewer-B5-tRXE2.js} +1 -1
  18. package/dist/web/assets/settings-tab-3-ewawy0.js +1 -0
  19. package/dist/web/assets/{sqlite-viewer-VrZiiegZ.js → sqlite-viewer-CfIer2x_.js} +1 -1
  20. package/dist/web/assets/{terminal-tab-CabMjIRO.js → terminal-tab-qJxp0iOK.js} +2 -2
  21. package/dist/web/index.html +4 -4
  22. package/dist/web/sw.js +1 -1
  23. package/docs/codebase-summary.md +16 -5
  24. package/docs/system-architecture.md +20 -2
  25. package/package.json +4 -1
  26. package/src/lib/account-crypto.ts +53 -0
  27. package/src/providers/claude-agent-sdk.ts +77 -3
  28. package/src/server/index.ts +8 -0
  29. package/src/server/routes/accounts.ts +165 -0
  30. package/src/server/routes/chat.ts +2 -0
  31. package/src/services/account-selector.service.ts +109 -0
  32. package/src/services/account.service.ts +411 -0
  33. package/src/services/claude-usage.service.ts +186 -124
  34. package/src/services/db.service.ts +117 -3
  35. package/src/types/chat.ts +2 -0
  36. package/src/web/app.tsx +0 -4
  37. package/src/web/components/chat/chat-history-bar.tsx +3 -0
  38. package/src/web/components/chat/usage-badge.tsx +86 -12
  39. package/src/web/components/settings/accounts-settings-section.tsx +358 -0
  40. package/src/web/components/settings/settings-tab.tsx +11 -0
  41. package/src/web/components/ui/badge.tsx +36 -0
  42. package/src/web/components/ui/switch.tsx +27 -0
  43. package/src/web/hooks/use-usage.ts +1 -1
  44. package/src/web/lib/api-settings.ts +65 -0
  45. package/dist/web/assets/ai-settings-section-ByRvOONz.js +0 -1
  46. package/dist/web/assets/chat-tab-DLfy6CBX.js +0 -7
  47. package/dist/web/assets/index-4pPCbWJp.css +0 -2
  48. package/dist/web/assets/index-DaQYRomz.js +0 -29
  49. package/dist/web/assets/input-P_K5CUiy.js +0 -41
  50. package/dist/web/assets/keybindings-store-xe6f5O18.js +0 -1
  51. package/dist/web/assets/settings-tab-CHONXRsW.js +0 -1
  52. package/src/web/hooks/use-health-check.ts +0 -95
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"068774d7d82b60095e9c1965a99f8640","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-Bt1Lr3jH.js"},{"revision":null,"url":"assets/terminal-tab-CabMjIRO.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/table-C0oSLUYn.js"},{"revision":null,"url":"assets/tab-store-0CKk8cSr.js"},{"revision":null,"url":"assets/sqlite-viewer-VrZiiegZ.js"},{"revision":null,"url":"assets/settings-tab-CHONXRsW.js"},{"revision":null,"url":"assets/settings-store-2NQzaOVJ.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-CguN1z3q.js"},{"revision":null,"url":"assets/markdown-renderer-Xe_wjdJH.js"},{"revision":null,"url":"assets/keybindings-store-xe6f5O18.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-P_K5CUiy.js"},{"revision":null,"url":"assets/index-DaQYRomz.js"},{"revision":null,"url":"assets/index-4pPCbWJp.css"},{"revision":null,"url":"assets/git-graph-DK_yDfWe.js"},{"revision":null,"url":"assets/dist-D9RHR8A4.js"},{"revision":null,"url":"assets/diff-viewer-BG2UNjTZ.js"},{"revision":null,"url":"assets/database-viewer-Hso-EwQH.js"},{"revision":null,"url":"assets/columns-2-fz8yNaAo.js"},{"revision":null,"url":"assets/code-editor-1FNaZKfA.js"},{"revision":null,"url":"assets/chat-tab-DLfy6CBX.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":null,"url":"assets/ai-settings-section-ByRvOONz.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"6d2149567ee99cc4f626a83c2b317668","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-Bt1Lr3jH.js"},{"revision":null,"url":"assets/terminal-tab-qJxp0iOK.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/table-C0oSLUYn.js"},{"revision":null,"url":"assets/tab-store-0CKk8cSr.js"},{"revision":null,"url":"assets/sqlite-viewer-CfIer2x_.js"},{"revision":null,"url":"assets/settings-tab-3-ewawy0.js"},{"revision":null,"url":"assets/settings-store-2NQzaOVJ.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-B5-tRXE2.js"},{"revision":null,"url":"assets/markdown-renderer-DpIu7iOT.js"},{"revision":null,"url":"assets/keybindings-store-DrBQMVKg.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-CVIzrYsH.js"},{"revision":null,"url":"assets/index-sMxUHxFZ.css"},{"revision":null,"url":"assets/index-qElHXk-7.js"},{"revision":null,"url":"assets/git-graph-B9eaNltz.js"},{"revision":null,"url":"assets/dist-D9RHR8A4.js"},{"revision":null,"url":"assets/diff-viewer-CS-wesGq.js"},{"revision":null,"url":"assets/database-viewer-BJYmlnr2.js"},{"revision":null,"url":"assets/columns-2-fz8yNaAo.js"},{"revision":null,"url":"assets/code-editor-BbhIHbts.js"},{"revision":null,"url":"assets/chat-tab-R_8ZfOG8.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":null,"url":"assets/ai-settings-section-BxCMGg-I.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
@@ -1,6 +1,6 @@
1
1
  # PPM Codebase Summary
2
2
 
3
- Generated from codebase analysis of 133 TypeScript files, ~22K LOC.
3
+ Generated from codebase analysis of 135+ TypeScript files, ~22.5K LOC (including multi-account feature).
4
4
 
5
5
  ## Directory Structure
6
6
 
@@ -32,6 +32,7 @@ ppm/
32
32
  │ │ ├── routes/
33
33
  │ │ │ ├── settings.ts # GET/PUT /api/settings/ai (AI provider config)
34
34
  │ │ │ ├── projects.ts # GET/POST /api/projects, DELETE /:name
35
+ │ │ │ ├── accounts.ts # GET/POST/PUT/DELETE /api/accounts, POST activate
35
36
  │ │ │ ├── project-scoped.ts # Mount chat, git, files under /api/project/:name/*
36
37
  │ │ │ ├── chat.ts # GET/POST/DELETE sessions, GET messages, usage, slash-items
37
38
  │ │ │ ├── git.ts # GET status, diff, log, graph; POST commit, stage, discard
@@ -48,10 +49,12 @@ ppm/
48
49
  │ │ ├── claude-agent-sdk.ts # Primary: SDK integration, tool approval, Windows CLI fallback, .env poisoning mitigation
49
50
  │ │ ├── mock-provider.ts # Test provider (ignores config)
50
51
  │ │ └── registry.ts # ProviderRegistry (singleton, router to active provider)
51
- │ ├── services/ # Business logic (18 files, 3100 LOC)
52
+ │ ├── services/ # Business logic (20 files, 3300+ LOC)
52
53
  │ │ ├── chat.service.ts # Session lifecycle, message streaming
53
54
  │ │ ├── config.service.ts # Config loading (YAML→SQLite migration)
54
- │ │ ├── db.service.ts # SQLite persistence (schema v3, WAL mode, 8 tables, connection CRUD)
55
+ │ │ ├── db.service.ts # SQLite persistence (schema v5, WAL mode, 9 tables, connection/account CRUD)
56
+ │ │ ├── account.service.ts # Account CRUD, token encryption/decryption, active selection
57
+ │ │ ├── account-selector.service.ts # Select active account based on config
55
58
  │ │ ├── project.service.ts # Project CRUD, scanning, resolution
56
59
  │ │ ├── file.service.ts # File ops with path validation
57
60
  │ │ ├── git.service.ts # Git operations (status, diff, log, graph)
@@ -70,6 +73,9 @@ ppm/
70
73
  │ │ ├── postgres-adapter.ts # PostgreSQL connection, query execution
71
74
  │ │ ├── init-adapters.ts # Initialize adapters at server start
72
75
  │ │ └── readonly-check.ts # isReadOnlyQuery() safety regex (CTE-safe)
76
+ │ ├── lib/ # Shared utilities (2 files)
77
+ │ │ ├── account-crypto.ts # AES-256 encryption/decryption for API keys
78
+ │ │ └── network-utils.ts # Network utility helpers
73
79
  │ ├── types/ # TypeScript interfaces (7 files, 450 LOC)
74
80
  │ │ ├── api.ts # ApiResponse envelope, WebSocket message types
75
81
  │ │ ├── chat.ts # Session, Message, ChatEvent types
@@ -157,7 +163,10 @@ ppm/
157
163
  │ │ ├── connection-color-picker.tsx # WCAG contrast-aware color picker
158
164
  │ │ └── use-connections.ts # Hook for connection CRUD operations
159
165
  │ ├── projects/ # Project management (339 LOC, 2 files)
160
- │ ├── settings/ # Settings panel (theme + AI provider config UI)
166
+ │ ├── settings/ # Settings panel (theme + AI provider + accounts config UI)
167
+ │ │ ├── settings-tab.tsx # Main settings panel with tabs
168
+ │ │ ├── ai-settings-section.tsx # AI provider configuration
169
+ │ │ └── accounts-settings-section.tsx # Multi-account management (add, edit, delete, activate)
161
170
  │ ├── terminal/ # xterm.js wrapper (143 LOC, 2 files)
162
171
  │ ├── shared/ # Shared components (2 files)
163
172
  │ │ ├── markdown-renderer.tsx # Render Markdown with syntax highlighting
@@ -228,7 +237,9 @@ ppm/
228
237
  - **Services:**
229
238
  - **ChatService** — Session lifecycle, message queueing, streaming
230
239
  - **ConfigService** — Config loading (YAML→SQLite migration)
231
- - **DbService** — SQLite persistence (8 tables, WAL mode, schema v3, connection CRUD, table cache)
240
+ - **DbService** — SQLite persistence (9 tables, WAL mode, schema v5, connection/account CRUD, table cache)
241
+ - **AccountService** — Multi-account management, token encryption/decryption
242
+ - **AccountSelectorService** — Select active account based on config
232
243
  - **GitService** — Git commands via simple-git
233
244
  - **FileService** — File ops with path validation
234
245
  - **ProjectService** — Project CRUD, scanning, resolution
@@ -107,6 +107,12 @@ GET /api/health → Health check
107
107
  GET /api/auth/check → Verify auth token
108
108
  GET /api/settings/ai → Get AI provider settings
109
109
  PUT /api/settings/ai → Update AI provider settings
110
+ GET /api/accounts → List all accounts (sanitized)
111
+ POST /api/accounts → Create account (encrypt & store token)
112
+ GET /api/accounts/:id → Get account (sanitized, no token)
113
+ PUT /api/accounts/:id → Update account (name, priority)
114
+ DELETE /api/accounts/:id → Delete account
115
+ POST /api/accounts/:id/activate → Set as active account
110
116
  POST /api/projects → Create project
111
117
  GET /api/projects → List projects
112
118
  DELETE /api/projects/:name → Delete project
@@ -153,7 +159,7 @@ WS /ws/project/:name/terminal/:id → Terminal I/O
153
159
  |---------|---------|-------------|
154
160
  | **ChatService** | Session management, message streaming | createSession, streamMessage, getHistory |
155
161
  | **ConfigService** | Config loading (YAML→SQLite migration) | load, save, getToken |
156
- | **DbService** | SQLite persistence (8 tables, WAL, connections CRUD) | getDb, openTestDb, getConnections, insertConnection, updateConnection, deleteConnection, getTableCache |
162
+ | **DbService** | SQLite persistence (9 tables, WAL, connections/accounts CRUD) | getDb, openTestDb, getConnections, insertConnection, deleteConnection, getTableCache |
157
163
  | **TableCacheService** | Cache table metadata, search tables | syncTables, searchTables, invalidateCache |
158
164
  | **GitService** | Git command execution | status, diff, commit, stage, branch |
159
165
  | **FileService** | File operations with validation | read, write, tree, delete, mkdir |
@@ -168,6 +174,8 @@ WS /ws/project/:name/terminal/:id → Terminal I/O
168
174
  | **DatabaseAdapterRegistry** | Register/retrieve DB adapters (extensible) | registerAdapter, getAdapter |
169
175
  | **SQLiteAdapter** | SQLite connection, query execution, readonly checks | testConnection, getTables, getTableSchema, getTableData, executeQuery, updateCell |
170
176
  | **PostgresAdapter** | PostgreSQL connection, query execution, readonly checks | testConnection, getTables, getTableSchema, getTableData, executeQuery, updateCell |
177
+ | **AccountService** | Account CRUD, token encryption/decryption | getAccounts, createAccount, updateAccount, deleteAccount |
178
+ | **AccountSelectorService** | Select active account based on config | getActiveAccount, setActiveAccount, selectByProject |
171
179
 
172
180
  **Key Files:** `src/services/*.service.ts`
173
181
 
@@ -192,7 +200,7 @@ interface AIProvider {
192
200
  ```
193
201
 
194
202
  **Implementations:**
195
- - **claude-agent-sdk** (Primary) — @anthropic-ai/claude-agent-sdk, streaming, tool use. Reads model/effort/maxTurns/budget/thinking from config. Settings refreshed per query. Windows CLI fallback for Bun subprocess pipe issues. .env poisoning mitigation.
203
+ - **claude-agent-sdk** (Primary) — @anthropic-ai/claude-agent-sdk, streaming, tool use. Reads model/effort/maxTurns/budget/thinking from config. Settings refreshed per query. Windows CLI fallback for Bun subprocess pipe issues. .env poisoning mitigation. **Multi-account support:** Injects account API token from AccountService instead of relying on ANTHROPIC_API_KEY env var when accounts configured.
196
204
  - **mock-provider** (Testing) — Returns canned responses
197
205
  - **Note:** CLI provider removed (v2); agent SDK is sole AI provider with Windows CLI fallback
198
206
 
@@ -716,6 +724,16 @@ UI displays results (read-only highlight if mutation was blocked)
716
724
 
717
725
  **SQLite Schema** (in `~/.ppm/ppm.db`):
718
726
  ```sql
727
+ CREATE TABLE accounts (
728
+ id TEXT PRIMARY KEY,
729
+ account_name TEXT NOT NULL,
730
+ encrypted_api_key TEXT NOT NULL,
731
+ priority INTEGER DEFAULT 0,
732
+ is_active INTEGER DEFAULT 0, -- 1 = active, 0 = inactive
733
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
734
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
735
+ );
736
+
719
737
  CREATE TABLE connections (
720
738
  id INTEGER PRIMARY KEY AUTOINCREMENT,
721
739
  type TEXT NOT NULL, -- 'sqlite' | 'postgres'
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
+ "author": "hienlh",
6
+ "license": "MIT",
5
7
  "module": "src/index.ts",
6
8
  "type": "module",
7
9
  "bin": {
@@ -40,6 +42,7 @@
40
42
  "@codemirror/lang-sql": "^6.10.0",
41
43
  "@inquirer/prompts": "^8.3.0",
42
44
  "@monaco-editor/react": "^4.7.0",
45
+ "@radix-ui/react-switch": "^1.2.6",
43
46
  "@tanstack/react-table": "^8.21.3",
44
47
  "@uiw/react-codemirror": "^4.25.8",
45
48
  "@xterm/addon-fit": "^0.11.0",
@@ -0,0 +1,53 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const ALGO = "aes-256-gcm";
7
+
8
+ let keyPath = resolve(homedir(), ".ppm", "account.key");
9
+
10
+ /** Override key path (for tests) */
11
+ export function setKeyPath(path: string): void {
12
+ keyPath = path;
13
+ _key = null; // invalidate cached key
14
+ }
15
+
16
+ function loadOrCreateKey(): Buffer {
17
+ if (existsSync(keyPath)) {
18
+ return Buffer.from(readFileSync(keyPath, "utf-8").trim(), "hex");
19
+ }
20
+ const key = randomBytes(32);
21
+ mkdirSync(resolve(keyPath, ".."), { recursive: true });
22
+ writeFileSync(keyPath, key.toString("hex"), { mode: 0o600 });
23
+ return key;
24
+ }
25
+
26
+ let _key: Buffer | null = null;
27
+
28
+ function getKey(): Buffer {
29
+ if (!_key) _key = loadOrCreateKey();
30
+ return _key;
31
+ }
32
+
33
+ /** Encrypt plaintext → "iv:authTag:ciphertext" (hex) */
34
+ export function encrypt(plaintext: string): string {
35
+ const iv = randomBytes(12);
36
+ const cipher = createCipheriv(ALGO, getKey(), iv);
37
+ const enc = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
38
+ const tag = cipher.getAuthTag();
39
+ return `${iv.toString("hex")}:${tag.toString("hex")}:${enc.toString("hex")}`;
40
+ }
41
+
42
+ /** Decrypt "iv:authTag:ciphertext" → plaintext */
43
+ export function decrypt(encoded: string): string {
44
+ const parts = encoded.split(":");
45
+ if (parts.length !== 3) throw new Error("Invalid encrypted format");
46
+ const [ivHex, tagHex, ctHex] = parts;
47
+ const decipher = createDecipheriv(ALGO, getKey(), Buffer.from(ivHex, "hex"));
48
+ decipher.setAuthTag(Buffer.from(tagHex, "hex"));
49
+ return Buffer.concat([
50
+ decipher.update(Buffer.from(ctHex, "hex")),
51
+ decipher.final(),
52
+ ]).toString("utf-8");
53
+ }
@@ -14,6 +14,8 @@ import type {
14
14
  import { configService } from "../services/config.service.ts";
15
15
  import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
16
16
  import { getSessionMapping, setSessionMapping } from "../services/db.service.ts";
17
+ import { accountSelector } from "../services/account-selector.service.ts";
18
+ import { accountService } from "../services/account.service.ts";
17
19
  import { resolve } from "node:path";
18
20
  import { homedir } from "node:os";
19
21
  import { readFileSync, existsSync } from "node:fs";
@@ -68,6 +70,49 @@ export class ClaudeAgentSdkProvider implements AIProvider {
68
70
  } catch { return {}; }
69
71
  }
70
72
 
73
+ /**
74
+ * Build env for SDK query.
75
+ * If account mode: detect token type and inject the correct env var.
76
+ * - OAuth tokens (sk-ant-oat*) → CLAUDE_CODE_OAUTH_TOKEN (SDK reads this for subscription auth)
77
+ * - API keys (sk-ant-api* or other) → ANTHROPIC_API_KEY
78
+ * Otherwise: pass through existing env (backward compatible).
79
+ */
80
+ private buildQueryEnv(
81
+ projectPath: string | undefined,
82
+ account: { id: string; accessToken: string } | null,
83
+ ): Record<string, string | undefined> {
84
+ const base = { ...process.env, ...this.getProjectEnvOverrides(projectPath) };
85
+ if (!account) return base;
86
+ const isOAuthToken = account.accessToken.startsWith("sk-ant-oat");
87
+ if (isOAuthToken) {
88
+ return {
89
+ ...base,
90
+ CLAUDE_CODE_OAUTH_TOKEN: account.accessToken,
91
+ ANTHROPIC_API_KEY: "", // neutralize — OAuth token takes precedence
92
+ };
93
+ }
94
+ return {
95
+ ...base,
96
+ ANTHROPIC_API_KEY: account.accessToken,
97
+ CLAUDE_CODE_OAUTH_TOKEN: "", // neutralize — API key takes precedence
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Parse SDK result event to detect 429 or 401.
103
+ * Only detects pre-stream errors (result event on first response).
104
+ */
105
+ private detectResultErrorCode(event: unknown): 429 | 401 | null {
106
+ if (!event || typeof event !== "object") return null;
107
+ const e = event as Record<string, unknown>;
108
+ if (e.type === "result" && e.subtype === "error_during_execution") {
109
+ const msg = String(e.error ?? e.error_message ?? e.message ?? "");
110
+ if (msg.includes("429") || msg.toLowerCase().includes("rate limit") || msg.toLowerCase().includes("overloaded")) return 429;
111
+ if (msg.includes("401") || msg.toLowerCase().includes("unauthorized") || msg.toLowerCase().includes("invalid api key")) return 401;
112
+ }
113
+ return null;
114
+ }
115
+
71
116
  /**
72
117
  * Direct CLI fallback for Windows — spawns `claude -p` with stream-json output.
73
118
  * Workaround for Bun + Windows SDK subprocess pipe buffering issue.
@@ -432,8 +477,15 @@ export class ClaudeAgentSdkProvider implements AIProvider {
432
477
  // Fallback cwd: SDK needs a valid working directory even when no project is selected.
433
478
  // On Windows daemons, undefined cwd can cause the subprocess to fail silently.
434
479
  const effectiveCwd = meta.projectPath || homedir();
435
- const queryEnv = { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) };
436
- console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform}`);
480
+
481
+ // Account-based auth injection (multi-account mode)
482
+ // Fallback to existing env (ANTHROPIC_API_KEY) when no accounts configured.
483
+ const account = accountSelector.isEnabled() ? accountSelector.next() : null;
484
+ if (account) {
485
+ console.log(`[sdk] Using account ${account.id} (${account.email ?? "no-email"})`);
486
+ }
487
+ const queryEnv = this.buildQueryEnv(meta.projectPath, account);
488
+ console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform} accountMode=${!!account}`);
437
489
 
438
490
  // TODO: Remove when TS SDK fixes Windows stdin pipe buffering (see queryDirectCli() JSDoc for tracking issues)
439
491
  // On Windows, SDK query() hangs because Bun subprocess stdin pipe never flushes to child process.
@@ -719,6 +771,27 @@ export class ClaudeAgentSdkProvider implements AIProvider {
719
771
  }
720
772
 
721
773
  if (msg.type === "result") {
774
+ // Account error detection — only act on pre-stream 429/401
775
+ if (account) {
776
+ const errCode = this.detectResultErrorCode(msg);
777
+ if (errCode === 429) {
778
+ accountSelector.onRateLimit(account.id);
779
+ // Post-stream 429 already has content — surface error to user
780
+ yield { type: "error", message: "Rate limited. This account is now on cooldown. Please retry." };
781
+ break;
782
+ } else if (errCode === 401) {
783
+ // Try refresh once
784
+ try {
785
+ await accountService.refreshAccessToken(account.id);
786
+ console.log(`[sdk] 401 on account ${account.id} — token refreshed`);
787
+ } catch {
788
+ accountSelector.onAuthError(account.id);
789
+ }
790
+ } else {
791
+ accountSelector.onSuccess(account.id);
792
+ }
793
+ }
794
+
722
795
  // Flush any remaining pending tool_results before finishing
723
796
  if (pendingToolCount > 0) {
724
797
  try {
@@ -814,7 +887,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
814
887
  try {
815
888
  const providerConfig = this.getProviderConfig();
816
889
  const effectiveCwd = meta.projectPath || homedir();
817
- const queryEnv = { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) };
890
+ const retryAccount = accountSelector.isEnabled() ? accountSelector.next() : null;
891
+ const queryEnv = this.buildQueryEnv(meta.projectPath, retryAccount);
818
892
  const retryQuery = query({
819
893
  prompt: message,
820
894
  options: {
@@ -12,6 +12,7 @@ import { projectScopedRouter } from "./routes/project-scoped.ts";
12
12
  import { postgresRoutes } from "./routes/postgres.ts";
13
13
  import { databaseRoutes } from "./routes/database.ts";
14
14
  import { fsBrowseRoutes } from "./routes/fs-browse.ts";
15
+ import { accountsRoutes } from "./routes/accounts.ts";
15
16
  import { initAdapters } from "../services/database/init-adapters.ts";
16
17
  import { terminalWebSocket } from "./ws/terminal.ts";
17
18
  import { chatWebSocket } from "./ws/chat.ts";
@@ -115,6 +116,7 @@ app.route("/api/projects", projectRoutes);
115
116
  app.route("/api/project/:projectName", projectScopedRouter);
116
117
  app.route("/api/postgres", postgresRoutes);
117
118
  app.route("/api/db", databaseRoutes);
119
+ app.route("/api/accounts", accountsRoutes);
118
120
 
119
121
  // Static files / SPA fallback (non-API routes)
120
122
  app.route("/", staticRoutes);
@@ -378,6 +380,9 @@ export async function startServer(options: {
378
380
  // Start background usage polling
379
381
  import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
380
382
 
383
+ // Start background account token refresh
384
+ import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
385
+
381
386
  console.log(`\n PPM ready\n`);
382
387
  console.log(` ➜ Local: http://localhost:${server.port}/`);
383
388
 
@@ -519,5 +524,8 @@ if (process.argv.includes("__serve__")) {
519
524
  } as Parameters<typeof Bun.serve>[0] extends { websocket?: infer W } ? W : never,
520
525
  });
521
526
 
527
+ // Start background account token refresh in daemon child
528
+ import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
529
+
522
530
  console.log(`Server child ready on port ${port}`);
523
531
  }
@@ -0,0 +1,165 @@
1
+ import { Hono } from "hono";
2
+ import type { Context } from "hono";
3
+ import { accountService } from "../../services/account.service.ts";
4
+ import { accountSelector } from "../../services/account-selector.service.ts";
5
+ import { getAllAccountUsages, getUsageForAccount } from "../../services/claude-usage.service.ts";
6
+ import { ok, err } from "../../types/api.ts";
7
+
8
+ export const accountsRoutes = new Hono();
9
+
10
+ function getCallbackUrl(c: Context): string {
11
+ const url = new URL(c.req.url);
12
+ return `${url.protocol}//${url.host}/api/accounts/oauth/callback`;
13
+ }
14
+
15
+ function getUiBase(c: Context): string {
16
+ const url = new URL(c.req.url);
17
+ return `${url.protocol}//${url.host}`;
18
+ }
19
+
20
+ /** GET /api/accounts */
21
+ accountsRoutes.get("/", (c) => {
22
+ return c.json(ok(accountService.list()));
23
+ });
24
+
25
+ /** GET /api/accounts/active — which account will be used next */
26
+ accountsRoutes.get("/active", (c) => {
27
+ const lastId = accountSelector.lastPickedId;
28
+ if (!lastId) {
29
+ // No account picked yet — peek at what next() would return without consuming it
30
+ const accounts = accountService.list().filter((a) => a.status === "active");
31
+ if (accounts.length === 0) return c.json(ok(null));
32
+ return c.json(ok(accounts[0]));
33
+ }
34
+ const account = accountService.list().find((a) => a.id === lastId) ?? null;
35
+ return c.json(ok(account));
36
+ });
37
+
38
+ /** GET /api/accounts/settings */
39
+ accountsRoutes.get("/settings", (c) => {
40
+ return c.json(ok({
41
+ strategy: accountSelector.getStrategy(),
42
+ maxRetry: accountSelector.getMaxRetry(),
43
+ activeCount: accountSelector.activeCount(),
44
+ }));
45
+ });
46
+
47
+ /** PUT /api/accounts/settings */
48
+ accountsRoutes.put("/settings", async (c) => {
49
+ const body = await c.req.json<{ strategy?: string; maxRetry?: number }>();
50
+ if (body.strategy !== undefined) {
51
+ if (!["round-robin", "fill-first"].includes(body.strategy)) {
52
+ return c.json(err("strategy must be round-robin or fill-first"), 400);
53
+ }
54
+ accountSelector.setStrategy(body.strategy as "round-robin" | "fill-first");
55
+ }
56
+ if (body.maxRetry !== undefined) {
57
+ if (!Number.isInteger(body.maxRetry) || body.maxRetry < 0) {
58
+ return c.json(err("maxRetry must be a non-negative integer"), 400);
59
+ }
60
+ accountSelector.setMaxRetry(body.maxRetry);
61
+ }
62
+ return c.json(ok({
63
+ strategy: accountSelector.getStrategy(),
64
+ maxRetry: accountSelector.getMaxRetry(),
65
+ activeCount: accountSelector.activeCount(),
66
+ }));
67
+ });
68
+
69
+ /** POST /api/accounts — add account manually with API key */
70
+ accountsRoutes.post("/", async (c) => {
71
+ const body = await c.req.json<{ apiKey: string; label?: string }>();
72
+ if (!body.apiKey || typeof body.apiKey !== "string") {
73
+ return c.json(err("apiKey is required"), 400);
74
+ }
75
+ try {
76
+ const account = await accountService.addManual({
77
+ apiKey: body.apiKey.trim(),
78
+ label: body.label?.trim() || null,
79
+ });
80
+ return c.json(ok(account));
81
+ } catch (e) {
82
+ return c.json(err((e as Error).message), 400);
83
+ }
84
+ });
85
+
86
+ /** GET /api/accounts/oauth/start → redirect to Claude OAuth */
87
+ accountsRoutes.get("/oauth/start", (c) => {
88
+ const url = accountService.startOAuthFlow(getCallbackUrl(c));
89
+ return c.redirect(url);
90
+ });
91
+
92
+ /** GET /api/accounts/oauth/callback — exchange code for tokens */
93
+ accountsRoutes.get("/oauth/callback", async (c) => {
94
+ const { code, state, error } = c.req.query();
95
+ const successRedirect = `${getUiBase(c)}/#/settings/accounts`;
96
+
97
+ if (error || !code || !state) {
98
+ return c.redirect(`${successRedirect}?error=${encodeURIComponent(error ?? "missing_params")}`);
99
+ }
100
+ try {
101
+ await accountService.completeOAuthFlow(code, state, getCallbackUrl(c));
102
+ return c.redirect(`${successRedirect}?success=1`);
103
+ } catch (e) {
104
+ return c.redirect(`${successRedirect}?error=${encodeURIComponent((e as Error).message)}`);
105
+ }
106
+ });
107
+
108
+ /** POST /api/accounts/oauth/refresh/:id */
109
+ accountsRoutes.post("/oauth/refresh/:id", async (c) => {
110
+ const { id } = c.req.param();
111
+ try {
112
+ await accountService.refreshAccessToken(id);
113
+ return c.json(ok({ refreshed: true }));
114
+ } catch (e) {
115
+ return c.json(err((e as Error).message), 400);
116
+ }
117
+ });
118
+
119
+ /** GET /api/accounts/export — download encrypted accounts backup */
120
+ accountsRoutes.get("/export", (c) => {
121
+ const blob = accountService.exportEncrypted();
122
+ c.header("Content-Disposition", "attachment; filename=ppm-accounts-backup.json");
123
+ c.header("Content-Type", "application/json");
124
+ return c.body(blob);
125
+ });
126
+
127
+ /** POST /api/accounts/import — restore accounts from backup */
128
+ accountsRoutes.post("/import", async (c) => {
129
+ try {
130
+ const body = await c.req.text();
131
+ const count = accountService.importEncrypted(body);
132
+ return c.json(ok({ imported: count }));
133
+ } catch (e) {
134
+ return c.json(err((e as Error).message), 400);
135
+ }
136
+ });
137
+
138
+ /** GET /api/accounts/usage — all accounts usage batch */
139
+ accountsRoutes.get("/usage", (c) => {
140
+ return c.json(ok(getAllAccountUsages()));
141
+ });
142
+
143
+ /** GET /api/accounts/:id/usage — single account usage */
144
+ accountsRoutes.get("/:id/usage", (c) => {
145
+ const { id } = c.req.param();
146
+ return c.json(ok(getUsageForAccount(id)));
147
+ });
148
+
149
+ /** DELETE /api/accounts/:id */
150
+ accountsRoutes.delete("/:id", (c) => {
151
+ const { id } = c.req.param();
152
+ accountService.remove(id);
153
+ return c.json(ok({ deleted: true }));
154
+ });
155
+
156
+ /** PATCH /api/accounts/:id — { status: "active" | "disabled" } */
157
+ accountsRoutes.patch("/:id", async (c) => {
158
+ const { id } = c.req.param();
159
+ const body = await c.req.json<{ status?: string }>();
160
+ if (body.status === "disabled") accountService.setDisabled(id);
161
+ else if (body.status === "active") accountService.setEnabled(id);
162
+ else return c.json(err("status must be active or disabled"), 400);
163
+ const account = accountService.list().find((a) => a.id === id) ?? null;
164
+ return c.json(ok(account));
165
+ });
@@ -43,6 +43,8 @@ chatRoutes.get("/usage", async (c) => {
43
43
  weeklyOpus: usage.weeklyOpus,
44
44
  weeklySonnet: usage.weeklySonnet,
45
45
  totalCostUsd: usage.totalCostUsd,
46
+ activeAccountId: usage.activeAccountId,
47
+ activeAccountLabel: usage.activeAccountLabel,
46
48
  }));
47
49
  });
48
50