@hydra-acp/archiver 0.1.18 → 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/README.md +3 -10
- package/dist/archive-loop.js +1 -1
- package/dist/backend/factory.js +1 -1
- package/dist/backend/google-drive.js +1 -1
- package/dist/bridge.js +1 -1
- package/dist/cold-sweep.js +1 -1
- package/dist/config.js +1 -1
- package/dist/daemon.js +1 -1
- package/dist/discovery.js +1 -1
- package/dist/index.js +2 -2
- package/dist/oauth/google.js +2 -12
- package/dist/rule.js +1 -1
- package/dist/setup/wizard.js +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,15 +64,10 @@ Re-run `hydra-acp archiver setup` any time to switch backends or rotate keys —
|
|
|
64
64
|
|
|
65
65
|
The Google Drive backend uses OAuth 2.0 with the **`drive.file`** scope — the archiver can only see files it creates plus those you explicitly hand it via a picker. Your other Drive contents stay invisible to it.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
The archiver ships with an embedded OAuth client (the published `hydra-acp-archiver` app), so there's no Cloud Console setup on your end:
|
|
68
68
|
|
|
69
|
-
1.
|
|
70
|
-
2.
|
|
71
|
-
3. Configure the **OAuth consent screen**. User type: **External**. Add your Google account under **Test Users**.
|
|
72
|
-
4. **Credentials → Create credentials → OAuth client ID**. Application type: **Desktop app**.
|
|
73
|
-
5. Download the resulting JSON and save it to `~/.hydra-acp/archiver-google-credentials.json` (or anywhere, and set `HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS`).
|
|
74
|
-
6. Run `hydra-acp archiver gdrive login`. Your browser opens to Google's consent screen. The "Google hasn't verified this app" interstitial is expected for an unverified personal-use client — click **Advanced → Go to (unsafe)** and approve. The redirect lands on a transient local server, the archiver writes `~/.hydra-acp/archiver-google-token.json` (mode 0600), and you're done.
|
|
75
|
-
7. Register: `hydra-acp extensions add hydra-acp-archiver`.
|
|
69
|
+
1. Run `hydra-acp archiver gdrive login`. Your browser opens to Google's consent screen — sign in and grant access. The redirect lands on a transient local server, the archiver writes `~/.hydra-acp/archiver-google-token.json` (mode 0600), and you're done.
|
|
70
|
+
2. Register: `hydra-acp extensions add hydra-acp-archiver`.
|
|
76
71
|
|
|
77
72
|
After this, restart the daemon. The archiver process starts up, creates a `hydra-acp-archive/` folder in your Drive on first upload, and begins syncing.
|
|
78
73
|
|
|
@@ -249,7 +244,6 @@ KEY_PATH=/home/you/.hydra-acp/archiver-key
|
|
|
249
244
|
HOST_ID=alice-macbook
|
|
250
245
|
|
|
251
246
|
# Google Drive
|
|
252
|
-
# GOOGLE_CREDENTIALS=/home/you/.hydra-acp/archiver-google-credentials.json
|
|
253
247
|
# DRIVE_FOLDER=hydra-acp-archive
|
|
254
248
|
|
|
255
249
|
# Tuning (rarely needed)
|
|
@@ -276,7 +270,6 @@ The file is optional — if it doesn't exist the archiver falls back to environm
|
|
|
276
270
|
| `HYDRA_ACP_ARCHIVER_PREFIX` | auto (fingerprint or username) | User-level namespace prefix applied by all backends |
|
|
277
271
|
| `HYDRA_ACP_ARCHIVER_HOST_ID` | `os.hostname()` (sanitized) | Host identifier used as a subdirectory under the user prefix |
|
|
278
272
|
| `HYDRA_ACP_ARCHIVER_KEY_PATH` | — | Path to encryption key file; if unset, encryption is off |
|
|
279
|
-
| `HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS` | `~/.hydra-acp/archiver-google-credentials.json` | OAuth client JSON from GCP |
|
|
280
273
|
| `HYDRA_ACP_ARCHIVER_CONFIG` | `~/.hydra-acp/archiver.config.js` | Rule file (optional) |
|
|
281
274
|
| `HYDRA_ACP_ARCHIVER_POLL_MS` | `2000` | Session discovery cadence |
|
|
282
275
|
| `HYDRA_ACP_ARCHIVER_DEBOUNCE_MS` | `5000` | Per-session upload debounce window |
|
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/backend/factory.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FsBackend as
|
|
1
|
+
import{FsBackend as n}from"./fs.js";import{GoogleDriveBackend as t}from"./google-drive.js";import{S3Backend as o}from"./s3.js";function p(e){switch(e.backend){case"fs":return new n({dir:e.fsDir,prefix:e.prefix});case"google-drive":return new t({tokenPath:e.tokenPath,folderName:e.driveFolderName,prefix:e.prefix});case"s3":return new o({bucket:e.s3Bucket,region:e.s3Region,endpoint:e.s3Endpoint,prefix:e.prefix});default:{const r=e.backend;throw new Error(`unknown backend: ${String(r)}`)}}}export{p as makeBackend};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Readable as c}from"node:stream";import{google as p}from"googleapis";import{loadGoogleAuth as g}from"../oauth/google.js";import{logger as u}from"../util/log.js";const f=u("backend.google-drive"),o="application/vnd.google-apps.folder",l="application/json";class I{constructor(r){this.opts=r}opts;drive;prefixFolderId;async init(){const r=await g({
|
|
1
|
+
import{Readable as c}from"node:stream";import{google as p}from"googleapis";import{loadGoogleAuth as g}from"../oauth/google.js";import{logger as u}from"../util/log.js";const f=u("backend.google-drive"),o="application/vnd.google-apps.folder",l="application/json";class I{constructor(r){this.opts=r}opts;drive;prefixFolderId;async init(){const r=await g({tokenPath:this.opts.tokenPath});this.drive=p.drive({version:"v3",auth:r});let e=await this.ensureFolder(this.opts.folderName,void 0);const t=this.opts.prefix.split("/").filter(i=>i!=="");for(const i of t)e=await this.ensureFolder(i,e);this.prefixFolderId=e,f.info(`google-drive backend ready: folder="${this.opts.folderName}" prefix="${this.opts.prefix}" id=${this.prefixFolderId}`)}async list(){return this.listFolder(this.requirePrefixFolderId(),"")}async get(r){const e=this.requireDrive(),{parentId:t,name:i}=await this.resolveKey(r),s=await this.findFileId(i,t);if(!s)throw new Error(`google-drive: no file named ${r}`);const d=await e.files.get({fileId:s,alt:"media"},{responseType:"arraybuffer"});return Buffer.from(d.data)}async put(r,e){const t=this.requireDrive(),{parentId:i,name:s}=await this.resolveKey(r),d=await this.findFileId(s,i),n={mimeType:l,body:c.from(e)};if(d){await t.files.update({fileId:d,media:n});return}await t.files.create({requestBody:{name:s,mimeType:l,parents:[i]},media:n,fields:"id"})}async delete(r){const{parentId:e,name:t}=await this.resolveKey(r),i=await this.findFileId(t,e);i&&await this.requireDrive().files.update({fileId:i,requestBody:{trashed:!0}})}requireDrive(){if(!this.drive)throw new Error("GoogleDriveBackend used before init()");return this.drive}requirePrefixFolderId(){if(!this.prefixFolderId)throw new Error("GoogleDriveBackend used before init()");return this.prefixFolderId}async resolveKey(r){const e=r.split("/"),t=e[e.length-1];let i=this.requirePrefixFolderId();for(const s of e.slice(0,-1))i=await this.ensureFolder(s,i);return{parentId:i,name:t}}async listFolder(r,e){const t=this.requireDrive(),i=[];let s;do{const d=await t.files.list({q:`'${r}' in parents and trashed = false`,fields:"nextPageToken, files(id, name, size, modifiedTime, mimeType)",spaces:"drive",pageSize:200,...s!==void 0?{pageToken:s}:{}});for(const n of d.data.files??[]){if(!n.name)continue;const a=e!==""?`${e}/${n.name}`:n.name;n.mimeType===o?i.push(...await this.listFolder(n.id,a)):i.push({key:a,size:typeof n.size=="string"?Number.parseInt(n.size,10):0,modifiedAt:n.modifiedTime??new Date(0).toISOString()})}s=d.data.nextPageToken??void 0}while(s);return i}async ensureFolder(r,e){const t=this.requireDrive(),i=r.replace(/'/g,"\\'"),s=e!==void 0?`'${e}' in parents and `:"",n=(await t.files.list({q:`${s}name = '${i}' and mimeType = '${o}' and trashed = false`,fields:"files(id, name)",spaces:"drive",pageSize:10})).data.files?.[0];if(n?.id)return n.id;const a=await t.files.create({requestBody:{name:r,mimeType:o,...e!==void 0?{parents:[e]}:{}},fields:"id"});if(!a.data.id)throw new Error("google-drive: folder create returned no id");return f.info(`created Drive folder "${r}" id=${a.data.id}`),a.data.id}async findFileId(r,e){const t=this.requireDrive(),i=r.replace(/'/g,"\\'");return(await t.files.list({q:`'${e}' in parents and name = '${i}' and trashed = false`,fields:"files(id, name)",spaces:"drive",pageSize:2})).data.files?.[0]?.id??void 0}}export{I as GoogleDriveBackend};
|
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/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFile as
|
|
1
|
+
import{readFile as R,readFileSync as C}from"node:fs";import{homedir as _,hostname as A}from"node:os";import{resolve as s}from"node:path";import{promisify as E}from"node:util";const l=E(R);function p(n){return process.env.HYDRA_ACP_ARCHIVER_CONF??s(n,"archiver.conf")}function H(n){return n==="~"||n.startsWith("~/")?_()+n.slice(1):n}function D(n){const r=new Map;for(const i of n.split(/\r?\n/)){const t=i.trim();if(!t||t.startsWith("#"))continue;const e=t.indexOf("=");if(e===-1)continue;const c=t.slice(0,e).trim();let o=t.slice(e+1).trim();(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),r.set(c,H(o))}return r}function P(n){try{return D(C(n,"utf8"))}catch(r){if(r.code==="ENOENT")return new Map;throw r}}const y=new Set(["1","true","yes","on","t"]);function a(n,r,i,t){return process.env[n]??i.get(r)??t}function g(n,r,i){return process.env[n]??i.get(r)}function f(n,r,i,t){const e=process.env[n]??i.get(r);if(!e)return t;const c=Number.parseInt(e,10);return Number.isFinite(c)?c:t}function m(n,r,i,t){const e=process.env[n]??i.get(r);return e===void 0?t:y.has(e.toLowerCase())}function I(n){if(n.startsWith("https://"))return"wss://"+n.slice(8).replace(/\/$/,"")+"/acp";if(n.startsWith("http://"))return"ws://"+n.slice(7).replace(/\/$/,"")+"/acp";throw new Error(`hydraDaemonUrl must start with http:// or https://: ${n}`)}function v(n){const r=n.toLowerCase();if(r==="google-drive"||r==="fs"||r==="s3")return r;throw new Error(`BACKEND must be one of: google-drive, fs, s3 (got "${n}")`)}function Y(){const n=process.env.HYDRA_ACP_DAEMON_URL??"http://127.0.0.1:8765",r=process.env.HYDRA_ACP_TOKEN??"";if(!r)throw new Error("Missing HYDRA_ACP_TOKEN env var. When run as a hydra extension, hydra injects this automatically.");const i=process.env.HYDRA_ACP_WS_URL??I(n),t=process.env.HYDRA_ACP_HOME??s(_(),".hydra-acp"),e=P(p(t)),c=a("HYDRA_ACP_ARCHIVER_CONFIG","CONFIG",e,s(t,"archiver.config.js")),o=s(t,"archiver-google-token.json"),h=s(t,"archiver-state.json"),d=v(a("HYDRA_ACP_ARCHIVER_BACKEND","BACKEND",e,"google-drive")),u=a("HYDRA_ACP_ARCHIVER_S3_BUCKET","S3_BUCKET",e,"");if(d==="s3"&&!u)throw new Error("S3_BUCKET is required when BACKEND is s3. Set it in archiver.conf or via HYDRA_ACP_ARCHIVER_S3_BUCKET.");return{hydraDaemonUrl:n,hydraWsUrl:i,hydraToken:r,hydraHome:t,hydraPollIntervalMs:f("HYDRA_ACP_ARCHIVER_POLL_MS","POLL_MS",e,2e3),ruleConfigPath:c,uploadDebounceMs:f("HYDRA_ACP_ARCHIVER_DEBOUNCE_MS","DEBOUNCE_MS",e,5e3),pullIntervalMs:f("HYDRA_ACP_ARCHIVER_PULL_MS","PULL_MS",e,6e4),backend:d,driveFolderName:a("HYDRA_ACP_ARCHIVER_DRIVE_FOLDER","DRIVE_FOLDER",e,"hydra-acp-archive"),fsDir:a("HYDRA_ACP_ARCHIVER_FS_DIR","FS_DIR",e,s(t,"archive")),s3Bucket:u,s3Region:g("HYDRA_ACP_ARCHIVER_S3_REGION","S3_REGION",e),s3Endpoint:g("HYDRA_ACP_ARCHIVER_S3_ENDPOINT","S3_ENDPOINT",e),prefix:a("HYDRA_ACP_ARCHIVER_PREFIX","PREFIX",e,""),hostId:(g("HYDRA_ACP_ARCHIVER_HOST_ID","HOST_ID",e)??A()).toLowerCase().replace(/[^a-z0-9-]/g,"-"),encryptionKeyPath:g("HYDRA_ACP_ARCHIVER_KEY_PATH","KEY_PATH",e),tokenPath:o,statePath:h,toolContent:N(a("HYDRA_ACP_ARCHIVER_TOOL_CONTENT","TOOL_CONTENT",e,"inline")),debug:m("DEBUG","DEBUG",e,!1)}}function N(n){return n==="references"||n==="summary"?n:"inline"}async function M(n){if(n===void 0)return;let r;try{r=(await l(n,"utf8")).trim()}catch(i){throw i.code==="ENOENT"?new Error(`Encryption key file not found at ${n}. Run \`hydra-acp-archiver keygen\` to generate one.`):i}if(!/^[0-9a-f]{64}$/i.test(r))throw new Error(`Encryption key at ${n} is not a valid 64-character hex string.`);return Buffer.from(r,"hex")}function L(){const n=process.env.HYDRA_ACP_HOME??s(_(),".hydra-acp");return{hydraHome:n,tokenPath:s(n,"archiver-google-token.json")}}export{Y as loadConfig,M as loadEncryptionKey,L as loadLoginConfig};
|
package/dist/daemon.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as d}from"./util/log.js";const c=d("daemon");class p{constructor(s){this.opts=s}opts;async listSessionIds(){const s=await fetch(`${this.opts.daemonUrl}/v1/sessions`,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!s.ok)throw new Error(`list sessions: HTTP ${s.status}`);const n=await s.json(),t=new Set;for(const e of n.sessions)typeof e.sessionId=="string"&&t.add(e.sessionId);return t}async exportSession(s){const n=this.opts.toolContent??"inline",t=n==="inline"?"":`?tools=${n}`,e=`${this.opts.daemonUrl}/v1/sessions/${encodeURIComponent(s)}/export${t}`,o=await fetch(e,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!o.ok){const i=await a(o);throw new Error(`export ${s}: HTTP ${o.status} ${i}`)}return await o.json()}async importBundle(s,n={}){const t={bundle:s};n.replace!==void 0&&(t.replace=n.replace),n.cwd!==void 0&&(t.cwd=n.cwd);const e=await fetch(`${this.opts.daemonUrl}/v1/sessions/import`,{method:"POST",headers:{Authorization:`Bearer ${this.opts.token}`,"content-type":"application/json"},body:JSON.stringify(t)});if(!e.ok){const i=await a(e);throw new Error(`import lineage=${s.session.lineageId}: HTTP ${e.status} ${i}`)}const o=await e.json();return o.sessionId?{sessionId:o.sessionId}:(c.warn(`import returned no sessionId for lineage ${s.session.lineageId}`),{sessionId:""})}}async function a(r){try{return(await r.text()).slice(0,500)}catch{return""}}export{p as DaemonClient};
|
|
1
|
+
import{logger as d}from"./util/log.js";const c=d("daemon");class p{constructor(s){this.opts=s}opts;async listSessionIds(){const s=await fetch(`${this.opts.daemonUrl}/v1/sessions?includeNonInteractive=1`,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!s.ok)throw new Error(`list sessions: HTTP ${s.status}`);const n=await s.json(),t=new Set;for(const e of n.sessions)typeof e.sessionId=="string"&&t.add(e.sessionId);return t}async exportSession(s){const n=this.opts.toolContent??"inline",t=n==="inline"?"":`?tools=${n}`,e=`${this.opts.daemonUrl}/v1/sessions/${encodeURIComponent(s)}/export${t}`,o=await fetch(e,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!o.ok){const i=await a(o);throw new Error(`export ${s}: HTTP ${o.status} ${i}`)}return await o.json()}async importBundle(s,n={}){const t={bundle:s};n.replace!==void 0&&(t.replace=n.replace),n.cwd!==void 0&&(t.cwd=n.cwd);const e=await fetch(`${this.opts.daemonUrl}/v1/sessions/import`,{method:"POST",headers:{Authorization:`Bearer ${this.opts.token}`,"content-type":"application/json"},body:JSON.stringify(t)});if(!e.ok){const i=await a(e);throw new Error(`import lineage=${s.session.lineageId}: HTTP ${e.status} ${i}`)}const o=await e.json();return o.sessionId?{sessionId:o.sessionId}:(c.warn(`import returned no sessionId for lineage ${s.session.lineageId}`),{sessionId:""})}}async function a(r){try{return(await r.text()).slice(0,500)}catch{return""}}export{p as DaemonClient};
|
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
|
|
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(
|
|
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/oauth/google.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import{spawn as
|
|
2
|
-
Set up Google OAuth credentials first:
|
|
3
|
-
1. Visit https://console.cloud.google.com/
|
|
4
|
-
2. Create or select a project, then enable the Google Drive API
|
|
5
|
-
3. Configure OAuth consent screen (User type: External; add yourself as a Test User)
|
|
6
|
-
4. Create credentials -> OAuth client ID -> Application type: Desktop app
|
|
7
|
-
5. Download the JSON and save it to ~/.hydra-acp/archiver-google-credentials.json
|
|
8
|
-
(or set HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS)
|
|
9
|
-
`;async function f(e){let t;try{t=await h(e,"utf8")}catch(i){throw i.code==="ENOENT"?new Error(`OAuth credentials file not found at ${e}.
|
|
10
|
-
${g}`):i}const n=JSON.parse(t),r=n.installed??n.web;if(!r||!r.client_id||!r.client_secret)throw new Error(`OAuth credentials at ${e} missing client_id/client_secret.
|
|
11
|
-
${g}`);return{client_id:r.client_id,client_secret:r.client_secret}}async function A(e){try{const t=await h(e,"utf8");return JSON.parse(t)}catch(t){if(t.code==="ENOENT")return;throw t}}async function m(e,t){await v(_(e),{recursive:!0}),await C(e,JSON.stringify(t,null,2),{mode:384}),await y(e,384)}function E(e){let t,n;switch(process.platform){case"darwin":t="open",n=[e];break;case"win32":t="cmd",n=["/c","start","",e];break;default:t="xdg-open",n=[e];break}try{const r=w(t,n,{stdio:"ignore",detached:!0});r.on("error",()=>{l.info("could not auto-open browser; copy the URL above into a browser")}),r.unref()}catch{l.info("could not auto-open browser; copy the URL above into a browser")}}async function $(e){const t=await f(e.credentialsPath),n=await A(e.tokenPath);if(!n||!n.refresh_token)throw new Error(`No Google OAuth token at ${e.tokenPath}. Run \`hydra-acp-archiver-login\` first.`);const r=new p.auth.OAuth2(t.client_id,t.client_secret);return r.setCredentials(n),r.on("tokens",i=>{const a={...n,...i};m(e.tokenPath,a).catch(s=>{l.warn(`failed to persist refreshed token: ${s.message}`)})}),r}async function U(e){const t=await f(e.credentialsPath),{port:n,codePromise:r,close:i}=await b(),a=`http://127.0.0.1:${n}`,s=new p.auth.OAuth2(t.client_id,t.client_secret,a),o=s.generateAuthUrl({access_type:"offline",scope:P,prompt:"consent"});l.info("Open this URL in a browser to authorize the archiver:"),l.info(o),E(o);let c;try{c=await r}finally{i()}const{tokens:d}=await s.getToken(c);if(!d.refresh_token)throw new Error("Google did not return a refresh_token. Revoke the app at https://myaccount.google.com/permissions and try again.");await m(e.tokenPath,d),l.info(`token saved to ${e.tokenPath}`)}async function b(){let e=()=>{},t=()=>{};const n=new Promise((s,o)=>{e=s,t=o}),r=k((s,o)=>{try{const c=new URL(s.url??"/","http://127.0.0.1"),d=c.searchParams.get("code"),u=c.searchParams.get("error");if(u){o.statusCode=400,o.setHeader("content-type","text/plain; charset=utf-8"),o.end(`Authorization error: ${u}
|
|
1
|
+
import{spawn as m}from"node:child_process";import{createServer as k}from"node:http";import{chmod as w,mkdir as v,readFile as y,writeFile as b}from"node:fs/promises";import{dirname as P}from"node:path";import{google as h}from"googleapis";import{logger as S}from"../util/log.js";const d=S("oauth"),O=["https://www.googleapis.com/auth/drive.file"],p="774694216181-tq9raja8oij25bb0okq00g5fvg002u3k.apps.googleusercontent.com",g="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";async function C(t){try{const e=await y(t,"utf8");return JSON.parse(e)}catch(e){if(e.code==="ENOENT")return;throw e}}async function f(t,e){await v(P(t),{recursive:!0}),await b(t,JSON.stringify(e,null,2),{mode:384}),await w(t,384)}function E(t){let e,r;switch(process.platform){case"darwin":e="open",r=[t];break;case"win32":e="cmd",r=["/c","start","",t];break;default:e="xdg-open",r=[t];break}try{const n=m(e,r,{stdio:"ignore",detached:!0});n.on("error",()=>{d.info("could not auto-open browser; copy the URL above into a browser")}),n.unref()}catch{d.info("could not auto-open browser; copy the URL above into a browser")}}async function L(t){const e=await C(t.tokenPath);if(!e||!e.refresh_token)throw new Error(`No Google OAuth token at ${t.tokenPath}. Run \`hydra-acp-archiver-login\` first.`);const r=new h.auth.OAuth2(p,g);return r.setCredentials(e),r.on("tokens",n=>{const i={...e,...n};f(t.tokenPath,i).catch(s=>{d.warn(`failed to persist refreshed token: ${s.message}`)})}),r}async function U(t){const{port:e,codePromise:r,close:n}=await T(),i=`http://127.0.0.1:${e}`,s=new h.auth.OAuth2(p,g,i),a=s.generateAuthUrl({access_type:"offline",scope:O,prompt:"consent"});d.info("Open this URL in a browser to authorize the archiver:"),d.info(a),E(a);let o;try{o=await r}finally{n()}const{tokens:c}=await s.getToken(o);if(!c.refresh_token)throw new Error("Google did not return a refresh_token. Revoke the app at https://myaccount.google.com/permissions and try again.");await f(t.tokenPath,c),d.info(`token saved to ${t.tokenPath}`)}async function T(){let t=()=>{},e=()=>{};const r=new Promise((a,o)=>{t=a,e=o}),n=k((a,o)=>{try{const c=new URL(a.url??"/","http://127.0.0.1"),u=c.searchParams.get("code"),l=c.searchParams.get("error");if(l){o.statusCode=400,o.setHeader("content-type","text/plain; charset=utf-8"),o.end(`Authorization error: ${l}
|
|
12
2
|
|
|
13
|
-
You can close this tab.`),
|
|
3
|
+
You can close this tab.`),e(new Error(`OAuth error: ${l}`));return}if(!u){o.statusCode=400,o.setHeader("content-type","text/plain; charset=utf-8"),o.end("Missing ?code parameter. You can close this tab.");return}o.statusCode=200,o.setHeader("content-type","text/plain; charset=utf-8"),o.end("hydra-acp-archiver: authorization complete. You can close this tab."),t(u)}catch(c){o.statusCode=500,o.end("internal error"),e(c)}});await new Promise((a,o)=>{n.once("error",o),n.listen(0,"127.0.0.1",()=>a())});const i=n.address();if(!i||typeof i=="string")throw n.close(),new Error("failed to determine callback server port");const s=i.port;return d.info(`listening for OAuth redirect on http://127.0.0.1:${s}`),{port:s,codePromise:r,close:()=>{try{n.close()}catch{}}}}export{L as loadGoogleAuth,U as runGoogleLogin};
|
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/dist/setup/wizard.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import{spawnSync as
|
|
2
|
-
${
|
|
1
|
+
import{spawnSync as C}from"node:child_process";import{createHash as _}from"node:crypto";import{existsSync as E,mkdirSync as T,readFileSync as R,rmSync as O,statSync as B,writeFileSync as I}from"node:fs";import{homedir as b}from"node:os";import{delimiter as G,join as L,resolve as u}from"node:path";import{loadLoginConfig as H}from"../config.js";import{runKeygen as K}from"../keygen.js";import{runGoogleLogin as N}from"../oauth/google.js";import{loadAwsCredentials as W}from"../util/aws-credentials.js";import{PRIMARY_CONF_PATH as m,readExisting as x,writeConf as Y}from"./conf-writer.js";import{ask as h,confirm as k,pause as U,pickFromList as M}from"./prompts.js";const d=6,A="\x1B[1m",j="\x1B[32m",z="\x1B[33m",V="\x1B[31m",w="\x1B[2m",a="\x1B[0m";function l(e,t,i){process.stdout.write(`
|
|
2
|
+
${A}[${e}/${t}] ${i}${a}
|
|
3
3
|
|
|
4
|
-
`)}function c(e){process.stdout.write(` ${
|
|
5
|
-
`)}function
|
|
6
|
-
`)}function
|
|
7
|
-
`),process.exit(1)}function
|
|
4
|
+
`)}function c(e){process.stdout.write(` ${j}\u2713${a} ${e}
|
|
5
|
+
`)}function p(e){process.stdout.write(` ${z}\u26A0${a} ${e}
|
|
6
|
+
`)}function v(e){process.stderr.write(` ${V}\u2717 ${e}${a}
|
|
7
|
+
`),process.exit(1)}function n(e){process.stdout.write(` ${e}
|
|
8
8
|
`)}function o(){process.stdout.write(`
|
|
9
|
-
`)}function
|
|
9
|
+
`)}function P(e){const t=(process.env.PATH??"").split(G),i=process.platform==="win32"?[".exe",".cmd",".bat",""]:[""];for(const r of t)if(r)for(const s of i){const y=L(r,e+s);try{if(B(y).isFile())return!0}catch{}}return!1}const $=u(b(),".hydra-acp"),q=u($,"config.json"),f=u($,"archiver-key"),g=u($,"archive"),S="hydra-acp-archive";function D(){try{const e=JSON.parse(R(q,"utf8"));return new Set(Object.keys(e.extensions??{}))}catch{return new Set}}function F(e){try{const t=R(e,"utf8").trim();if(!/^[0-9a-f]+$/i.test(t))return;const i=Buffer.from(t,"hex");return i.length!==32?void 0:_("sha256").update(i).digest().subarray(0,8).toString("hex")}catch{return}}async function J(){l(1,d,"Checking existing setup"),n("This wizard configures a backend (Google Drive / S3 / Filesystem),"),n("optional encryption, and registers the archiver with hydra."),o();const{map:e}=x(m);if(e.size===0)return c("No existing archiver config found."),{reconfigure:!0};const t=e.get("BACKEND")??"google-drive";n(`Existing config at ${m}`),n(` Backend: ${t}`),t==="google-drive"?n(` Drive folder: ${e.get("DRIVE_FOLDER")??S}`):t==="s3"?n(` S3 bucket: ${e.get("S3_BUCKET")??"(missing)"}`):t==="fs"&&n(` FS dir: ${e.get("FS_DIR")??g}`);const i=e.get("KEY_PATH")??(E(f)?f:void 0);if(e.get("KEY_PATH")){const s=F(e.get("KEY_PATH"));n(` Encryption: on${s?` (fingerprint ${s})`:""}`)}else n(" Encryption: off");const r=D().has("hydra-acp-archiver");return n(` Registered with hydra: ${r?"yes":"no"}`),o(),await k("Reconfigure from scratch?",!1)?{reconfigure:!0}:(n("Edit ~/.hydra-acp/archiver.conf directly to tune individual settings."),{reconfigure:!1})}async function Q(){l(2,d,"Pick a backend");const t=await M("Choose:",[{backend:"google-drive",label:`Google Drive
|
|
10
10
|
Easiest cross-machine sync. Free up to 15 GB. One-time GCP
|
|
11
11
|
setup (~5 min in the Cloud Console).`},{backend:"s3",label:`S3 / S3-compatible (R2, B2, MinIO, Wasabi)
|
|
12
12
|
Best for larger archives. Needs AWS-style creds and an
|
|
13
13
|
existing bucket.`},{backend:"fs",label:`Filesystem
|
|
14
14
|
Useful with Syncthing/Dropbox mirroring a folder, or for
|
|
15
|
-
local-only testing.`}],i=>i.label);return
|
|
16
|
-
${
|
|
17
|
-
`),!(await
|
|
15
|
+
local-only testing.`}],i=>i.label);return t||v("A backend is required."),c(`Backend: ${t.backend}`),{backend:t.backend}}async function X(){l(3,d,"Configure Google Drive");const e=H(),t=await h("Drive folder name",S);o(),n("Now we run Google's OAuth flow. Your browser will open to a Google"),n("consent screen \u2014 sign in and grant access to the hydra-acp-archiver app."),n("The archiver uses the drive.file scope, so it can only see files it"),n("creates (not the rest of your Drive)."),o(),await U("Press Enter to start OAuth...");try{await N({tokenPath:e.tokenPath})}catch(i){v(`Google OAuth failed: ${i.message}`)}return c(`OAuth complete. Token saved to ${e.tokenPath}.`),{driveFolder:t}}async function Z(){l(3,d,"Configure S3"),o(),n("S3 endpoint hints:"),n(` ${w}AWS S3${a} leave Endpoint blank, set Region`),n(` ${w}Cloudflare R2${a} https://<accountid>.r2.cloudflarestorage.com`),n(` ${w}Backblaze B2${a} https://s3.<region>.backblazeb2.com`),n(` ${w}MinIO${a} your MinIO endpoint URL`),o();const e=await h("S3 bucket name");e||v("A bucket name is required for S3.");const t=await h("Region (blank for SDK default)")||void 0,i=await h("Endpoint URL (blank for AWS S3)")||void 0;o();try{const r=W(),s=`${r.accessKeyId.slice(0,4)}\u2026${r.accessKeyId.slice(-4)}`,y=process.env.AWS_ACCESS_KEY_ID?"env vars":`~/.aws/credentials (profile: ${process.env.AWS_PROFILE??"default"})`;c(`AWS creds found via ${y} \u2014 access key ${s}.`)}catch(r){p(`${r.message}`),n("The archiver will fail to start until creds are in place.")}return{bucket:e,region:t,endpoint:i}}async function ee(){l(3,d,"Configure Filesystem");const e=[];for(const i of["Syncthing","Dropbox","iCloud Drive","OneDrive"]){const r=u(b(),i);E(r)&&e.push(r)}let t;if(e.length>0){n("Found sync folders that could host the archive:"),o(),e.forEach((r,s)=>n(` ${s+1}. ${r}/hydra-acp-archive`)),n(" c. Custom path"),n(` d. Default (${g})`),o();const i=(await h("Choice","d")).toLowerCase();if(i==="d"||i==="")t=g;else if(i==="c"){const r=await h("Archive directory",g);t=r.startsWith("~/")?r.replace(/^~/,b()):r}else{const r=Number.parseInt(i,10);Number.isInteger(r)&&r>=1&&r<=e.length&&(t=u(e[r-1],"hydra-acp-archive"))}t||v("Invalid choice.")}else{const i=await h("Archive directory",g);t=i.startsWith("~/")?i.replace(/^~/,b()):i}try{T(t,{recursive:!0});const i=u(t,".hydra-acp-archiver-test");I(i,"ok"),O(i),c(`Directory writable: ${t}`)}catch(i){v(`Cannot write to ${t}: ${i.message}`)}return{dir:t}}async function te(e){l(4,d,"Encryption");const t=e!=="fs";if(n(e==="fs"?"Filesystem backend \u2014 encryption is optional (you already control the disk).":`${e==="google-drive"?"Google Drive":"S3"} backend \u2014 encryption is recommended (data leaves your machine).`),o(),!await k("Enable AES-256-GCM encryption at rest?",t))return{enabled:!1,keyPath:void 0};const i=F(f);if(i){if(n(`Existing key at ${f} (fingerprint ${i}).`),await k("Use this key?",!0))return{enabled:!0,keyPath:f};if(o(),p("Rotating the key makes the existing archive unreadable until re-uploaded."),!await k("Generate a new key (overwrites the existing file)?",!1))return{enabled:!0,keyPath:f}}return o(),await K(),o(),n(`Copy ${f} to each machine that should share this archive.`),{enabled:!0,keyPath:f}}async function ne(e){l(5,d,"Writing config");const t={BACKEND:e.backend};e.backend==="google-drive"&&e.google?e.google.driveFolder!==S&&(t.DRIVE_FOLDER=e.google.driveFolder):e.backend==="s3"&&e.s3?(t.S3_BUCKET=e.s3.bucket,e.s3.region&&(t.S3_REGION=e.s3.region),e.s3.endpoint&&(t.S3_ENDPOINT=e.s3.endpoint)):e.backend==="fs"&&e.fs&&e.fs.dir!==g&&(t.FS_DIR=e.fs.dir),e.encryption.enabled&&e.encryption.keyPath&&(t.KEY_PATH=e.encryption.keyPath),Y(m,t),c(`Wrote ${m} (chmod 600).`),o(),n("Final config:");const{map:i}=x(m);for(const[r,s]of i)n(` ${r}=${s}`)}async function ie(){if(l(6,d,"Register with hydra (optional)"),!P("hydra-acp")){n("hydra-acp not found on PATH. Register manually later with:"),n(" hydra-acp extensions add hydra-acp-archiver");return}if(D().has("hydra-acp-archiver")){c("Already registered as a hydra extension."),n("Restart the daemon to pick up the new config: hydra-acp daemon restart");return}if(n("hydra can manage hydra-acp-archiver as a subprocess that auto-starts"),n("with the daemon. This adds an entry to ~/.hydra-acp/config.json."),o(),!await k("Register hydra-acp-archiver as a hydra extension?",!0)){n("Skipping. Register later with:"),n(" hydra-acp extensions add hydra-acp-archiver");return}const e=["extensions","add","hydra-acp-archiver"];if(!P("hydra-acp-archiver")){const i=process.argv[1]??"";if(!i)p("Couldn't determine script path; falling back to bare command.");else if(i.includes("/.npm/_npx/")){p("Looks like you're running via npx \u2014 registering this transient path"),p("would break on the next npx cache cleanup. Install globally first:"),n(" npm install -g @hydra-acp/archiver"),n("Then register with: hydra-acp extensions add hydra-acp-archiver");return}else e.push("--command","node","--args",i)}n(`Running: hydra-acp ${e.join(" ")}`);const t=C("hydra-acp",e,{stdio:"inherit"});t.status===0?(c("Registered."),n("Start the daemon (or restart if already running): hydra-acp daemon restart")):(o(),p(`hydra-acp exited with code ${t.status??"?"}.`),n("Register manually later with:"),n(` hydra-acp ${e.join(" ")}`))}async function me(){if(process.stdout.write(`
|
|
16
|
+
${A}hydra-acp-archiver setup${a}
|
|
17
|
+
`),!(await J()).reconfigure)return;const{backend:t}=await Q();let i,r,s;t==="google-drive"?i=await X():t==="s3"?r=await Z():s=await ee();const y=await te(t);await ne({backend:t,google:i,s3:r,fs:s,encryption:y}),await ie(),o(),c("Setup complete.")}export{me as runSetup};
|
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",
|