twokey 1.0.11 → 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.
@@ -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.11";
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 (args.includes("--desktop")) {
31
- launchDesktopApp().then(async (startedCommand) => {
32
- if (startedCommand) {
33
- if (args.includes("--enable-autostart")) {
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.log("TwoKey desktop app started in background.");
40
+ console.warn(`Runtime setup skipped: ${error.message || String(error)}`);
45
41
  }
46
42
  process.exit(0);
47
- }
48
- if (!QUIET) {
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.11",
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",