twokey 1.0.10 → 1.0.12
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/bin/postinstall.js +24 -6
- package/bin/twokey.js +176 -42
- package/package.json +2 -1
package/bin/postinstall.js
CHANGED
|
@@ -16,10 +16,6 @@ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
|
16
16
|
const sudoUser = process.env.SUDO_USER;
|
|
17
17
|
|
|
18
18
|
if (isRoot && sudoUser && sudoUser !== "root") {
|
|
19
|
-
if (isDesktopRunningForUser(sudoUser)) {
|
|
20
|
-
process.exit(0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
const uid = resolveUid(sudoUser);
|
|
24
20
|
const homeDir = resolveHomeDir(sudoUser);
|
|
25
21
|
const env = { ...process.env };
|
|
@@ -32,9 +28,24 @@ if (isRoot && sudoUser && sudoUser !== "root") {
|
|
|
32
28
|
env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${env.XDG_RUNTIME_DIR}/bus`;
|
|
33
29
|
}
|
|
34
30
|
|
|
31
|
+
const runtimePrep = spawn(
|
|
32
|
+
"sudo",
|
|
33
|
+
["-u", sudoUser, "-H", process.execPath, cliPath, "--prepare-runtime-only", "--quiet"],
|
|
34
|
+
{
|
|
35
|
+
stdio: "ignore",
|
|
36
|
+
shell: false,
|
|
37
|
+
env,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
runtimePrep.on("error", () => undefined);
|
|
41
|
+
|
|
42
|
+
if (isDesktopRunningForUser(sudoUser)) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
const delegated = spawn(
|
|
36
47
|
"sudo",
|
|
37
|
-
["-u", sudoUser, "-H", process.execPath, cliPath, "--desktop", "--enable-autostart"],
|
|
48
|
+
["-u", sudoUser, "-H", process.execPath, cliPath, "--prepare-runtime", "--desktop", "--enable-autostart"],
|
|
38
49
|
{
|
|
39
50
|
stdio: "ignore",
|
|
40
51
|
shell: false,
|
|
@@ -45,11 +56,18 @@ if (isRoot && sudoUser && sudoUser !== "root") {
|
|
|
45
56
|
delegated.on("error", () => process.exit(0));
|
|
46
57
|
delegated.on("close", () => process.exit(0));
|
|
47
58
|
} else {
|
|
59
|
+
const runtimePrep = spawn(process.execPath, [cliPath, "--prepare-runtime-only", "--quiet"], {
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
shell: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
runtimePrep.on("error", () => undefined);
|
|
65
|
+
|
|
48
66
|
if (isDesktopRunningForUser(process.env.USER || "")) {
|
|
49
67
|
process.exit(0);
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
const child = spawn(process.execPath, [cliPath, "--desktop", "--enable-autostart"], {
|
|
70
|
+
const child = spawn(process.execPath, [cliPath, "--prepare-runtime", "--desktop", "--enable-autostart"], {
|
|
53
71
|
stdio: "ignore",
|
|
54
72
|
shell: false,
|
|
55
73
|
});
|
package/bin/twokey.js
CHANGED
|
@@ -6,16 +6,21 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import readline from "node:readline";
|
|
8
8
|
|
|
9
|
-
const VERSION = process.env.npm_package_version || "1.0.
|
|
9
|
+
const VERSION = process.env.npm_package_version || "1.0.12";
|
|
10
10
|
const DEFAULT_MODEL = process.env.TWOKEY_OLLAMA_MODEL || "qwen2.5:3b";
|
|
11
11
|
const DEFAULT_OLLAMA_URL = process.env.TWOKEY_OLLAMA_URL || "http://127.0.0.1:11434";
|
|
12
12
|
const LATEST_RELEASE_API = "https://api.github.com/repos/meinzeug/twokey/releases/latest";
|
|
13
13
|
const APPIMAGE_DIR = path.join(os.homedir(), ".local", "share", "twokey", "bin");
|
|
14
14
|
const APPIMAGE_PATH = path.join(APPIMAGE_DIR, "twokey-ai.AppImage");
|
|
15
15
|
const APPIMAGE_META_PATH = path.join(APPIMAGE_DIR, "twokey-ai.meta.json");
|
|
16
|
+
const MANAGED_FFMPEG_PATH = path.join(APPIMAGE_DIR, "ffmpeg");
|
|
17
|
+
const WHISPER_VENV_DIR = path.join(os.homedir(), ".local", "share", "twokey", "whisper-venv");
|
|
18
|
+
const MANAGED_WHISPER_PATH = path.join(WHISPER_VENV_DIR, "bin", "whisper");
|
|
16
19
|
|
|
17
20
|
const args = process.argv.slice(2);
|
|
18
21
|
const QUIET = args.includes("--quiet");
|
|
22
|
+
const PREPARE_RUNTIME = args.includes("--prepare-runtime");
|
|
23
|
+
const PREPARE_RUNTIME_ONLY = args.includes("--prepare-runtime-only");
|
|
19
24
|
|
|
20
25
|
if (args.includes("--help") || args.includes("-h")) {
|
|
21
26
|
printHelp();
|
|
@@ -27,50 +32,16 @@ if (args.includes("--version") || args.includes("-v")) {
|
|
|
27
32
|
process.exit(0);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await ensureUserService(startedCommand);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (!QUIET) {
|
|
38
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
-
console.warn(`Autostart setup skipped: ${message}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
35
|
+
if (PREPARE_RUNTIME_ONLY) {
|
|
36
|
+
ensureRuntimeDependencies()
|
|
37
|
+
.then(() => process.exit(0))
|
|
38
|
+
.catch((error) => {
|
|
43
39
|
if (!QUIET) {
|
|
44
|
-
console.
|
|
40
|
+
console.warn(`Runtime setup skipped: ${error.message || String(error)}`);
|
|
45
41
|
}
|
|
46
42
|
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.error("No native desktop binary found in PATH.");
|
|
50
|
-
console.error("Install the .deb/.AppImage release and ensure 'twokey-ai' is available in PATH.");
|
|
51
|
-
}
|
|
52
|
-
process.exit(1);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const onceIndex = args.findIndex((value) => value === "--once");
|
|
57
|
-
if (onceIndex >= 0) {
|
|
58
|
-
const prompt = args.slice(onceIndex + 1).join(" ").trim();
|
|
59
|
-
if (!prompt) {
|
|
60
|
-
console.error("Missing prompt after --once");
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
runSinglePrompt(prompt).catch((error) => {
|
|
65
|
-
console.error(error.message || String(error));
|
|
66
|
-
process.exit(1);
|
|
67
|
-
});
|
|
68
|
-
} else if (args.includes("--cli")) {
|
|
69
|
-
startRepl().catch((error) => {
|
|
70
|
-
console.error(error.message || String(error));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
});
|
|
73
|
-
} else {
|
|
43
|
+
});
|
|
44
|
+
} else if (args.includes("--desktop")) {
|
|
74
45
|
launchDesktopApp().then(async (startedCommand) => {
|
|
75
46
|
if (startedCommand) {
|
|
76
47
|
if (args.includes("--enable-autostart")) {
|
|
@@ -96,6 +67,51 @@ if (onceIndex >= 0) {
|
|
|
96
67
|
}
|
|
97
68
|
process.exit(1);
|
|
98
69
|
});
|
|
70
|
+
} else {
|
|
71
|
+
const onceIndex = args.findIndex((value) => value === "--once");
|
|
72
|
+
if (onceIndex >= 0) {
|
|
73
|
+
const prompt = args.slice(onceIndex + 1).join(" ").trim();
|
|
74
|
+
if (!prompt) {
|
|
75
|
+
console.error("Missing prompt after --once");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runSinglePrompt(prompt).catch((error) => {
|
|
80
|
+
console.error(error.message || String(error));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
} else if (args.includes("--cli")) {
|
|
84
|
+
startRepl().catch((error) => {
|
|
85
|
+
console.error(error.message || String(error));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
launchDesktopApp().then(async (startedCommand) => {
|
|
90
|
+
if (startedCommand) {
|
|
91
|
+
if (args.includes("--enable-autostart")) {
|
|
92
|
+
try {
|
|
93
|
+
await ensureUserService(startedCommand);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!QUIET) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
console.warn(`Autostart setup skipped: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!QUIET) {
|
|
102
|
+
console.log("TwoKey desktop app started in background.");
|
|
103
|
+
}
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!QUIET) {
|
|
108
|
+
console.error("Could not start desktop app.");
|
|
109
|
+
console.error("Tried system binaries and auto-download from GitHub Releases.");
|
|
110
|
+
console.error("Use 'twokey --cli' to run terminal mode.");
|
|
111
|
+
}
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
async function runSinglePrompt(prompt) {
|
|
@@ -191,6 +207,17 @@ async function askOllama(prompt) {
|
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
async function launchDesktopApp() {
|
|
210
|
+
if (PREPARE_RUNTIME) {
|
|
211
|
+
try {
|
|
212
|
+
await ensureRuntimeDependencies();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!QUIET) {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
console.warn(`Runtime dependencies could not be fully prepared: ${message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
194
221
|
let appImageReady = false;
|
|
195
222
|
try {
|
|
196
223
|
appImageReady = await ensureLocalAppImage();
|
|
@@ -330,6 +357,7 @@ function spawnDetached(command) {
|
|
|
330
357
|
detached: true,
|
|
331
358
|
stdio: "ignore",
|
|
332
359
|
shell: false,
|
|
360
|
+
env: withManagedBinPath(process.env),
|
|
333
361
|
});
|
|
334
362
|
|
|
335
363
|
child.once("spawn", () => {
|
|
@@ -343,6 +371,109 @@ function spawnDetached(command) {
|
|
|
343
371
|
});
|
|
344
372
|
}
|
|
345
373
|
|
|
374
|
+
async function ensureRuntimeDependencies() {
|
|
375
|
+
await fs.promises.mkdir(APPIMAGE_DIR, { recursive: true });
|
|
376
|
+
await ensureManagedFfmpeg();
|
|
377
|
+
await ensureManagedWhisperCli();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function ensureManagedFfmpeg() {
|
|
381
|
+
if (await hasExecutable(MANAGED_FFMPEG_PATH)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let sourcePath = "";
|
|
386
|
+
try {
|
|
387
|
+
const module = await import("@ffmpeg-installer/ffmpeg");
|
|
388
|
+
sourcePath =
|
|
389
|
+
module?.default?.path
|
|
390
|
+
|| module?.path
|
|
391
|
+
|| module?.default?.default?.path
|
|
392
|
+
|| "";
|
|
393
|
+
} catch {
|
|
394
|
+
sourcePath = "";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!sourcePath) {
|
|
398
|
+
throw new Error("@ffmpeg-installer/ffmpeg konnte nicht geladen werden");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await fs.promises.copyFile(sourcePath, MANAGED_FFMPEG_PATH);
|
|
402
|
+
await fs.promises.chmod(MANAGED_FFMPEG_PATH, 0o755);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function ensureManagedWhisperCli() {
|
|
406
|
+
if (await hasExecutable(MANAGED_WHISPER_PATH)) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!(await commandExists("python3"))) {
|
|
411
|
+
throw new Error("python3 fehlt, Whisper-CLI konnte nicht automatisch installiert werden");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await fs.promises.mkdir(path.dirname(WHISPER_VENV_DIR), { recursive: true });
|
|
415
|
+
|
|
416
|
+
const venvPython = path.join(WHISPER_VENV_DIR, "bin", "python");
|
|
417
|
+
if (!(await hasExecutable(venvPython))) {
|
|
418
|
+
await runCommand("python3", ["-m", "venv", WHISPER_VENV_DIR]);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await runCommand(venvPython, ["-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"]);
|
|
422
|
+
await runCommand(venvPython, ["-m", "pip", "install", "--upgrade", "openai-whisper"]);
|
|
423
|
+
|
|
424
|
+
if (!(await hasExecutable(MANAGED_WHISPER_PATH))) {
|
|
425
|
+
throw new Error("Whisper wurde installiert, aber das CLI-Binary fehlt");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function commandExists(name) {
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
const child = spawn("sh", ["-c", `command -v ${name} >/dev/null 2>&1`], {
|
|
432
|
+
stdio: "ignore",
|
|
433
|
+
shell: false,
|
|
434
|
+
});
|
|
435
|
+
child.on("error", () => resolve(false));
|
|
436
|
+
child.on("close", (code) => resolve(code === 0));
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function runCommand(program, commandArgs) {
|
|
441
|
+
return new Promise((resolve, reject) => {
|
|
442
|
+
const child = spawn(program, commandArgs, {
|
|
443
|
+
shell: false,
|
|
444
|
+
stdio: QUIET ? "ignore" : "pipe",
|
|
445
|
+
env: withManagedBinPath(process.env),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
let stderr = "";
|
|
449
|
+
if (child.stderr) {
|
|
450
|
+
child.stderr.on("data", (chunk) => {
|
|
451
|
+
stderr += String(chunk);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
child.on("error", (error) => reject(error));
|
|
456
|
+
child.on("close", (code) => {
|
|
457
|
+
if (code === 0) {
|
|
458
|
+
resolve();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
reject(new Error(stderr.trim() || `${program} failed with code ${code}`));
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function withManagedBinPath(baseEnv) {
|
|
467
|
+
const env = { ...baseEnv };
|
|
468
|
+
const currentPath = String(env.PATH || "");
|
|
469
|
+
const parts = currentPath.split(":").filter(Boolean);
|
|
470
|
+
if (!parts.includes(APPIMAGE_DIR)) {
|
|
471
|
+
parts.unshift(APPIMAGE_DIR);
|
|
472
|
+
}
|
|
473
|
+
env.PATH = parts.join(":");
|
|
474
|
+
return env;
|
|
475
|
+
}
|
|
476
|
+
|
|
346
477
|
function printHelp() {
|
|
347
478
|
console.log("twokey <command/options>");
|
|
348
479
|
console.log("");
|
|
@@ -352,6 +483,8 @@ function printHelp() {
|
|
|
352
483
|
console.log(" --cli Start interactive terminal mode");
|
|
353
484
|
console.log(" --once <prompt> Send one prompt to Ollama and print response");
|
|
354
485
|
console.log(" --desktop Start native desktop app in background");
|
|
486
|
+
console.log(" --prepare-runtime Install/check local runtime dependencies");
|
|
487
|
+
console.log(" --prepare-runtime-only Only install/check dependencies and exit");
|
|
355
488
|
console.log("");
|
|
356
489
|
console.log("Without options, twokey starts the native desktop app in background.");
|
|
357
490
|
console.log("If no desktop binary is installed, twokey tries to download an AppImage from latest GitHub release.");
|
|
@@ -369,6 +502,7 @@ async function ensureUserService(command) {
|
|
|
369
502
|
"After=graphical-session.target",
|
|
370
503
|
"",
|
|
371
504
|
"[Service]",
|
|
505
|
+
`Environment=PATH=${path.join(os.homedir(), ".local", "share", "twokey", "bin")}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
372
506
|
`ExecStart=/bin/sh -lc ${shellEscape(command)}`,
|
|
373
507
|
"Restart=on-failure",
|
|
374
508
|
"RestartSec=3",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twokey",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Linux-first desktop AI assistant built with Tauri, React, and TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"tauri:build": "GTK_MODULES='' LIBGL_ALWAYS_SOFTWARE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 tauri build"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
51
52
|
"@tauri-apps/api": "^2.5.0",
|
|
52
53
|
"lucide-react": "^0.468.0",
|
|
53
54
|
"react": "^18.3.1",
|