@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.
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +46 -0
- package/LICENSE +21 -0
- package/README.md +34 -1
- package/bun.lock +1 -0
- package/dist/web/assets/ai-settings-section-BxCMGg-I.js +1 -0
- package/dist/web/assets/chat-tab-R_8ZfOG8.js +7 -0
- package/dist/web/assets/{code-editor-1FNaZKfA.js → code-editor-BbhIHbts.js} +1 -1
- package/dist/web/assets/{database-viewer-Hso-EwQH.js → database-viewer-BJYmlnr2.js} +1 -1
- package/dist/web/assets/{diff-viewer-BG2UNjTZ.js → diff-viewer-CS-wesGq.js} +1 -1
- package/dist/web/assets/{git-graph-DK_yDfWe.js → git-graph-B9eaNltz.js} +1 -1
- package/dist/web/assets/index-qElHXk-7.js +28 -0
- package/dist/web/assets/index-sMxUHxFZ.css +2 -0
- package/dist/web/assets/input-CVIzrYsH.js +41 -0
- package/dist/web/assets/keybindings-store-DrBQMVKg.js +1 -0
- package/dist/web/assets/{markdown-renderer-Xe_wjdJH.js → markdown-renderer-DpIu7iOT.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CguN1z3q.js → postgres-viewer-B5-tRXE2.js} +1 -1
- package/dist/web/assets/settings-tab-3-ewawy0.js +1 -0
- package/dist/web/assets/{sqlite-viewer-VrZiiegZ.js → sqlite-viewer-CfIer2x_.js} +1 -1
- package/dist/web/assets/{terminal-tab-CabMjIRO.js → terminal-tab-qJxp0iOK.js} +2 -2
- package/dist/web/index.html +4 -4
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +16 -5
- package/docs/system-architecture.md +20 -2
- package/package.json +4 -1
- package/src/lib/account-crypto.ts +53 -0
- package/src/providers/claude-agent-sdk.ts +77 -3
- package/src/server/index.ts +8 -0
- package/src/server/routes/accounts.ts +165 -0
- package/src/server/routes/chat.ts +2 -0
- package/src/services/account-selector.service.ts +109 -0
- package/src/services/account.service.ts +411 -0
- package/src/services/claude-usage.service.ts +186 -124
- package/src/services/db.service.ts +117 -3
- package/src/types/chat.ts +2 -0
- package/src/web/app.tsx +0 -4
- package/src/web/components/chat/chat-history-bar.tsx +3 -0
- package/src/web/components/chat/usage-badge.tsx +86 -12
- package/src/web/components/settings/accounts-settings-section.tsx +358 -0
- package/src/web/components/settings/settings-tab.tsx +11 -0
- package/src/web/components/ui/badge.tsx +36 -0
- package/src/web/components/ui/switch.tsx +27 -0
- package/src/web/hooks/use-usage.ts +1 -1
- package/src/web/lib/api-settings.ts +65 -0
- package/dist/web/assets/ai-settings-section-ByRvOONz.js +0 -1
- package/dist/web/assets/chat-tab-DLfy6CBX.js +0 -7
- package/dist/web/assets/index-4pPCbWJp.css +0 -2
- package/dist/web/assets/index-DaQYRomz.js +0 -29
- package/dist/web/assets/input-P_K5CUiy.js +0 -41
- package/dist/web/assets/keybindings-store-xe6f5O18.js +0 -1
- package/dist/web/assets/settings-tab-CHONXRsW.js +0 -1
- 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||`/`)}))});
|
package/docs/codebase-summary.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PPM Codebase Summary
|
|
2
2
|
|
|
3
|
-
Generated from codebase analysis of
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
436
|
-
|
|
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
|
|
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: {
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
+
});
|