twokey 1.0.0 → 1.0.3

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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/bin/twokey.js +273 -0
  3. package/package.json +5 -1
package/README.md CHANGED
@@ -22,6 +22,24 @@ No TTS or text injection is implemented yet.
22
22
  npm install twokey
23
23
  ```
24
24
 
25
+ Start the tool directly:
26
+
27
+ ```bash
28
+ twokey
29
+ ```
30
+
31
+ Default behavior: start the native desktop app in background.
32
+ If no desktop binary is installed yet, `twokey` attempts to download an AppImage from the latest GitHub release into `~/.local/share/twokey/bin/` and starts it.
33
+
34
+ Useful CLI options:
35
+
36
+ ```bash
37
+ twokey --help
38
+ twokey --cli
39
+ twokey --once "Erklaere kurz den Unterschied zwischen X11 und Wayland"
40
+ twokey --desktop
41
+ ```
42
+
25
43
  ## Minimal Usage
26
44
 
27
45
  ```ts
package/bin/twokey.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import readline from "node:readline";
8
+
9
+ const VERSION = "1.0.3";
10
+ const DEFAULT_MODEL = process.env.TWOKEY_OLLAMA_MODEL || "qwen2.5:3b";
11
+ const DEFAULT_OLLAMA_URL = process.env.TWOKEY_OLLAMA_URL || "http://127.0.0.1:11434";
12
+ const LATEST_RELEASE_API = "https://api.github.com/repos/meinzeug/twokey/releases/latest";
13
+ const APPIMAGE_DIR = path.join(os.homedir(), ".local", "share", "twokey", "bin");
14
+ const APPIMAGE_PATH = path.join(APPIMAGE_DIR, "twokey-ai.AppImage");
15
+
16
+ const args = process.argv.slice(2);
17
+
18
+ if (args.includes("--help") || args.includes("-h")) {
19
+ printHelp();
20
+ process.exit(0);
21
+ }
22
+
23
+ if (args.includes("--version") || args.includes("-v")) {
24
+ console.log(VERSION);
25
+ process.exit(0);
26
+ }
27
+
28
+ if (args.includes("--desktop")) {
29
+ launchDesktopApp().then((started) => {
30
+ if (started) {
31
+ console.log("TwoKey desktop app started in background.");
32
+ process.exit(0);
33
+ }
34
+ console.error("No native desktop binary found in PATH.");
35
+ console.error("Install the .deb/.AppImage release and ensure 'twokey-ai' is available in PATH.");
36
+ process.exit(1);
37
+ });
38
+ }
39
+
40
+ const onceIndex = args.findIndex((value) => value === "--once");
41
+ if (onceIndex >= 0) {
42
+ const prompt = args.slice(onceIndex + 1).join(" ").trim();
43
+ if (!prompt) {
44
+ console.error("Missing prompt after --once");
45
+ process.exit(1);
46
+ }
47
+
48
+ runSinglePrompt(prompt).catch((error) => {
49
+ console.error(error.message || String(error));
50
+ process.exit(1);
51
+ });
52
+ } else if (args.includes("--cli")) {
53
+ startRepl().catch((error) => {
54
+ console.error(error.message || String(error));
55
+ process.exit(1);
56
+ });
57
+ } else {
58
+ launchDesktopApp().then((started) => {
59
+ if (started) {
60
+ console.log("TwoKey desktop app started in background.");
61
+ process.exit(0);
62
+ }
63
+
64
+ console.error("Could not start desktop app.");
65
+ console.error("Tried system binaries and auto-download from GitHub Releases.");
66
+ console.error("Use 'twokey --cli' to run terminal mode.");
67
+ process.exit(1);
68
+ });
69
+ }
70
+
71
+ async function runSinglePrompt(prompt) {
72
+ const answer = await askOllama(prompt);
73
+ console.log(answer);
74
+ }
75
+
76
+ async function startRepl() {
77
+ console.log("TwoKey CLI");
78
+ console.log("Type your prompt. Commands: /help, /exit");
79
+ console.log(`Ollama endpoint: ${DEFAULT_OLLAMA_URL}`);
80
+ console.log(`Model: ${DEFAULT_MODEL}`);
81
+
82
+ const rl = readline.createInterface({
83
+ input: process.stdin,
84
+ output: process.stdout,
85
+ prompt: "twokey> ",
86
+ });
87
+
88
+ rl.prompt();
89
+
90
+ rl.on("line", async (line) => {
91
+ const input = line.trim();
92
+ if (!input) {
93
+ rl.prompt();
94
+ return;
95
+ }
96
+
97
+ if (input === "/exit" || input === "/quit") {
98
+ rl.close();
99
+ return;
100
+ }
101
+
102
+ if (input === "/help") {
103
+ printHelp();
104
+ rl.prompt();
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const answer = await askOllama(input);
110
+ console.log(`\n${answer}\n`);
111
+ } catch (error) {
112
+ console.error(error.message || String(error));
113
+ }
114
+
115
+ rl.prompt();
116
+ });
117
+
118
+ rl.on("close", () => {
119
+ console.log("bye");
120
+ process.exit(0);
121
+ });
122
+ }
123
+
124
+ async function askOllama(prompt) {
125
+ const response = await fetch(`${DEFAULT_OLLAMA_URL.replace(/\/$/, "")}/api/chat`, {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ },
130
+ body: JSON.stringify({
131
+ model: DEFAULT_MODEL,
132
+ stream: false,
133
+ messages: [
134
+ {
135
+ role: "system",
136
+ content: "You are TwoKey, a concise Linux assistant.",
137
+ },
138
+ {
139
+ role: "user",
140
+ content: prompt,
141
+ },
142
+ ],
143
+ options: {
144
+ temperature: 0.3,
145
+ num_predict: 384,
146
+ },
147
+ }),
148
+ });
149
+
150
+ if (!response.ok) {
151
+ throw new Error(`Ollama request failed with HTTP ${response.status}`);
152
+ }
153
+
154
+ const payload = await response.json();
155
+ const content = payload?.message?.content?.trim();
156
+ if (!content) {
157
+ throw new Error("Ollama returned an empty response");
158
+ }
159
+
160
+ return content;
161
+ }
162
+
163
+ async function launchDesktopApp() {
164
+ const candidates = [];
165
+ if (process.env.TWOKEY_DESKTOP_CMD) {
166
+ candidates.push(process.env.TWOKEY_DESKTOP_CMD);
167
+ }
168
+ candidates.push("twokey-ai", "twokey-desktop", APPIMAGE_PATH);
169
+
170
+ for (const command of candidates) {
171
+ const started = await spawnDetached(command);
172
+ if (started) {
173
+ return true;
174
+ }
175
+ }
176
+
177
+ try {
178
+ const downloaded = await ensureLocalAppImage();
179
+ if (downloaded) {
180
+ return spawnDetached(APPIMAGE_PATH);
181
+ }
182
+ } catch {
183
+ return false;
184
+ }
185
+
186
+ return false;
187
+ }
188
+
189
+ async function ensureLocalAppImage() {
190
+ try {
191
+ await fs.promises.access(APPIMAGE_PATH, fs.constants.X_OK);
192
+ return true;
193
+ } catch {
194
+ // Not installed yet.
195
+ }
196
+
197
+ await fs.promises.mkdir(APPIMAGE_DIR, { recursive: true });
198
+ const assetUrl = await resolveLatestAppImageUrl();
199
+ if (!assetUrl) {
200
+ return false;
201
+ }
202
+
203
+ const response = await fetch(assetUrl, {
204
+ headers: {
205
+ "User-Agent": "twokey-cli",
206
+ Accept: "application/octet-stream",
207
+ },
208
+ });
209
+
210
+ if (!response.ok) {
211
+ return false;
212
+ }
213
+
214
+ const data = Buffer.from(await response.arrayBuffer());
215
+ await fs.promises.writeFile(APPIMAGE_PATH, data, { mode: 0o755 });
216
+
217
+ await fs.promises.chmod(APPIMAGE_PATH, 0o755);
218
+ return true;
219
+ }
220
+
221
+ async function resolveLatestAppImageUrl() {
222
+ const response = await fetch(LATEST_RELEASE_API, {
223
+ headers: {
224
+ "User-Agent": "twokey-cli",
225
+ Accept: "application/vnd.github+json",
226
+ },
227
+ });
228
+
229
+ if (!response.ok) {
230
+ return null;
231
+ }
232
+
233
+ const payload = await response.json();
234
+ const assets = Array.isArray(payload.assets) ? payload.assets : [];
235
+ const appImage = assets.find(
236
+ (asset) => typeof asset?.name === "string" && asset.name.endsWith(".AppImage") && asset.name.includes("amd64"),
237
+ ) || assets.find((asset) => typeof asset?.name === "string" && asset.name.endsWith(".AppImage"));
238
+
239
+ return appImage?.browser_download_url ?? null;
240
+ }
241
+
242
+ function spawnDetached(command) {
243
+ return new Promise((resolve) => {
244
+ const child = spawn(command, [], {
245
+ detached: true,
246
+ stdio: "ignore",
247
+ shell: false,
248
+ });
249
+
250
+ child.once("spawn", () => {
251
+ child.unref();
252
+ resolve(true);
253
+ });
254
+
255
+ child.once("error", () => {
256
+ resolve(false);
257
+ });
258
+ });
259
+ }
260
+
261
+ function printHelp() {
262
+ console.log("twokey <command/options>");
263
+ console.log("");
264
+ console.log("Options:");
265
+ console.log(" --help, -h Show help");
266
+ console.log(" --version, -v Show version");
267
+ console.log(" --cli Start interactive terminal mode");
268
+ console.log(" --once <prompt> Send one prompt to Ollama and print response");
269
+ console.log(" --desktop Start native desktop app in background");
270
+ console.log("");
271
+ console.log("Without options, twokey starts the native desktop app in background.");
272
+ console.log("If no desktop binary is installed, twokey tries to download an AppImage from latest GitHub release.");
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twokey",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Linux-first desktop AI assistant built with Tauri, React, and TypeScript.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,6 +15,9 @@
15
15
  "main": "./lib/index.js",
16
16
  "module": "./lib/index.js",
17
17
  "types": "./lib/index.d.ts",
18
+ "bin": {
19
+ "twokey": "./bin/twokey.js"
20
+ },
18
21
  "exports": {
19
22
  ".": {
20
23
  "types": "./lib/index.d.ts",
@@ -23,6 +26,7 @@
23
26
  }
24
27
  },
25
28
  "files": [
29
+ "bin",
26
30
  "lib",
27
31
  "README.md",
28
32
  "LICENSE"