hyperframes 0.8.79 → 0.8.81
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/cli.js +2650 -1988
- package/dist/fontLocalizeCli.js +222 -64
- package/dist/hyperframe-runtime.js +55 -55
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +55 -55
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +6 -25
- package/dist/skills/media-use/audio/references/remove-background.md +1 -1
- package/dist/skills/media-use/registry/localSemantic.js +6 -25
- package/dist/skills/media-use/scripts/lib/config-lock.d.mts +3 -0
- package/dist/skills/media-use/scripts/lib/config-lock.mjs +45 -0
- package/dist/skills/media-use/scripts/lib/config-lock.test.mjs +79 -0
- package/dist/skills/media-use/scripts/lib/telemetry.mjs +24 -8
- package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +63 -0
- package/dist/skills/media-use/scripts/lib/test-home.test.mjs +10 -0
- package/dist/skills/registry/localSemantic.js +6 -25
- package/dist/studio/assets/hyperframes-player-C68UawR3.js +464 -0
- package/dist/studio/assets/{index-ORZXYB0k.js → index-CMzPGv9P.js} +1 -1
- package/dist/studio/assets/{index-CJP92F4-.js → index-CS5SImRT.js} +1 -1
- package/dist/studio/assets/{index-DBKySIo7.js → index-hNVM9Pd2.js} +200 -197
- package/dist/studio/index.d.ts +18 -1
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +1673 -1348
- package/dist/studio/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/studio/assets/hyperframes-player-Bx_Kw4Ey.js +0 -464
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The one lock on ~/.hyperframes/config.json, for the CLI, its auto-update step and media-use. The auto-update step
|
|
2
|
+
// embeds this function's source, so it must use only its arguments and globals.
|
|
3
|
+
export function withFileLock(lockPath, fs, task) {
|
|
4
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
5
|
+
const started = Date.now();
|
|
6
|
+
for (;;) {
|
|
7
|
+
let fd;
|
|
8
|
+
try {
|
|
9
|
+
fd = fs.openSync(lockPath, "wx");
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error.code !== "EEXIST") throw error;
|
|
12
|
+
// Never taken over: a lock older than any hold was left by a process that stopped, and a person removes it.
|
|
13
|
+
let leftover = false;
|
|
14
|
+
try {
|
|
15
|
+
leftover = Date.now() - fs.statSync(lockPath).mtimeMs > 5000;
|
|
16
|
+
} catch {}
|
|
17
|
+
if (leftover || Date.now() - started > 10000)
|
|
18
|
+
throw Object.assign(
|
|
19
|
+
new Error(
|
|
20
|
+
`Settings are locked by another hyperframes process. If none is running, delete ${lockPath}`,
|
|
21
|
+
),
|
|
22
|
+
{ code: "HF_SETTINGS_LOCKED" },
|
|
23
|
+
);
|
|
24
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
fs.writeSync(fd, token);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fs.rmSync(lockPath, { force: true });
|
|
31
|
+
throw error;
|
|
32
|
+
} finally {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return task();
|
|
39
|
+
} finally {
|
|
40
|
+
// No code removes another's lock, so release needs no check (only a person deleting a live one defeats it).
|
|
41
|
+
try {
|
|
42
|
+
fs.rmSync(lockPath);
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { withFileLock } from "./config-lock.mjs";
|
|
8
|
+
|
|
9
|
+
function lockIn() {
|
|
10
|
+
const dir = fs.mkdtempSync(join(tmpdir(), "hf-config-lock-"));
|
|
11
|
+
return {
|
|
12
|
+
lock: join(dir, "config.json.lock"),
|
|
13
|
+
cleanup: () => fs.rmSync(dir, { recursive: true }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("returns the task's result even when the lock cannot be released", () => {
|
|
18
|
+
const { lock, cleanup } = lockIn();
|
|
19
|
+
try {
|
|
20
|
+
const refusing = {
|
|
21
|
+
...fs,
|
|
22
|
+
rmSync(path, options) {
|
|
23
|
+
if (path === lock) throw Object.assign(new Error("EPERM"), { code: "EPERM" });
|
|
24
|
+
return fs.rmSync(path, options);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
assert.equal(
|
|
28
|
+
withFileLock(lock, refusing, () => "saved"),
|
|
29
|
+
"saved",
|
|
30
|
+
);
|
|
31
|
+
} finally {
|
|
32
|
+
cleanup();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("refuses a lock left by a process that stopped, names it, and never removes it", () => {
|
|
37
|
+
const { lock, cleanup } = lockIn();
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(lock, "dead");
|
|
40
|
+
const past = new Date(Date.now() - 60_000);
|
|
41
|
+
fs.utimesSync(lock, past, past);
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
let ran = false;
|
|
44
|
+
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => withFileLock(lock, fs, () => (ran = true)),
|
|
47
|
+
(error) => error.code === "HF_SETTINGS_LOCKED" && error.message.includes(lock),
|
|
48
|
+
);
|
|
49
|
+
assert.ok(Date.now() - started < 1000);
|
|
50
|
+
assert.equal(ran, false);
|
|
51
|
+
assert.equal(fs.readFileSync(lock, "utf8"), "dead");
|
|
52
|
+
} finally {
|
|
53
|
+
cleanup();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("waits for a process that holds the lock, then takes it", async () => {
|
|
58
|
+
const { lock, cleanup } = lockIn();
|
|
59
|
+
try {
|
|
60
|
+
const script = `
|
|
61
|
+
const fs = require("fs");
|
|
62
|
+
const lock = process.argv[1];
|
|
63
|
+
fs.writeFileSync(lock, "other", { flag: "wx" });
|
|
64
|
+
process.stdout.write("locked\\n");
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
fs.writeFileSync(lock + ".released", String(Date.now()));
|
|
67
|
+
fs.rmSync(lock);
|
|
68
|
+
}, 300);`;
|
|
69
|
+
const other = spawn(process.execPath, ["-e", script, lock]);
|
|
70
|
+
await new Promise((ready) => other.stdout.once("data", ready));
|
|
71
|
+
|
|
72
|
+
const ranAt = withFileLock(lock, fs, () => Date.now());
|
|
73
|
+
|
|
74
|
+
assert.ok(ranAt >= Number(fs.readFileSync(`${lock}.released`, "utf8")));
|
|
75
|
+
await new Promise((exited) => other.once("exit", exited));
|
|
76
|
+
} finally {
|
|
77
|
+
cleanup();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// never carry intent text, file names, or paths.
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { withFileLock } from "./config-lock.mjs";
|
|
8
10
|
import { globalMediaDir } from "./media-home.mjs";
|
|
9
11
|
|
|
10
12
|
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
@@ -62,10 +64,24 @@ function readSharedConfig() {
|
|
|
62
64
|
return {};
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// Best effort: skipped when the lock cannot be taken or the file cannot be read, never written unlocked.
|
|
68
|
+
function updateSharedConfig(patch) {
|
|
69
|
+
const file = sharedConfigPath();
|
|
70
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
71
|
+
try {
|
|
72
|
+
withFileLock(`${file}.lock`, fs, () => {
|
|
73
|
+
let config = {};
|
|
74
|
+
try {
|
|
75
|
+
config = JSON.parse(readFileSync(file, "utf8"));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code !== "ENOENT") return;
|
|
78
|
+
}
|
|
79
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return;
|
|
80
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
81
|
+
writeFileSync(tmp, JSON.stringify({ ...config, ...patch }, null, 2) + "\n", { mode: 0o600 });
|
|
82
|
+
renameSync(tmp, file);
|
|
83
|
+
});
|
|
84
|
+
} catch {}
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
// Adopt a pre-existing media-use-only id (~/.media/anon-id from before this
|
|
@@ -93,7 +109,7 @@ function anonymousId() {
|
|
|
93
109
|
return config.anonymousId.trim();
|
|
94
110
|
}
|
|
95
111
|
const id = legacyMediaAnonId() || randomUUID();
|
|
96
|
-
|
|
112
|
+
updateSharedConfig({ anonymousId: id });
|
|
97
113
|
return id;
|
|
98
114
|
} catch {
|
|
99
115
|
return "anon"; // best-effort; a shared bucket is fine if the fs is read-only
|
|
@@ -133,7 +149,7 @@ function showTelemetryNotice() {
|
|
|
133
149
|
"If you sign in to HeyGen, usage links to your account email or username. Opt out with HYPERFRAMES_NO_TELEMETRY=1 or DO_NOT_TRACK=1.",
|
|
134
150
|
].join("\n"),
|
|
135
151
|
);
|
|
136
|
-
|
|
152
|
+
updateSharedConfig({ telemetryNoticeShown: true });
|
|
137
153
|
} catch {
|
|
138
154
|
// notice is best-effort; never surface into the command
|
|
139
155
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { test } from "node:test";
|
|
3
4
|
import {
|
|
4
5
|
existsSync,
|
|
@@ -372,3 +373,65 @@ test("read-only telemetry state degrades without throwing", async () => {
|
|
|
372
373
|
__resetTelemetryForTest();
|
|
373
374
|
}
|
|
374
375
|
});
|
|
376
|
+
|
|
377
|
+
async function trackQuietly(home) {
|
|
378
|
+
const originalFetch = globalThis.fetch;
|
|
379
|
+
const originalError = console.error;
|
|
380
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
381
|
+
console.error = () => {};
|
|
382
|
+
try {
|
|
383
|
+
withoutTelemetryOptOut();
|
|
384
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
385
|
+
return readFileSync(join(home, ".hyperframes/config.json"), "utf8");
|
|
386
|
+
} finally {
|
|
387
|
+
globalThis.fetch = originalFetch;
|
|
388
|
+
console.error = originalError;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
test("waits for another process's locked settings write, then keeps the no it saved", async () => {
|
|
393
|
+
const savedEnv = { ...process.env };
|
|
394
|
+
const { root, home } = sandbox();
|
|
395
|
+
const dir = join(home, ".hyperframes");
|
|
396
|
+
mkdirSync(dir, { recursive: true });
|
|
397
|
+
const holder = spawn(process.execPath, [
|
|
398
|
+
"-e",
|
|
399
|
+
`const fs = require("fs");
|
|
400
|
+
const dir = process.argv[1];
|
|
401
|
+
fs.writeFileSync(dir + "/config.json.lock", "", { flag: "wx" });
|
|
402
|
+
process.stdout.write("locked\\n");
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
fs.writeFileSync(dir + "/other.tmp", JSON.stringify({ localEmbeddingEnabled: false }));
|
|
405
|
+
fs.renameSync(dir + "/other.tmp", dir + "/config.json");
|
|
406
|
+
fs.rmSync(dir + "/config.json.lock");
|
|
407
|
+
}, 1000);`,
|
|
408
|
+
dir,
|
|
409
|
+
]);
|
|
410
|
+
const exited = new Promise((done) => holder.on("exit", done));
|
|
411
|
+
try {
|
|
412
|
+
await new Promise((locked) => holder.stdout.once("data", locked));
|
|
413
|
+
const saved = JSON.parse(await trackQuietly(home));
|
|
414
|
+
await exited;
|
|
415
|
+
assert.equal(saved.localEmbeddingEnabled, false);
|
|
416
|
+
assert.equal(saved.telemetryNoticeShown, true);
|
|
417
|
+
assert.equal(typeof saved.anonymousId, "string");
|
|
418
|
+
} finally {
|
|
419
|
+
restoreEnv(savedEnv);
|
|
420
|
+
rmSync(root, { recursive: true, force: true });
|
|
421
|
+
__resetTelemetryForTest();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("leaves a settings file it cannot read untouched", async () => {
|
|
426
|
+
const savedEnv = { ...process.env };
|
|
427
|
+
const { root, home } = sandbox();
|
|
428
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
429
|
+
writeFileSync(join(home, ".hyperframes/config.json"), "{not json");
|
|
430
|
+
try {
|
|
431
|
+
assert.equal(await trackQuietly(home), "{not json");
|
|
432
|
+
} finally {
|
|
433
|
+
restoreEnv(savedEnv);
|
|
434
|
+
rmSync(root, { recursive: true, force: true });
|
|
435
|
+
__resetTelemetryForTest();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
4
|
+
import { sep } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
|
|
7
|
+
test("the skills test run has a home folder of its own, not the user's", () => {
|
|
8
|
+
assert.ok(realpathSync(homedir()).startsWith(realpathSync(tmpdir()) + sep));
|
|
9
|
+
assert.notEqual(homedir(), userInfo().homedir);
|
|
10
|
+
});
|
|
@@ -50,30 +50,10 @@ var CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
|
|
|
50
50
|
function optionalPackageDir(name, cacheDir = CACHE_DIR) {
|
|
51
51
|
return join(cacheDir, `${name.replace("/", "__")}@${OPTIONAL_PACKAGES[name]}`);
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const dir = optionalPackageDir(name, deps.cacheDir);
|
|
57
|
-
const installed = deps.loadInstalled(dir, name);
|
|
58
|
-
if (installed !== null) return installed;
|
|
59
|
-
const version = OPTIONAL_PACKAGES[name];
|
|
60
|
-
deps.log(`installing ${name} for ${feature}, once`);
|
|
61
|
-
try {
|
|
62
|
-
await deps.install(dir, name, version);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
const output = err.message.trim();
|
|
65
|
-
const advice = NETWORK_FAILURE.test(output) ? "Check your network connection, then retry" : "Retry";
|
|
66
|
-
throw new Error(
|
|
67
|
-
`${feature} needs ${name}, and installing it failed (${output.split("\n")[0]}). ${advice}, or install it yourself: npm install ${name}@${version} --prefix "${dir}"`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const loaded = deps.loadInstalled(dir, name);
|
|
71
|
-
if (loaded === null) {
|
|
72
|
-
throw new Error(`${name}@${version} installed into ${dir} but could not be loaded.`);
|
|
73
|
-
}
|
|
74
|
-
return loaded;
|
|
53
|
+
function loadInstalledOptionalPackage(name, deps = defaultDeps) {
|
|
54
|
+
const found = deps.loadBesideCli(name) ?? deps.loadInstalled(optionalPackageDir(name, deps.cacheDir), name);
|
|
55
|
+
return found;
|
|
75
56
|
}
|
|
76
|
-
var NETWORK_FAILURE = /ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|ECONNREFUSED|network/i;
|
|
77
57
|
function manifestPath(dir, name) {
|
|
78
58
|
return join(dir, "node_modules", name, "package.json");
|
|
79
59
|
}
|
|
@@ -187,6 +167,7 @@ import { Transform } from "stream";
|
|
|
187
167
|
import { pipeline } from "stream/promises";
|
|
188
168
|
|
|
189
169
|
// src/telemetry/config.ts
|
|
170
|
+
import * as fs from "fs";
|
|
190
171
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
191
172
|
import { join as join2 } from "path";
|
|
192
173
|
import { homedir as homedir2 } from "os";
|
|
@@ -333,9 +314,9 @@ function isChinese(code) {
|
|
|
333
314
|
|
|
334
315
|
// src/registry/localEmbedder.ts
|
|
335
316
|
var MAX_TOKENS = 512;
|
|
336
|
-
var FEATURE = "on-device search";
|
|
337
317
|
async function loadLocalEmbedder() {
|
|
338
|
-
const ort =
|
|
318
|
+
const ort = loadInstalledOptionalPackage("onnxruntime-node");
|
|
319
|
+
if (ort === null) throw new Error("the on-device runtime is not installed yet");
|
|
339
320
|
const config = configFromTokenizerJson(readFileSync4(localTokenizerPath(), "utf-8"));
|
|
340
321
|
const session = await ort.InferenceSession.create(localModelPath());
|
|
341
322
|
return {
|