@hydra-acp/archiver 0.1.15 → 0.1.17
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/acp/attach.js +1 -201
- package/dist/acp/protocol.js +1 -13
- package/dist/archive-loop.js +1 -134
- package/dist/backend/encrypted.js +1 -59
- package/dist/backend/factory.js +1 -28
- package/dist/backend/fs.js +1 -78
- package/dist/backend/google-drive.js +1 -182
- package/dist/backend/s3.js +5 -189
- package/dist/backend/types.js +0 -2
- package/dist/bridge.js +1 -100
- package/dist/cold-sweep.js +1 -54
- package/dist/config.js +1 -155
- package/dist/daemon.js +1 -74
- package/dist/discovery.js +1 -82
- package/dist/envelope.js +1 -123
- package/dist/index.js +15 -240
- package/dist/keygen.js +8 -26
- package/dist/oauth/google.js +6 -195
- package/dist/pull-loop.js +1 -147
- package/dist/rule.js +1 -37
- package/dist/setup/conf-writer.js +4 -85
- package/dist/setup/downloads-scan.js +1 -44
- package/dist/setup/prompts.js +14 -123
- package/dist/setup/wizard.js +17 -415
- package/dist/state.js +1 -129
- package/dist/util/aws-credentials.js +1 -82
- package/dist/util/log.js +2 -46
- package/package.json +5 -4
- package/dist/acp/attach.js.map +0 -1
- package/dist/acp/protocol.js.map +0 -1
- package/dist/archive-loop.js.map +0 -1
- package/dist/backend/encrypted.js.map +0 -1
- package/dist/backend/factory.js.map +0 -1
- package/dist/backend/fs.js.map +0 -1
- package/dist/backend/google-drive.js.map +0 -1
- package/dist/backend/s3.js.map +0 -1
- package/dist/backend/types.js.map +0 -1
- package/dist/bridge.js.map +0 -1
- package/dist/cold-sweep.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/discovery.js.map +0 -1
- package/dist/envelope.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keygen.js.map +0 -1
- package/dist/oauth/google.js.map +0 -1
- package/dist/pull-loop.js.map +0 -1
- package/dist/rule.js.map +0 -1
- package/dist/setup/conf-writer.js.map +0 -1
- package/dist/setup/downloads-scan.js.map +0 -1
- package/dist/setup/prompts.js.map +0 -1
- package/dist/setup/wizard.js.map +0 -1
- package/dist/state.js.map +0 -1
- package/dist/util/aws-credentials.js.map +0 -1
- package/dist/util/log.js.map +0 -1
package/dist/config.js
CHANGED
|
@@ -1,155 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir, hostname } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
const readFileAsync = promisify(readFile);
|
|
6
|
-
// ── Conf file ────────────────────────────────────────────────────────────────
|
|
7
|
-
function confPath(hydraHome) {
|
|
8
|
-
return process.env.HYDRA_ACP_ARCHIVER_CONF ?? resolve(hydraHome, "archiver.conf");
|
|
9
|
-
}
|
|
10
|
-
function expandTilde(val) {
|
|
11
|
-
if (val === "~" || val.startsWith("~/"))
|
|
12
|
-
return homedir() + val.slice(1);
|
|
13
|
-
return val;
|
|
14
|
-
}
|
|
15
|
-
function parseConfFile(text) {
|
|
16
|
-
const out = new Map();
|
|
17
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
18
|
-
const line = rawLine.trim();
|
|
19
|
-
if (!line || line.startsWith("#"))
|
|
20
|
-
continue;
|
|
21
|
-
const eq = line.indexOf("=");
|
|
22
|
-
if (eq === -1)
|
|
23
|
-
continue;
|
|
24
|
-
const key = line.slice(0, eq).trim();
|
|
25
|
-
let val = line.slice(eq + 1).trim();
|
|
26
|
-
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
27
|
-
(val.startsWith("'") && val.endsWith("'")))
|
|
28
|
-
val = val.slice(1, -1);
|
|
29
|
-
out.set(key, expandTilde(val));
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
// Returns an empty map when the file does not exist — conf file is optional.
|
|
34
|
-
function readConf(path) {
|
|
35
|
-
try {
|
|
36
|
-
return parseConfFile(readFileSync(path, "utf8"));
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
const e = err;
|
|
40
|
-
if (e.code === "ENOENT")
|
|
41
|
-
return new Map();
|
|
42
|
-
throw err;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// ── Value helpers ─────────────────────────────────────────────────────────────
|
|
46
|
-
// Priority for every config value: env var > conf file > default.
|
|
47
|
-
const TRUTHY = new Set(["1", "true", "yes", "on", "t"]);
|
|
48
|
-
function str(envName, confKey, conf, fallback) {
|
|
49
|
-
return process.env[envName] ?? conf.get(confKey) ?? fallback;
|
|
50
|
-
}
|
|
51
|
-
function optStr(envName, confKey, conf) {
|
|
52
|
-
return process.env[envName] ?? conf.get(confKey);
|
|
53
|
-
}
|
|
54
|
-
function intVal(envName, confKey, conf, fallback) {
|
|
55
|
-
const raw = process.env[envName] ?? conf.get(confKey);
|
|
56
|
-
if (!raw)
|
|
57
|
-
return fallback;
|
|
58
|
-
const n = Number.parseInt(raw, 10);
|
|
59
|
-
return Number.isFinite(n) ? n : fallback;
|
|
60
|
-
}
|
|
61
|
-
function boolVal(envName, confKey, conf, fallback) {
|
|
62
|
-
const raw = process.env[envName] ?? conf.get(confKey);
|
|
63
|
-
if (raw === undefined)
|
|
64
|
-
return fallback;
|
|
65
|
-
return TRUTHY.has(raw.toLowerCase());
|
|
66
|
-
}
|
|
67
|
-
// ── Derived helpers ───────────────────────────────────────────────────────────
|
|
68
|
-
function deriveWsUrl(httpUrl) {
|
|
69
|
-
if (httpUrl.startsWith("https://"))
|
|
70
|
-
return "wss://" + httpUrl.slice("https://".length).replace(/\/$/, "") + "/acp";
|
|
71
|
-
if (httpUrl.startsWith("http://"))
|
|
72
|
-
return "ws://" + httpUrl.slice("http://".length).replace(/\/$/, "") + "/acp";
|
|
73
|
-
throw new Error(`hydraDaemonUrl must start with http:// or https://: ${httpUrl}`);
|
|
74
|
-
}
|
|
75
|
-
function parseBackend(raw) {
|
|
76
|
-
const v = raw.toLowerCase();
|
|
77
|
-
if (v === "google-drive" || v === "fs" || v === "s3")
|
|
78
|
-
return v;
|
|
79
|
-
throw new Error(`BACKEND must be one of: google-drive, fs, s3 (got "${raw}")`);
|
|
80
|
-
}
|
|
81
|
-
// ── Public API ────────────────────────────────────────────────────────────────
|
|
82
|
-
export function loadConfig() {
|
|
83
|
-
const hydraDaemonUrl = process.env.HYDRA_ACP_DAEMON_URL ?? "http://127.0.0.1:8765";
|
|
84
|
-
const hydraToken = process.env.HYDRA_ACP_TOKEN ?? "";
|
|
85
|
-
if (!hydraToken) {
|
|
86
|
-
throw new Error("Missing HYDRA_ACP_TOKEN env var. When run as a hydra extension, hydra injects this automatically.");
|
|
87
|
-
}
|
|
88
|
-
const hydraWsUrl = process.env.HYDRA_ACP_WS_URL ?? deriveWsUrl(hydraDaemonUrl);
|
|
89
|
-
const hydraHome = process.env.HYDRA_ACP_HOME ?? resolve(homedir(), ".hydra-acp");
|
|
90
|
-
const conf = readConf(confPath(hydraHome));
|
|
91
|
-
const ruleConfigPath = str("HYDRA_ACP_ARCHIVER_CONFIG", "CONFIG", conf, resolve(hydraHome, "archiver.config.js"));
|
|
92
|
-
const credentialsPath = str("HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS", "GOOGLE_CREDENTIALS", conf, resolve(hydraHome, "archiver-google-credentials.json"));
|
|
93
|
-
const tokenPath = resolve(hydraHome, "archiver-google-token.json");
|
|
94
|
-
const statePath = resolve(hydraHome, "archiver-state.json");
|
|
95
|
-
const backend = parseBackend(str("HYDRA_ACP_ARCHIVER_BACKEND", "BACKEND", conf, "google-drive"));
|
|
96
|
-
const s3Bucket = str("HYDRA_ACP_ARCHIVER_S3_BUCKET", "S3_BUCKET", conf, "");
|
|
97
|
-
if (backend === "s3" && !s3Bucket) {
|
|
98
|
-
throw new Error("S3_BUCKET is required when BACKEND is s3. Set it in archiver.conf or via HYDRA_ACP_ARCHIVER_S3_BUCKET.");
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
hydraDaemonUrl,
|
|
102
|
-
hydraWsUrl,
|
|
103
|
-
hydraToken,
|
|
104
|
-
hydraHome,
|
|
105
|
-
hydraPollIntervalMs: intVal("HYDRA_ACP_ARCHIVER_POLL_MS", "POLL_MS", conf, 2000),
|
|
106
|
-
ruleConfigPath,
|
|
107
|
-
uploadDebounceMs: intVal("HYDRA_ACP_ARCHIVER_DEBOUNCE_MS", "DEBOUNCE_MS", conf, 5000),
|
|
108
|
-
pullIntervalMs: intVal("HYDRA_ACP_ARCHIVER_PULL_MS", "PULL_MS", conf, 60000),
|
|
109
|
-
backend,
|
|
110
|
-
driveFolderName: str("HYDRA_ACP_ARCHIVER_DRIVE_FOLDER", "DRIVE_FOLDER", conf, "hydra-acp-archive"),
|
|
111
|
-
fsDir: str("HYDRA_ACP_ARCHIVER_FS_DIR", "FS_DIR", conf, resolve(hydraHome, "archive")),
|
|
112
|
-
s3Bucket,
|
|
113
|
-
s3Region: optStr("HYDRA_ACP_ARCHIVER_S3_REGION", "S3_REGION", conf),
|
|
114
|
-
s3Endpoint: optStr("HYDRA_ACP_ARCHIVER_S3_ENDPOINT", "S3_ENDPOINT", conf),
|
|
115
|
-
prefix: str("HYDRA_ACP_ARCHIVER_PREFIX", "PREFIX", conf, ""),
|
|
116
|
-
hostId: (optStr("HYDRA_ACP_ARCHIVER_HOST_ID", "HOST_ID", conf) ?? hostname())
|
|
117
|
-
.toLowerCase()
|
|
118
|
-
.replace(/[^a-z0-9-]/g, "-"),
|
|
119
|
-
encryptionKeyPath: optStr("HYDRA_ACP_ARCHIVER_KEY_PATH", "KEY_PATH", conf),
|
|
120
|
-
credentialsPath,
|
|
121
|
-
tokenPath,
|
|
122
|
-
statePath,
|
|
123
|
-
debug: boolVal("DEBUG", "DEBUG", conf, false),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
export async function loadEncryptionKey(path) {
|
|
127
|
-
if (path === undefined)
|
|
128
|
-
return undefined;
|
|
129
|
-
let hex;
|
|
130
|
-
try {
|
|
131
|
-
hex = (await readFileAsync(path, "utf8")).trim();
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
const e = err;
|
|
135
|
-
if (e.code === "ENOENT") {
|
|
136
|
-
throw new Error(`Encryption key file not found at ${path}. Run \`hydra-acp-archiver keygen\` to generate one.`);
|
|
137
|
-
}
|
|
138
|
-
throw err;
|
|
139
|
-
}
|
|
140
|
-
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
141
|
-
throw new Error(`Encryption key at ${path} is not a valid 64-character hex string.`);
|
|
142
|
-
}
|
|
143
|
-
return Buffer.from(hex, "hex");
|
|
144
|
-
}
|
|
145
|
-
export function loadLoginConfig() {
|
|
146
|
-
const hydraHome = process.env.HYDRA_ACP_HOME ?? resolve(homedir(), ".hydra-acp");
|
|
147
|
-
const conf = readConf(confPath(hydraHome));
|
|
148
|
-
const credentialsPath = str("HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS", "GOOGLE_CREDENTIALS", conf, resolve(hydraHome, "archiver-google-credentials.json"));
|
|
149
|
-
return {
|
|
150
|
-
hydraHome,
|
|
151
|
-
credentialsPath,
|
|
152
|
-
tokenPath: resolve(hydraHome, "archiver-google-token.json"),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
//# sourceMappingURL=config.js.map
|
|
1
|
+
import{readFile as C,readFileSync as l}from"node:fs";import{homedir as _,hostname as p}from"node:os";import{resolve as s}from"node:path";import{promisify as D}from"node:util";const H=D(C);function R(n){return process.env.HYDRA_ACP_ARCHIVER_CONF??s(n,"archiver.conf")}function P(n){return n==="~"||n.startsWith("~/")?_()+n.slice(1):n}function y(n){const e=new Map;for(const i of n.split(/\r?\n/)){const t=i.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r===-1)continue;const c=t.slice(0,r).trim();let o=t.slice(r+1).trim();(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),e.set(c,P(o))}return e}function h(n){try{return y(l(n,"utf8"))}catch(e){if(e.code==="ENOENT")return new Map;throw e}}const I=new Set(["1","true","yes","on","t"]);function a(n,e,i,t){return process.env[n]??i.get(e)??t}function g(n,e,i){return process.env[n]??i.get(e)}function d(n,e,i,t){const r=process.env[n]??i.get(e);if(!r)return t;const c=Number.parseInt(r,10);return Number.isFinite(c)?c:t}function m(n,e,i,t){const r=process.env[n]??i.get(e);return r===void 0?t:I.has(r.toLowerCase())}function O(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 N(n){const e=n.toLowerCase();if(e==="google-drive"||e==="fs"||e==="s3")return e;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",e=process.env.HYDRA_ACP_TOKEN??"";if(!e)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??O(n),t=process.env.HYDRA_ACP_HOME??s(_(),".hydra-acp"),r=h(R(t)),c=a("HYDRA_ACP_ARCHIVER_CONFIG","CONFIG",r,s(t,"archiver.config.js")),o=a("HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS","GOOGLE_CREDENTIALS",r,s(t,"archiver-google-credentials.json")),u=s(t,"archiver-google-token.json"),A=s(t,"archiver-state.json"),f=N(a("HYDRA_ACP_ARCHIVER_BACKEND","BACKEND",r,"google-drive")),E=a("HYDRA_ACP_ARCHIVER_S3_BUCKET","S3_BUCKET",r,"");if(f==="s3"&&!E)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:e,hydraHome:t,hydraPollIntervalMs:d("HYDRA_ACP_ARCHIVER_POLL_MS","POLL_MS",r,2e3),ruleConfigPath:c,uploadDebounceMs:d("HYDRA_ACP_ARCHIVER_DEBOUNCE_MS","DEBOUNCE_MS",r,5e3),pullIntervalMs:d("HYDRA_ACP_ARCHIVER_PULL_MS","PULL_MS",r,6e4),backend:f,driveFolderName:a("HYDRA_ACP_ARCHIVER_DRIVE_FOLDER","DRIVE_FOLDER",r,"hydra-acp-archive"),fsDir:a("HYDRA_ACP_ARCHIVER_FS_DIR","FS_DIR",r,s(t,"archive")),s3Bucket:E,s3Region:g("HYDRA_ACP_ARCHIVER_S3_REGION","S3_REGION",r),s3Endpoint:g("HYDRA_ACP_ARCHIVER_S3_ENDPOINT","S3_ENDPOINT",r),prefix:a("HYDRA_ACP_ARCHIVER_PREFIX","PREFIX",r,""),hostId:(g("HYDRA_ACP_ARCHIVER_HOST_ID","HOST_ID",r)??p()).toLowerCase().replace(/[^a-z0-9-]/g,"-"),encryptionKeyPath:g("HYDRA_ACP_ARCHIVER_KEY_PATH","KEY_PATH",r),credentialsPath:o,tokenPath:u,statePath:A,toolContent:v(a("HYDRA_ACP_ARCHIVER_TOOL_CONTENT","TOOL_CONTENT",r,"inline")),debug:m("DEBUG","DEBUG",r,!1)}}function v(n){return n==="references"||n==="summary"?n:"inline"}async function M(n){if(n===void 0)return;let e;try{e=(await H(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(e))throw new Error(`Encryption key at ${n} is not a valid 64-character hex string.`);return Buffer.from(e,"hex")}function V(){const n=process.env.HYDRA_ACP_HOME??s(_(),".hydra-acp"),e=h(R(n)),i=a("HYDRA_ACP_ARCHIVER_GOOGLE_CREDENTIALS","GOOGLE_CREDENTIALS",e,s(n,"archiver-google-credentials.json"));return{hydraHome:n,credentialsPath:i,tokenPath:s(n,"archiver-google-token.json")}}export{Y as loadConfig,M as loadEncryptionKey,V as loadLoginConfig};
|
package/dist/daemon.js
CHANGED
|
@@ -1,74 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// archiver uses. The daemon's WebSocket surface is handled in src/acp/.
|
|
3
|
-
import { logger } from "./util/log.js";
|
|
4
|
-
const log = logger("daemon");
|
|
5
|
-
export class DaemonClient {
|
|
6
|
-
opts;
|
|
7
|
-
constructor(opts) {
|
|
8
|
-
this.opts = opts;
|
|
9
|
-
}
|
|
10
|
-
async listSessionIds() {
|
|
11
|
-
const r = await fetch(`${this.opts.daemonUrl}/v1/sessions`, {
|
|
12
|
-
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
13
|
-
});
|
|
14
|
-
if (!r.ok) {
|
|
15
|
-
throw new Error(`list sessions: HTTP ${r.status}`);
|
|
16
|
-
}
|
|
17
|
-
const body = (await r.json());
|
|
18
|
-
const out = new Set();
|
|
19
|
-
for (const s of body.sessions) {
|
|
20
|
-
if (typeof s.sessionId === "string") {
|
|
21
|
-
out.add(s.sessionId);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return out;
|
|
25
|
-
}
|
|
26
|
-
async exportSession(sessionId) {
|
|
27
|
-
const url = `${this.opts.daemonUrl}/v1/sessions/${encodeURIComponent(sessionId)}/export`;
|
|
28
|
-
const r = await fetch(url, {
|
|
29
|
-
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
30
|
-
});
|
|
31
|
-
if (!r.ok) {
|
|
32
|
-
const text = await safeBody(r);
|
|
33
|
-
throw new Error(`export ${sessionId}: HTTP ${r.status} ${text}`);
|
|
34
|
-
}
|
|
35
|
-
return (await r.json());
|
|
36
|
-
}
|
|
37
|
-
async importBundle(bundle, opts = {}) {
|
|
38
|
-
const body = { bundle };
|
|
39
|
-
if (opts.replace !== undefined) {
|
|
40
|
-
body.replace = opts.replace;
|
|
41
|
-
}
|
|
42
|
-
if (opts.cwd !== undefined) {
|
|
43
|
-
body.cwd = opts.cwd;
|
|
44
|
-
}
|
|
45
|
-
const r = await fetch(`${this.opts.daemonUrl}/v1/sessions/import`, {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: {
|
|
48
|
-
Authorization: `Bearer ${this.opts.token}`,
|
|
49
|
-
"content-type": "application/json",
|
|
50
|
-
},
|
|
51
|
-
body: JSON.stringify(body),
|
|
52
|
-
});
|
|
53
|
-
if (!r.ok) {
|
|
54
|
-
const text = await safeBody(r);
|
|
55
|
-
throw new Error(`import lineage=${bundle.session.lineageId}: HTTP ${r.status} ${text}`);
|
|
56
|
-
}
|
|
57
|
-
const out = (await r.json());
|
|
58
|
-
if (!out.sessionId) {
|
|
59
|
-
log.warn(`import returned no sessionId for lineage ${bundle.session.lineageId}`);
|
|
60
|
-
return { sessionId: "" };
|
|
61
|
-
}
|
|
62
|
-
return { sessionId: out.sessionId };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async function safeBody(r) {
|
|
66
|
-
try {
|
|
67
|
-
const t = await r.text();
|
|
68
|
-
return t.slice(0, 500);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
//# sourceMappingURL=daemon.js.map
|
|
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};
|
package/dist/discovery.js
CHANGED
|
@@ -1,82 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const log = logger("discovery");
|
|
3
|
-
const DEFAULT_POLL_MS = 2_000;
|
|
4
|
-
export class HydraDiscovery {
|
|
5
|
-
opts;
|
|
6
|
-
timer;
|
|
7
|
-
known = new Map();
|
|
8
|
-
stopped = false;
|
|
9
|
-
inFlight = false;
|
|
10
|
-
constructor(opts) {
|
|
11
|
-
this.opts = opts;
|
|
12
|
-
}
|
|
13
|
-
start() {
|
|
14
|
-
log.info(`polling ${this.opts.daemonUrl}/v1/sessions every ${this.opts.pollIntervalMs ?? DEFAULT_POLL_MS}ms`);
|
|
15
|
-
void this.poll();
|
|
16
|
-
this.timer = setInterval(() => {
|
|
17
|
-
void this.poll();
|
|
18
|
-
}, this.opts.pollIntervalMs ?? DEFAULT_POLL_MS);
|
|
19
|
-
}
|
|
20
|
-
stop() {
|
|
21
|
-
this.stopped = true;
|
|
22
|
-
if (this.timer) {
|
|
23
|
-
clearInterval(this.timer);
|
|
24
|
-
this.timer = undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async poll() {
|
|
28
|
-
if (this.stopped || this.inFlight) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
this.inFlight = true;
|
|
32
|
-
try {
|
|
33
|
-
const r = await fetch(`${this.opts.daemonUrl}/v1/sessions`, {
|
|
34
|
-
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
35
|
-
});
|
|
36
|
-
if (!r.ok) {
|
|
37
|
-
log.warn(`daemon /v1/sessions returned ${r.status}`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const body = (await r.json());
|
|
41
|
-
const seen = new Map();
|
|
42
|
-
for (const s of body.sessions) {
|
|
43
|
-
if (s.status !== "live") {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
seen.set(s.sessionId, s);
|
|
47
|
-
}
|
|
48
|
-
for (const [id, s] of seen) {
|
|
49
|
-
if (!this.known.has(id)) {
|
|
50
|
-
this.known.set(id, s);
|
|
51
|
-
try {
|
|
52
|
-
this.opts.onAdd(s);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
log.warn(`onAdd error for ${id}: ${err.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
this.known.set(id, s);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
for (const id of [...this.known.keys()]) {
|
|
63
|
-
if (!seen.has(id)) {
|
|
64
|
-
this.known.delete(id);
|
|
65
|
-
try {
|
|
66
|
-
this.opts.onRemove(id);
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
log.warn(`onRemove error for ${id}: ${err.message}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
log.debug(`poll error: ${err.message}`);
|
|
76
|
-
}
|
|
77
|
-
finally {
|
|
78
|
-
this.inFlight = false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
//# sourceMappingURL=discovery.js.map
|
|
1
|
+
import{logger as d}from"./util/log.js";const o=d("discovery"),r=2e3;class h{constructor(t){this.opts=t}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 t=await fetch(`${this.opts.daemonUrl}/v1/sessions`,{headers:{Authorization:`Bearer ${this.opts.token}`}});if(!t.ok){o.warn(`daemon /v1/sessions returned ${t.status}`);return}const n=await t.json(),i=new Map;for(const s of n.sessions)s.status==="live"&&i.set(s.sessionId,s);for(const[s,e]of i)if(this.known.has(s))this.known.set(s,e);else{this.known.set(s,e);try{this.opts.onAdd(e)}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(e){o.warn(`onRemove error for ${s}: ${e.message}`)}}}catch(t){o.debug(`poll error: ${t.message}`)}finally{this.inFlight=!1}}}}export{h as HydraDiscovery};
|
package/dist/envelope.js
CHANGED
|
@@ -1,123 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const SYNC_VERSION = 1;
|
|
3
|
-
// Canonical JSON for hashing: object keys sorted recursively. Arrays are
|
|
4
|
-
// left in order — the bundle's `history` array is order-significant.
|
|
5
|
-
function canonicalize(value) {
|
|
6
|
-
if (value === null || typeof value !== "object") {
|
|
7
|
-
return JSON.stringify(value);
|
|
8
|
-
}
|
|
9
|
-
if (Array.isArray(value)) {
|
|
10
|
-
return `[${value.map(canonicalize).join(",")}]`;
|
|
11
|
-
}
|
|
12
|
-
const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
13
|
-
return `{${entries
|
|
14
|
-
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`)
|
|
15
|
-
.join(",")}}`;
|
|
16
|
-
}
|
|
17
|
-
// Fields that don't represent the session's content — they change on
|
|
18
|
-
// every export or are regenerated by the daemon on import. Excluding
|
|
19
|
-
// them makes the hash stable across the export → import → export
|
|
20
|
-
// round-trip, which is what stops the multi-machine ping-pong loop.
|
|
21
|
-
const BUNDLE_EPHEMERAL = new Set(["exportedAt", "exportedFrom"]);
|
|
22
|
-
const SESSION_EPHEMERAL = new Set([
|
|
23
|
-
"sessionId",
|
|
24
|
-
"upstreamSessionId",
|
|
25
|
-
"createdAt",
|
|
26
|
-
"updatedAt",
|
|
27
|
-
]);
|
|
28
|
-
function stripEphemeral(bundle) {
|
|
29
|
-
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
30
|
-
return bundle;
|
|
31
|
-
}
|
|
32
|
-
const out = {};
|
|
33
|
-
for (const [k, v] of Object.entries(bundle)) {
|
|
34
|
-
if (BUNDLE_EPHEMERAL.has(k)) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (k === "session" && v && typeof v === "object" && !Array.isArray(v)) {
|
|
38
|
-
const sessionOut = {};
|
|
39
|
-
for (const [sk, sv] of Object.entries(v)) {
|
|
40
|
-
if (SESSION_EPHEMERAL.has(sk)) {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
sessionOut[sk] = sv;
|
|
44
|
-
}
|
|
45
|
-
out[k] = sessionOut;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
out[k] = v;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
export function hashBundle(bundle) {
|
|
54
|
-
return ("sha256:" +
|
|
55
|
-
createHash("sha256")
|
|
56
|
-
.update(canonicalize(stripEphemeral(bundle)))
|
|
57
|
-
.digest("hex"));
|
|
58
|
-
}
|
|
59
|
-
export function wrap(bundle, lineageId, host, now = new Date()) {
|
|
60
|
-
return {
|
|
61
|
-
syncVersion: SYNC_VERSION,
|
|
62
|
-
lineageId,
|
|
63
|
-
uploadedAt: now.toISOString(),
|
|
64
|
-
uploadedBy: host,
|
|
65
|
-
bundleHash: hashBundle(bundle),
|
|
66
|
-
bundle,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export function unwrap(raw) {
|
|
70
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
71
|
-
throw new Error("envelope: expected JSON object");
|
|
72
|
-
}
|
|
73
|
-
const r = raw;
|
|
74
|
-
if (r.syncVersion !== SYNC_VERSION) {
|
|
75
|
-
throw new Error(`envelope: unsupported syncVersion ${String(r.syncVersion)}; expected ${SYNC_VERSION}`);
|
|
76
|
-
}
|
|
77
|
-
if (typeof r.lineageId !== "string" || !r.lineageId) {
|
|
78
|
-
throw new Error("envelope: missing lineageId");
|
|
79
|
-
}
|
|
80
|
-
if (typeof r.uploadedAt !== "string" || !r.uploadedAt) {
|
|
81
|
-
throw new Error("envelope: missing uploadedAt");
|
|
82
|
-
}
|
|
83
|
-
if (typeof r.bundleHash !== "string" || !r.bundleHash) {
|
|
84
|
-
throw new Error("envelope: missing bundleHash");
|
|
85
|
-
}
|
|
86
|
-
const by = r.uploadedBy;
|
|
87
|
-
if (!by ||
|
|
88
|
-
typeof by.host !== "string" ||
|
|
89
|
-
typeof by.user !== "string") {
|
|
90
|
-
throw new Error("envelope: missing uploadedBy.{host,user}");
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
syncVersion: SYNC_VERSION,
|
|
94
|
-
lineageId: r.lineageId,
|
|
95
|
-
uploadedAt: r.uploadedAt,
|
|
96
|
-
uploadedBy: { host: by.host, user: by.user },
|
|
97
|
-
bundleHash: r.bundleHash,
|
|
98
|
-
bundle: r.bundle,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
export function serialize(envelope) {
|
|
102
|
-
return Buffer.from(JSON.stringify(envelope), "utf8");
|
|
103
|
-
}
|
|
104
|
-
export function deserialize(data) {
|
|
105
|
-
const text = typeof data === "string" ? data : data.toString("utf8");
|
|
106
|
-
return unwrap(JSON.parse(text));
|
|
107
|
-
}
|
|
108
|
-
export function keyFor(lineageId) {
|
|
109
|
-
return `${lineageId}.hydra.archive`;
|
|
110
|
-
}
|
|
111
|
-
const KEY_SUFFIX = ".hydra.archive";
|
|
112
|
-
// Reverse of keyFor — returns undefined for unrelated keys so callers
|
|
113
|
-
// can safely iterate a mixed backend listing. Handles an optional leading
|
|
114
|
-
// host segment (e.g. "alice-macbook/uuid.hydra.archive" → "uuid").
|
|
115
|
-
export function lineageFromKey(key) {
|
|
116
|
-
if (!key.endsWith(KEY_SUFFIX)) {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
const withoutSuffix = key.slice(0, -KEY_SUFFIX.length);
|
|
120
|
-
const slash = withoutSuffix.lastIndexOf("/");
|
|
121
|
-
return slash >= 0 ? withoutSuffix.slice(slash + 1) : withoutSuffix;
|
|
122
|
-
}
|
|
123
|
-
//# sourceMappingURL=envelope.js.map
|
|
1
|
+
import{createHash as f}from"node:crypto";const o=1;function s(n){return n===null||typeof n!="object"?JSON.stringify(n):Array.isArray(n)?`[${n.map(s).join(",")}]`:`{${Object.entries(n).sort(([r],[t])=>r<t?-1:r>t?1:0).map(([r,t])=>`${JSON.stringify(r)}:${s(t)}`).join(",")}}`}const p=new Set(["exportedAt","exportedFrom"]),a=new Set(["sessionId","upstreamSessionId","createdAt","updatedAt"]);function g(n){if(!n||typeof n!="object"||Array.isArray(n))return n;const e={};for(const[r,t]of Object.entries(n))if(!p.has(r))if(r==="session"&&t&&typeof t=="object"&&!Array.isArray(t)){const i={};for(const[u,c]of Object.entries(t))a.has(u)||(i[u]=c);e[r]=i}else e[r]=t;return e}function l(n){return"sha256:"+f("sha256").update(s(g(n))).digest("hex")}function w(n,e,r,t=new Date){return{syncVersion:o,lineageId:e,uploadedAt:t.toISOString(),uploadedBy:r,bundleHash:l(n),bundle:n}}function y(n){if(!n||typeof n!="object"||Array.isArray(n))throw new Error("envelope: expected JSON object");const e=n;if(e.syncVersion!==o)throw new Error(`envelope: unsupported syncVersion ${String(e.syncVersion)}; expected ${o}`);if(typeof e.lineageId!="string"||!e.lineageId)throw new Error("envelope: missing lineageId");if(typeof e.uploadedAt!="string"||!e.uploadedAt)throw new Error("envelope: missing uploadedAt");if(typeof e.bundleHash!="string"||!e.bundleHash)throw new Error("envelope: missing bundleHash");const r=e.uploadedBy;if(!r||typeof r.host!="string"||typeof r.user!="string")throw new Error("envelope: missing uploadedBy.{host,user}");return{syncVersion:o,lineageId:e.lineageId,uploadedAt:e.uploadedAt,uploadedBy:{host:r.host,user:r.user},bundleHash:e.bundleHash,bundle:e.bundle}}function S(n){return Buffer.from(JSON.stringify(n),"utf8")}function E(n){const e=typeof n=="string"?n:n.toString("utf8");return y(JSON.parse(e))}function A(n){return`${n}.hydra.archive`}const d=".hydra.archive";function x(n){if(!n.endsWith(d))return;const e=n.slice(0,-d.length),r=e.lastIndexOf("/");return r>=0?e.slice(r+1):e}export{o as SYNC_VERSION,E as deserialize,l as hashBundle,A as keyFor,x as lineageFromKey,S as serialize,y as unwrap,w as wrap};
|