@hydra-acp/archiver 0.1.19 → 0.1.20
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/dist/archive-loop.js +1 -1
- package/dist/bridge.js +1 -1
- package/dist/cold-sweep.js +1 -1
- package/dist/discovery.js +1 -1
- package/dist/index.js +2 -2
- package/dist/rule.js +1 -1
- package/package.json +1 -1
package/dist/archive-loop.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hostname as p,userInfo as
|
|
1
|
+
import{hostname as p,userInfo as c}from"node:os";import{hashBundle as h,keyFor as u,serialize as d,wrap as g}from"./envelope.js";import{logger as m}from"./util/log.js";const n=m("archive");class b{constructor(e){this.opts=e;this.host=e.host??v()}opts;host;pending=new Map;meta=new Map;stopped=!1;setMeta(e,t){this.meta.set(e,t)}forgetSession(e){const t=this.pending.get(e);t&&(clearTimeout(t.timer),this.pending.delete(e)),this.meta.delete(e)}async finalFlush(e){const t=this.pending.get(e);t&&(clearTimeout(t.timer),this.pending.delete(e));try{await this.flush(e)}catch(i){n.warn(`final flush ${e} failed: ${i.message}`)}this.meta.delete(e)}markDirty(e){if(this.stopped)return;const t=this.pending.get(e);t&&clearTimeout(t.timer);const i=setTimeout(()=>{this.pending.delete(e),this.flush(e).catch(r=>{n.warn(`flush ${e} failed: ${r.message}`)})},this.opts.debounceMs);i.unref(),this.pending.set(e,{timer:i})}stop(){this.stopped=!0;for(const e of this.pending.values())clearTimeout(e.timer);this.pending.clear()}async flushNow(e){const t=this.pending.get(e);t&&(clearTimeout(t.timer),this.pending.delete(e)),await this.flush(e)}async flush(e){const t=await this.opts.daemon.exportSession(e),i=t.session.lineageId;if(!i){n.warn(`session ${e} export had no lineageId; skipping`);return}const r=this.meta.get(e),l=this.opts.getRule();if(!await f(l,{sessionId:e,lineageId:i,meta:r??{}})){n.debug(`rule skipped session ${e} (lineage ${i})`);return}const s=h(t);if(this.opts.state.get(i).lastUploadedHash===s){n.debug(`unchanged content for lineage ${i} \u2014 skip upload`);return}const a=g(t,i,this.host);await this.opts.backend.put(u(i),d(a)),await this.opts.state.set(i,{lastUploadedHash:s,lastUploadedAt:a.uploadedAt}),n.info(`uploaded lineage=${i} session=${e} hash=${s.slice(0,22)}\u2026`)}}async function f(o,e){try{return await o(e)!==!1}catch(t){return n.warn(`rule threw for ${e.sessionId}: ${t.message}; defaulting to archive`),!0}}function v(){let o="unknown";try{o=c().username}catch{}return{host:p(),user:o}}export{b as ArchiveLoop};
|
package/dist/bridge.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AcpAttach as
|
|
1
|
+
import{AcpAttach as d}from"./acp/attach.js";import{logger as c}from"./util/log.js";const a=c("bridge");class u{constructor(t){this.opts=t;this.meta=t.meta,this.attach=new d({sessionId:t.sessionId,daemonWsUrl:t.daemonWsUrl,token:t.token}),this.opts.archive.setMeta(t.sessionId,this.meta)}opts;attach;meta;stopped=!1;start(){this.attach.on("notification",t=>this.onNotification(t)),this.attach.on("request",t=>this.onRequest(t)),this.attach.on("error",t=>{a.warn(`attach error ${this.opts.sessionId}: ${t.message}`)}),this.attach.start()}stop(){this.stopped||(this.stopped=!0,this.opts.archive.finalFlush(this.opts.sessionId),this.attach.stop())}updateMeta(t){this.meta=t,this.opts.archive.setMeta(this.opts.sessionId,t)}onNotification(t){if(t.method!=="session/update")return;const i=(t.params??{}).update??{},s=typeof i.sessionUpdate=="string"?i.sessionUpdate:"";s==="session_info_update"&&this.applySessionInfoUpdate(i),s==="turn_complete"&&this.opts.archive.markDirty(this.opts.sessionId)}applySessionInfoUpdate(t){const e={...this.meta};let i=!1;typeof t.title=="string"&&e.title!==t.title&&(e.title=t.title,i=!0);const s=p(t._meta),o=typeof s?.agentId=="string"?s.agentId:void 0;o!==void 0&&e.agentId!==o&&(e.agentId=o,i=!0);const r=typeof s?.interactive=="boolean"?s.interactive:void 0;r!==void 0&&e.interactive!==r&&(e.interactive=r,i=!0),i&&this.updateMeta(e)}onRequest(t){a.debug(`ignoring inbound request ${t.method} id=${String(t.id)}`)}}function p(n){if(!n||typeof n!="object"||Array.isArray(n))return;const t=n["hydra-acp"];if(!(!t||typeof t!="object"||Array.isArray(t)))return t}export{u as ArchiverBridge};
|
package/dist/cold-sweep.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as d}from"./util/log.js";const
|
|
1
|
+
import{logger as d}from"./util/log.js";const r=d("cold-sweep");async function l(n){const o=await a(n.daemonUrl,n.token);let s=0,i=0;for(const e of o)if(e.status!=="live"){if(e.importedFromMachine&&!e.upstreamSessionId){i+=1;continue}s+=1,n.archive.setMeta(e.sessionId,{...e.cwd!==void 0?{cwd:e.cwd}:{},...e.agentId!==void 0?{agentId:e.agentId}:{},...e.title!==void 0?{title:e.title}:{},...e.interactive!==void 0?{interactive:e.interactive}:{}});try{await n.archive.flushNow(e.sessionId)}catch(t){r.warn(`cold sweep flush ${e.sessionId} failed: ${t.message}`)}}return r.info(`cold sweep done: scanned=${o.length} cold=${s} skipped-mirrors=${i}`),{scanned:o.length,cold:s,skippedMirrors:i}}async function a(n,o){const s=await fetch(`${n}/v1/sessions`,{headers:{Authorization:`Bearer ${o}`}});if(!s.ok)throw new Error(`daemon /v1/sessions returned ${s.status}`);return(await s.json()).sessions}export{l as runColdSweep};
|
package/dist/discovery.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as d}from"./util/log.js";const o=d("discovery"),r=2e3;class h{constructor(
|
|
1
|
+
import{logger as d}from"./util/log.js";const o=d("discovery"),r=2e3;class h{constructor(e){this.opts=e}opts;timer;known=new Map;stopped=!1;inFlight=!1;start(){o.info(`polling ${this.opts.daemonUrl}/v1/sessions every ${this.opts.pollIntervalMs??r}ms`),this.poll(),this.timer=setInterval(()=>{this.poll()},this.opts.pollIntervalMs??r)}stop(){this.stopped=!0,this.timer&&(clearInterval(this.timer),this.timer=void 0)}async poll(){if(!(this.stopped||this.inFlight)){this.inFlight=!0;try{const e=await fetch(`${this.opts.daemonUrl}/v1/sessions`,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!e.ok){o.warn(`daemon /v1/sessions returned ${e.status}`);return}const n=await e.json(),i=new Map;for(const s of n.sessions)s.status==="live"&&i.set(s.sessionId,s);for(const[s,t]of i)if(this.known.has(s))this.known.set(s,t);else{this.known.set(s,t);try{this.opts.onAdd(t)}catch(a){o.warn(`onAdd error for ${s}: ${a.message}`)}}for(const s of[...this.known.keys()])if(!i.has(s)){this.known.delete(s);try{this.opts.onRemove(s)}catch(t){o.warn(`onRemove error for ${s}: ${t.message}`)}}}catch(e){o.debug(`poll error: ${e.message}`)}finally{this.inFlight=!1}}}}export{h as HydraDiscovery};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{createHash as x}from"node:crypto";import{readFileSync as P}from"node:fs";import{userInfo as w}from"node:os";import{dirname as S,resolve as U}from"node:path";import{fileURLToPath as R}from"node:url";import{ArchiveLoop as C}from"./archive-loop.js";import{EncryptedBackend as E}from"./backend/encrypted.js";import{makeBackend as A}from"./backend/factory.js";import{ArchiverBridge as T}from"./bridge.js";import{runColdSweep as G}from"./cold-sweep.js";import{loadConfig as D,loadEncryptionKey as L,loadLoginConfig as B}from"./config.js";import{DaemonClient as M}from"./daemon.js";import{HydraDiscovery as H}from"./discovery.js";import{lineageFromKey as F}from"./envelope.js";import{runKeygen as K}from"./keygen.js";import{runGoogleLogin as _}from"./oauth/google.js";import{PullLoop as z}from"./pull-loop.js";import{DEFAULT_RULE as j,loadRule as y}from"./rule.js";import{SyncState as N}from"./state.js";import{logger as O,setDebug as v}from"./util/log.js";const
|
|
2
|
+
import{createHash as x}from"node:crypto";import{readFileSync as P}from"node:fs";import{userInfo as w}from"node:os";import{dirname as S,resolve as U}from"node:path";import{fileURLToPath as R}from"node:url";import{ArchiveLoop as C}from"./archive-loop.js";import{EncryptedBackend as E}from"./backend/encrypted.js";import{makeBackend as A}from"./backend/factory.js";import{ArchiverBridge as T}from"./bridge.js";import{runColdSweep as G}from"./cold-sweep.js";import{loadConfig as D,loadEncryptionKey as L,loadLoginConfig as B}from"./config.js";import{DaemonClient as M}from"./daemon.js";import{HydraDiscovery as H}from"./discovery.js";import{lineageFromKey as F}from"./envelope.js";import{runKeygen as K}from"./keygen.js";import{runGoogleLogin as _}from"./oauth/google.js";import{PullLoop as z}from"./pull-loop.js";import{DEFAULT_RULE as j,loadRule as y}from"./rule.js";import{SyncState as N}from"./state.js";import{logger as O,setDebug as v}from"./util/log.js";const o=O("main"),k=`usage: hydra-acp-archiver [<command>]
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
5
|
setup Interactive first-run wizard \u2014 picks a backend, sets up
|
|
@@ -15,7 +15,7 @@ Commands:
|
|
|
15
15
|
Flags:
|
|
16
16
|
--version, -v Print version and exit.
|
|
17
17
|
--help, -h Show this message.
|
|
18
|
-
`;function b(){try{const e=S(R(import.meta.url));return JSON.parse(P(U(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}const Y=new Set(["1","true","yes","on","t"]);async function V(){v(Y.has((process.env.DEBUG??"").toLowerCase()));const e=B();
|
|
18
|
+
`;function b(){try{const e=S(R(import.meta.url));return JSON.parse(P(U(e,"../package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}const Y=new Set(["1","true","yes","on","t"]);async function V(){v(Y.has((process.env.DEBUG??"").toLowerCase()));const e=B();o.info(`token=${e.tokenPath}`),await _({tokenPath:e.tokenPath}),o.info("login complete \u2014 you can now start the archiver extension")}async function W(){const e=D();v(e.debug);const n=await L(e.encryptionKeyPath);e.prefix===""&&(n!==void 0?(e.prefix=x("sha256").update(n).digest().subarray(0,8).toString("hex")+"/",o.info(`auto-prefix (key fingerprint): ${e.prefix}`)):(e.prefix=w().username.toLowerCase().replace(/[^a-z0-9-]/g,"-")+"/",o.info(`auto-prefix (username): ${e.prefix}`)));const a=new N(e.statePath);await a.load(b(),e.prefix,e.backend);function p(r){const t=A({...e,prefix:r});return n!==void 0?new E(t,n):t}const s=p(e.prefix),l=p(e.prefix+e.hostId+"/");await s.init(),await l.init();try{const r=await s.list(),t=new Set;for(const $ of r){const g=F($.key);g!==void 0&&t.add(g)}const I=await a.reconcile(t);o.info(`reconciled state with backend: present=${t.size} pruned=${I}`)}catch(r){o.warn(`state reconcile skipped \u2014 backend list failed: ${r.message}`)}const u=new M({daemonUrl:e.hydraDaemonUrl,token:e.hydraToken,toolContent:e.toolContent});let c=j;c=await y(e.ruleConfigPath);const d=new C({daemon:u,backend:l,state:a,getRule:()=>c,debounceMs:e.uploadDebounceMs,host:{host:e.hostId,user:w().username}}),m=new z({daemon:u,backend:s,state:a,intervalMs:e.pullIntervalMs,hostId:e.hostId});m.start(),G({daemonUrl:e.hydraDaemonUrl,token:e.hydraToken,archive:d}).catch(r=>{o.warn(`cold sweep failed: ${r.message}`)});const i=new Map,f=new H({daemonUrl:e.hydraDaemonUrl,token:e.hydraToken,pollIntervalMs:e.hydraPollIntervalMs,onAdd:r=>{if(i.has(r.sessionId))return;o.info(`attaching to ${r.sessionId} agent=${r.agentId??"?"} cwd=${r.cwd}`);const t=new T({daemonWsUrl:e.hydraWsUrl,token:e.hydraToken,sessionId:r.sessionId,meta:{...r.cwd!==void 0?{cwd:r.cwd}:{},...r.agentId!==void 0?{agentId:r.agentId}:{},...r.title!==void 0?{title:r.title}:{},...r.interactive!==void 0?{interactive:r.interactive}:{}},archive:d});i.set(r.sessionId,t),t.start()},onRemove:r=>{const t=i.get(r);t&&(o.info(`detaching from ${r}`),i.delete(r),t.stop())}});f.start(),process.on("SIGHUP",()=>{o.info(`SIGHUP \u2014 reloading rule from ${e.ruleConfigPath}`),y(e.ruleConfigPath).then(r=>{c=r,o.info("rule reload complete")}).catch(r=>{o.warn(`rule reload failed: ${r.message}`)})});const h=r=>{o.info(`${r} received \u2014 shutting down`),f.stop(),m.stop(),d.stop();for(const t of i.values())t.stop();setTimeout(()=>process.exit(0),200).unref()};process.on("SIGINT",()=>h("SIGINT")),process.on("SIGTERM",()=>h("SIGTERM")),o.info(`hydra-acp-archiver up; daemon=${e.hydraDaemonUrl} backend=${e.backend} host=${e.hostId} rule=${e.ruleConfigPath}`)}async function J(){const e=process.argv[2];if(e==="-v"||e==="--version"){process.stdout.write(`hydra-acp-archiver ${b()}
|
|
19
19
|
`);return}if(e===void 0){await W();return}if(e==="gdrive"){const n=process.argv[3];if(n==="login"){await V();return}const a="login";process.stderr.write(n!==void 0?`hydra-acp-archiver gdrive: unknown subcommand "${n}"
|
|
20
20
|
|
|
21
21
|
Available: ${a}
|
package/dist/rule.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stat as
|
|
1
|
+
import{stat as s}from"node:fs/promises";import{pathToFileURL as u}from"node:url";import{logger as c}from"./util/log.js";const n=c("rule"),o=e=>e.meta.interactive===!0;let i=0;async function m(e){try{await s(e)}catch(r){const t=r;return t.code==="ENOENT"?(n.info(`no rule config at ${e} \u2014 archiving only sessions the daemon marks interactive=true (drop a JS file at that path to customize)`),o):(n.warn(`stat ${e} failed: ${t.message}; using DEFAULT_RULE`),o)}i+=1;const a=`${u(e).href}?v=${Date.now()}-${i}`;try{const t=(await import(a)).default;return typeof t!="function"?(n.warn(`${e} did not export a default function; using DEFAULT_RULE`),o):(n.info(`loaded archiver rule from ${e}`),t)}catch(r){return n.warn(`import ${e} failed: ${r.message}; using DEFAULT_RULE`),o}}export{o as DEFAULT_RULE,m as loadRule};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydra-acp/archiver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Session sync extension for hydra-acp — uploads session bundles to a shared backend (Google Drive, fs) and imports peers' bundles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|