@sarkar-ai/deskmate 0.2.0 → 0.3.0

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/.env.example CHANGED
@@ -4,24 +4,22 @@
4
4
  # Run `deskmate init` for interactive setup, or copy this file to .env and edit.
5
5
  # Alternative: ./install.sh
6
6
 
7
- # Telegram Bot Token (get from @BotFather)
8
- # https://t.me/BotFather → /newbot → copy token
9
- TELEGRAM_BOT_TOKEN=your_bot_token_here
10
-
11
- # Your Telegram User ID (get from @userinfobot)
12
- # https://t.me/userinfobot → send any message → copy Id
13
- # Only this user can interact with the bot
14
- ALLOWED_USER_ID=your_user_id_here
15
-
16
- # Multi-client allowed users (for gateway mode)
7
+ # Allowed users (required)
17
8
  # Format: clientType:userId, comma-separated
18
9
  # Example: telegram:123456,discord:987654321,slack:U12345
19
10
  ALLOWED_USERS=telegram:your_user_id_here
20
11
 
12
+ # Telegram Bot Token (get from @BotFather)
13
+ # https://t.me/BotFather -> /newbot -> copy token
14
+ TELEGRAM_BOT_TOKEN=your_bot_token_here
15
+
21
16
  # Anthropic API Key
22
17
  # https://console.anthropic.com/
23
18
  ANTHROPIC_API_KEY=your_anthropic_key_here
24
19
 
20
+ # Legacy single-user Telegram ID (still supported, converted to telegram:<id> internally)
21
+ # ALLOWED_USER_ID=your_user_id_here
22
+
25
23
  # ===========================================
26
24
  # Optional Configuration
27
25
  # ===========================================
package/README.md CHANGED
@@ -8,7 +8,7 @@ Control your Local Machine from anywhere using natural language.
8
8
  <a href="#requirements"><img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=for-the-badge" alt="Node"></a>
9
9
  </p>
10
10
 
11
- Deskmate is a personal AI assistant that runs on your personal machine and talks to you on the channels you already use. Send a Telegram message from your phone, and it executes on your machine. Powered by the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code/agent-sdk) with full local tool access — no sandboxed command set, no artificial limits.
11
+ Deskmate is a local execution agent that lets you control your personal machine using natural language and talks to you on the channels you already use. Deskmate focuses on execution, not autonomy or orchestration. Send a Telegram message from your phone, and it executes on your machine. Powered by the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code/agent-sdk) with full local tool access — no sandboxed command set, no artificial limits.
12
12
 
13
13
  A passion project developed, born from a simple goal: staying in creative and developer flow even when I'm not sitting at my desk. Inspired by [gen-shell](https://github.com/sarkarsaurabh27/gen-shell).
14
14
 
@@ -18,6 +18,10 @@ A passion project developed, born from a simple goal: staying in creative and de
18
18
 
19
19
  ## Demo
20
20
 
21
+ <p align="center">
22
+ <img src="assets/deskmate-screenshot.jpeg" alt="Deskmate Screenshot" width="500">
23
+ </p>
24
+
21
25
  | Telegram Conversation | Installation |
22
26
  |:---:|:---:|
23
27
  | ![Telegram Demo](assets/deskmate-tg.gif) | ![Installation Demo](assets/deskmate-install.gif) |
@@ -47,6 +51,17 @@ Telegram / Discord* / Slack* / ...
47
51
 
48
52
  The Gateway is the control plane. Each messaging platform is a thin I/O adapter. The agent has unrestricted access to your machine (approve-by-default), with optional approval gating for protected folders.
49
53
 
54
+ ## Responsibility Boundary
55
+
56
+ Deskmate’s responsibility is **execution**.
57
+
58
+ - It turns intent into concrete system actions
59
+ - It does not coordinate other agents
60
+ - It does not monitor agent health or resource usage
61
+
62
+ If you want visibility into what agents are doing on your machine,
63
+ see **Riva**, the local observability layer.
64
+
50
65
  ## Highlights
51
66
 
52
67
  - **Full local access** — the agent can run any command, read/write any file, take screenshots. No artificial 6-tool sandbox.
@@ -87,25 +102,34 @@ The installer guides you through these (macOS only). You can also configure them
87
102
 
88
103
  ## Quick Start
89
104
 
90
- ### Install from npm (recommended for users)
105
+ ### Option A: Install from npm (recommended)
91
106
 
92
107
  ```bash
93
- npm install -g deskmate
108
+ npm install -g @sarkar-ai/deskmate
94
109
  deskmate init
95
110
  ```
96
111
 
97
112
  The wizard walks you through everything: API keys, Telegram credentials,
98
- platform permissions, and background service setup.
113
+ platform permissions, and background service setup. Config is stored in
114
+ `~/.config/deskmate/.env`.
99
115
 
100
- ### Install from source (for contributors)
116
+ After setup, run manually with `deskmate` or let the background service handle it.
117
+
118
+ ### Option B: Install from source (for contributors)
101
119
 
102
120
  ```bash
103
121
  git clone https://github.com/sarkar-ai-taken/deskmate.git
104
122
  cd deskmate
105
123
  npm install --legacy-peer-deps
106
- cp .env.example .env # edit with your credentials
107
124
  npm run build
108
- ./install.sh # or: npx deskmate init
125
+ ./install.sh # interactive: configures .env, service, permissions
126
+ ```
127
+
128
+ Or use the TypeScript wizard instead of the shell installer:
129
+
130
+ ```bash
131
+ cp .env.example .env # edit with your credentials
132
+ npx deskmate init # or: npm link && deskmate init
109
133
  ```
110
134
 
111
135
  To reconfigure later: `deskmate init`
@@ -114,30 +138,26 @@ To reconfigure later: `deskmate init`
114
138
 
115
139
  | Mode | Command | Description |
116
140
  |------|---------|-------------|
117
- | Telegram | `deskmate telegram` | Standalone Telegram bot (legacy) |
118
- | Gateway | `deskmate gateway` | Multi-client gateway (recommended for new setups) |
141
+ | Gateway | `deskmate` | Multi-client gateway (default) |
119
142
  | MCP | `deskmate mcp` | MCP server for Claude Desktop |
120
- | Both | `deskmate both` | Telegram + MCP simultaneously |
143
+ | Both | `deskmate both` | Gateway + MCP simultaneously |
144
+
145
+ > **Note:** `deskmate telegram` still works but is a deprecated alias that starts the gateway.
121
146
 
122
147
  ## Gateway Mode
123
148
 
124
- The gateway is the recommended way to run Deskmate. It separates platform I/O from agent logic, so adding a new messaging client doesn't require touching auth, sessions, or the agent layer.
149
+ The gateway is the default way to run Deskmate. It separates platform I/O from agent logic, so adding a new messaging client doesn't require touching auth, sessions, or the agent layer.
125
150
 
126
151
  ```bash
127
152
  # Configure multi-client auth
128
153
  ALLOWED_USERS=telegram:123456,discord:987654321
129
154
 
130
155
  # Start
131
- deskmate gateway
156
+ deskmate
132
157
  ```
133
158
 
134
159
  The gateway auto-registers clients based on available env vars. If `TELEGRAM_BOT_TOKEN` is set, Telegram is active. Future clients (Discord, Slack) follow the same pattern.
135
160
 
136
- ### Gateway vs Telegram mode
137
-
138
- - **`deskmate telegram`** — original standalone bot. Simple, self-contained, no gateway overhead. Good for single-user Telegram-only setups.
139
- - **`deskmate gateway`** — centralized architecture. Auth, sessions, and agent orchestration are shared. Required for multi-client setups and recommended for new installations.
140
-
141
161
  ## Bot Commands
142
162
 
143
163
  | Command | Description |
@@ -187,32 +207,77 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
187
207
 
188
208
  Restart Claude Desktop. You can now ask Claude to interact with your local machine.
189
209
 
190
- ### Combined Mode (MCP + Telegram)
210
+ ### Combined Mode (Gateway + MCP)
211
+
212
+ Run both with `deskmate both`. MCP handles Claude Desktop requests; the gateway handles Telegram (and future clients), sending approval notifications to your phone so you can approve sensitive operations from anywhere.
191
213
 
192
- Run both with `deskmate both`. MCP handles Claude Desktop requests; Telegram sends approval notifications to your phone so you can approve sensitive operations from anywhere.
214
+ ### Observability
215
+
216
+ Deskmate focuses on executing actions safely.
217
+
218
+ For monitoring agent behavior, resource usage, and failures across
219
+ multiple local agents, see **Riva** (local-first agent observability).
193
220
 
194
221
  ## Security
195
222
 
196
223
  > **Important**: The agent can execute arbitrary commands on your machine. This is by design — the strategy is approve-by-default for read-only operations, with approval gating for protected folders and write operations.
197
224
 
198
- **Built-in protections:**
225
+ ### Built-in protections
199
226
 
200
227
  | Layer | What it does |
201
228
  |-------|-------------|
202
- | **User authentication** | Only allowlisted user IDs can interact (per-client) |
203
- | **Folder protection** | Desktop, Documents, Downloads, etc. require explicit approval |
204
- | **No sudo by default** | The agent won't use sudo unless you explicitly ask |
205
- | **No open ports** | The bot polls Telegram's servers, doesn't expose any ports |
206
- | **Structured logging** | All actions are logged with timestamps for audit |
207
- | **Session isolation** | Gateway sessions are keyed by `clientType:channelId` |
208
-
209
- **Recommendations:**
229
+ | **User authentication** | Allowlist-based access control via `SecurityManager`. Only users in `ALLOWED_USERS` can interact. Supports per-client auth (`telegram:123`, `discord:456`) and wildcards (`*:*`). |
230
+ | **Action approval** | `ApprovalManager` gates sensitive operations. Write commands, file writes, and folder access require explicit human approval with configurable timeouts (default 5 min). |
231
+ | **Protected folders** | OS-aware folder protection. Desktop, Documents, Downloads, Pictures, Movies/Videos, Music, and iCloud (macOS) require approval. Session-based caching avoids repeated prompts. |
232
+ | **Safe command auto-approval** | Read-only commands (`ls`, `cat`, `git status`, `docker ps`, `node -v`, etc.) auto-approve. Full list in `src/core/approval.ts`. |
233
+ | **Command execution limits** | 2-minute timeout and 10 MB output buffer per command. Prevents runaway processes and memory exhaustion. |
234
+ | **Session isolation** | Sessions keyed by `clientType:channelId`. 30-minute idle timeout with automatic pruning. Optional disk persistence survives restarts. |
235
+ | **Input validation** | MCP tools use Zod schema validation. Telegram callbacks validated via regex patterns. |
236
+ | **No open ports** | The bot polls Telegram's servers — no inbound ports exposed. |
237
+ | **No sudo by default** | The agent won't use sudo unless you explicitly ask. |
238
+ | **Structured logging** | All actions logged with timestamps, context hierarchy, and configurable log levels for audit trails. |
239
+ | **Stale message protection** | Telegram client drops pending updates on startup (`drop_pending_updates: true`), preventing replay of messages received while offline. |
240
+
241
+ ### Approval workflow
242
+
243
+ 1. User sends a message that triggers a sensitive operation (e.g., writing to `~/Documents`)
244
+ 2. `ApprovalManager` checks if the action matches a safe auto-approve pattern
245
+ 3. If not safe, a pending approval is created with a timeout countdown
246
+ 4. Approval request is broadcast to all clients with recent activity (last 30 min)
247
+ 5. User taps Approve/Reject via inline buttons (Telegram) or equivalent
248
+ 6. Action executes on approval, or is cancelled on rejection/timeout
249
+
250
+ Set `REQUIRE_APPROVAL_FOR_ALL=true` to gate every operation, including reads.
251
+
252
+ ### Recommendations
253
+
210
254
  - Set `WORKING_DIR` to limit default command scope
211
- - Use `ALLOWED_USERS` (gateway mode) for multi-client allowlisting
255
+ - Use `ALLOWED_USERS` for multi-client allowlisting
256
+ - Use `ALLOWED_FOLDERS` to pre-approve specific directories
212
257
  - Review logs regularly (`logs/stdout.log`)
213
258
  - Keep `.env` secure and never commit it
214
259
  - Use `REQUIRE_APPROVAL_FOR_ALL=true` if you want to approve every operation
215
260
 
261
+ ### Execution Philosophy
262
+
263
+ Deskmate follows an **approve-by-default, visible-by-design** model.
264
+
265
+ - Read-only operations are auto-approved
266
+ - Sensitive operations require explicit confirmation
267
+ - All actions are logged locally
268
+
269
+ The goal is speed without hidden behavior.
270
+
271
+ ## Non-goals
272
+
273
+ Deskmate is intentionally not:
274
+ - A multi-agent orchestration framework
275
+ - A cloud-hosted control plane
276
+ - A long-running autonomous system
277
+ - A monitoring or observability tool
278
+
279
+ These constraints are deliberate.
280
+
216
281
  ## Architecture
217
282
 
218
283
  ```
@@ -233,8 +298,6 @@ src/
233
298
  │ └── session.ts # Session manager (composite keys, idle pruning)
234
299
  ├── clients/
235
300
  │ └── telegram.ts # Telegram adapter (grammY)
236
- ├── telegram/
237
- │ └── bot.ts # Legacy standalone Telegram bot
238
301
  └── mcp/
239
302
  └── server.ts # MCP server
240
303
  ```
@@ -312,7 +375,7 @@ systemctl --user status deskmate.service
312
375
 
313
376
  **Bot not responding?**
314
377
  1. Check logs: `tail -f logs/stderr.log`
315
- 2. Verify your `ALLOWED_USER_ID` matches your Telegram ID
378
+ 2. Verify your `ALLOWED_USERS` includes your Telegram ID (e.g. `telegram:123456`)
316
379
  3. Ensure Claude Code CLI is installed: `which claude`
317
380
 
318
381
  **Commands timing out?**
package/dist/cli/init.js CHANGED
@@ -2,10 +2,16 @@
2
2
  /**
3
3
  * Interactive Setup Wizard
4
4
  *
5
- * Self-contained setup: writes .env, optionally installs a background service
6
- * (launchd on macOS, systemd on Linux), and guides through macOS permissions.
5
+ * Supports two install paths:
6
+ * 1. npm global: `npm install -g @sarkar-ai/deskmate && deskmate init`
7
+ * — config stored in ~/.config/deskmate/.env
8
+ * — service uses the global `deskmate` binary
7
9
  *
8
- * For an alternative shell-based installer, see ./install.sh.
10
+ * 2. Source clone: `git clone ... && cd deskmate && deskmate init` (or ./install.sh)
11
+ * — config stored in project root .env
12
+ * — service uses `node <projectDir>/dist/index.js`
13
+ *
14
+ * For an alternative shell-based installer (source path only), see ./install.sh.
9
15
  */
10
16
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
17
  if (k2 === undefined) k2 = k;
@@ -89,6 +95,67 @@ function detectPlatform() {
89
95
  return "unsupported";
90
96
  }
91
97
  }
98
+ function resolveInstallPaths() {
99
+ // dist/cli/init.js -> go up two levels to get package root
100
+ const packageDir = path.resolve(__dirname, "..", "..");
101
+ const isGlobal = __dirname.includes("node_modules");
102
+ if (isGlobal) {
103
+ const configDir = path.join(os.homedir(), ".config", "deskmate");
104
+ // Use the global deskmate binary (which is in PATH)
105
+ const deskmateCmd = process.argv[1] || "deskmate";
106
+ // Resolve to an absolute path so the service always finds it
107
+ let deskmateBin;
108
+ try {
109
+ deskmateBin = (0, child_process_1.execSync)("which deskmate", { encoding: "utf-8" }).trim();
110
+ }
111
+ catch {
112
+ deskmateBin = deskmateCmd;
113
+ }
114
+ return {
115
+ isGlobalInstall: true,
116
+ packageDir,
117
+ configDir,
118
+ execStart: (runMode) => `${deskmateBin} ${runMode}`,
119
+ };
120
+ }
121
+ // Source install — configDir is the project root
122
+ return {
123
+ isGlobalInstall: false,
124
+ packageDir,
125
+ configDir: packageDir,
126
+ execStart: (runMode) => `${process.execPath} ${packageDir}/dist/index.js ${runMode}`,
127
+ };
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // .env reader
131
+ // ---------------------------------------------------------------------------
132
+ async function loadExistingEnv(envPath) {
133
+ const existing = {};
134
+ try {
135
+ const content = await fs.readFile(envPath, "utf-8");
136
+ for (const line of content.split("\n")) {
137
+ const trimmed = line.trim();
138
+ if (!trimmed || trimmed.startsWith("#"))
139
+ continue;
140
+ const eqIdx = trimmed.indexOf("=");
141
+ if (eqIdx > 0) {
142
+ const key = trimmed.slice(0, eqIdx).trim();
143
+ const value = trimmed.slice(eqIdx + 1).trim();
144
+ if (value)
145
+ existing[key] = value;
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ // file doesn't exist — fine
151
+ }
152
+ return existing;
153
+ }
154
+ function mask(value) {
155
+ if (value.length <= 8)
156
+ return "****";
157
+ return value.slice(0, 4) + "..." + value.slice(-4);
158
+ }
92
159
  // ---------------------------------------------------------------------------
93
160
  // Service installation helpers
94
161
  // ---------------------------------------------------------------------------
@@ -102,7 +169,10 @@ function systemdDir() {
102
169
  function systemdPath() {
103
170
  return path.join(systemdDir(), "deskmate.service");
104
171
  }
105
- function buildPlist(nodePath, projectDir, logsDir, runMode) {
172
+ function buildPlist(execStart, workingDir, logsDir) {
173
+ // Split the execStart into program + arguments for ProgramArguments array
174
+ const parts = execStart.split(" ");
175
+ const argsXml = parts.map((p) => ` <string>${p}</string>`).join("\n");
106
176
  return `<?xml version="1.0" encoding="UTF-8"?>
107
177
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
108
178
  <plist version="1.0">
@@ -112,13 +182,11 @@ function buildPlist(nodePath, projectDir, logsDir, runMode) {
112
182
 
113
183
  <key>ProgramArguments</key>
114
184
  <array>
115
- <string>${nodePath}</string>
116
- <string>${projectDir}/dist/index.js</string>
117
- <string>${runMode}</string>
185
+ ${argsXml}
118
186
  </array>
119
187
 
120
188
  <key>WorkingDirectory</key>
121
- <string>${projectDir}</string>
189
+ <string>${workingDir}</string>
122
190
 
123
191
  <key>EnvironmentVariables</key>
124
192
  <dict>
@@ -140,7 +208,7 @@ function buildPlist(nodePath, projectDir, logsDir, runMode) {
140
208
  </dict>
141
209
  </plist>`;
142
210
  }
143
- function buildSystemdUnit(nodePath, projectDir, logsDir, runMode) {
211
+ function buildSystemdUnit(execStart, workingDir, logsDir) {
144
212
  return `[Unit]
145
213
  Description=Deskmate - Local Machine Assistant
146
214
  After=network-online.target
@@ -148,8 +216,8 @@ Wants=network-online.target
148
216
 
149
217
  [Service]
150
218
  Type=simple
151
- ExecStart=${nodePath} ${projectDir}/dist/index.js ${runMode}
152
- WorkingDirectory=${projectDir}
219
+ ExecStart=${execStart}
220
+ WorkingDirectory=${workingDir}
153
221
  Environment=PATH=/usr/local/bin:/usr/bin:/bin:${os.homedir()}/.local/bin
154
222
  Restart=always
155
223
  RestartSec=5
@@ -162,8 +230,7 @@ StandardError=append:${logsDir}/stderr.log
162
230
  [Install]
163
231
  WantedBy=default.target`;
164
232
  }
165
- async function installMacosService(projectDir, logsDir, runMode) {
166
- const nodePath = process.execPath;
233
+ async function installMacosService(execStart, workingDir, logsDir) {
167
234
  const dest = plistPath();
168
235
  // Unload existing
169
236
  try {
@@ -177,13 +244,12 @@ async function installMacosService(projectDir, logsDir, runMode) {
177
244
  }
178
245
  await fs.mkdir(path.dirname(dest), { recursive: true });
179
246
  await fs.mkdir(logsDir, { recursive: true });
180
- await fs.writeFile(dest, buildPlist(nodePath, projectDir, logsDir, runMode), "utf-8");
247
+ await fs.writeFile(dest, buildPlist(execStart, workingDir, logsDir), "utf-8");
181
248
  (0, child_process_1.execSync)(`launchctl load "${dest}"`);
182
249
  console.log("\n Service installed and started via launchd.");
183
250
  console.log(` Plist: ${dest}`);
184
251
  }
185
- async function installLinuxService(projectDir, logsDir, runMode) {
186
- const nodePath = process.execPath;
252
+ async function installLinuxService(execStart, workingDir, logsDir) {
187
253
  const dest = systemdPath();
188
254
  // Stop existing
189
255
  try {
@@ -194,7 +260,7 @@ async function installLinuxService(projectDir, logsDir, runMode) {
194
260
  }
195
261
  await fs.mkdir(systemdDir(), { recursive: true });
196
262
  await fs.mkdir(logsDir, { recursive: true });
197
- await fs.writeFile(dest, buildSystemdUnit(nodePath, projectDir, logsDir, runMode), "utf-8");
263
+ await fs.writeFile(dest, buildSystemdUnit(execStart, workingDir, logsDir), "utf-8");
198
264
  (0, child_process_1.execSync)("systemctl --user daemon-reload");
199
265
  (0, child_process_1.execSync)("systemctl --user enable deskmate.service");
200
266
  (0, child_process_1.execSync)("systemctl --user start deskmate.service");
@@ -286,33 +352,89 @@ async function runInitWizard() {
286
352
  console.log("Install it from: https://docs.anthropic.com/en/docs/claude-code");
287
353
  console.log("");
288
354
  }
355
+ // Detect install type and resolve paths
356
+ const paths = resolveInstallPaths();
357
+ if (paths.isGlobalInstall) {
358
+ console.log(`Install type: npm global`);
359
+ console.log(`Config directory: ${paths.configDir}\n`);
360
+ }
361
+ else {
362
+ console.log(`Install type: source`);
363
+ console.log(`Project directory: ${paths.packageDir}\n`);
364
+ }
365
+ // Ensure config directory exists
366
+ await fs.mkdir(paths.configDir, { recursive: true });
289
367
  // ----- Step 1: .env wizard -----
368
+ const envPath = path.join(paths.configDir, ".env");
369
+ const existing = await loadExistingEnv(envPath);
370
+ const hasExisting = Object.keys(existing).length > 0;
371
+ if (hasExisting) {
372
+ console.log("Found existing .env — values shown below. Press Enter to keep current value.\n");
373
+ }
290
374
  const env = {};
291
- env.AGENT_PROVIDER = "claude-code";
292
- const apiKey = await ask(rl, "Anthropic API Key (for Claude Code): ");
293
- if (apiKey)
294
- env.ANTHROPIC_API_KEY = apiKey;
375
+ env.AGENT_PROVIDER = existing.AGENT_PROVIDER || "claude-code";
376
+ // Anthropic API Key
377
+ if (existing.ANTHROPIC_API_KEY) {
378
+ const apiKey = await ask(rl, `Anthropic API Key [${mask(existing.ANTHROPIC_API_KEY)}]: `);
379
+ env.ANTHROPIC_API_KEY = apiKey || existing.ANTHROPIC_API_KEY;
380
+ }
381
+ else {
382
+ const apiKey = await ask(rl, "Anthropic API Key (for Claude Code): ");
383
+ if (apiKey)
384
+ env.ANTHROPIC_API_KEY = apiKey;
385
+ }
386
+ // Telegram
295
387
  console.log("\n--- Telegram Configuration ---\n");
296
- console.log(" Bot token → message @BotFather on Telegram, send /newbot");
297
- console.log(" User ID → message @userinfobot on Telegram, copy the number");
298
- console.log("");
299
- const token = await ask(rl, "Telegram Bot Token (from @BotFather): ");
300
- if (token)
301
- env.TELEGRAM_BOT_TOKEN = token;
302
- const userId = await ask(rl, "Your Telegram User ID (from @userinfobot): ");
303
- if (userId) {
304
- env.ALLOWED_USER_ID = userId;
305
- env.ALLOWED_USERS = `telegram:${userId}`;
388
+ if (!existing.TELEGRAM_BOT_TOKEN) {
389
+ console.log(" Bot token → message @BotFather on Telegram, send /newbot");
390
+ console.log(" User ID → message @userinfobot on Telegram, copy the number");
391
+ console.log("");
392
+ }
393
+ if (existing.TELEGRAM_BOT_TOKEN) {
394
+ const token = await ask(rl, `Telegram Bot Token [${mask(existing.TELEGRAM_BOT_TOKEN)}]: `);
395
+ env.TELEGRAM_BOT_TOKEN = token || existing.TELEGRAM_BOT_TOKEN;
396
+ }
397
+ else {
398
+ const token = await ask(rl, "Telegram Bot Token (from @BotFather): ");
399
+ if (token)
400
+ env.TELEGRAM_BOT_TOKEN = token;
401
+ }
402
+ // User ID / Allowed Users
403
+ const existingUsers = existing.ALLOWED_USERS;
404
+ const existingUserId = existing.ALLOWED_USER_ID;
405
+ if (existingUsers) {
406
+ const users = await ask(rl, `Allowed users [${existingUsers}]: `);
407
+ env.ALLOWED_USERS = users || existingUsers;
408
+ }
409
+ else if (existingUserId) {
410
+ const userId = await ask(rl, `Telegram User ID [${existingUserId}]: `);
411
+ const id = userId || existingUserId;
412
+ env.ALLOWED_USER_ID = id;
413
+ env.ALLOWED_USERS = `telegram:${id}`;
306
414
  }
415
+ else {
416
+ const userId = await ask(rl, "Your Telegram User ID (from @userinfobot): ");
417
+ if (userId) {
418
+ env.ALLOWED_USER_ID = userId;
419
+ env.ALLOWED_USERS = `telegram:${userId}`;
420
+ }
421
+ }
422
+ // General config
307
423
  console.log("\n--- General Configuration ---\n");
308
- const workingDir = await ask(rl, `Working directory (default: ${os.homedir()}): `);
309
- if (workingDir)
310
- env.WORKING_DIR = workingDir;
311
- const botName = await ask(rl, "Bot name (default: Deskmate): ");
312
- if (botName)
313
- env.BOT_NAME = botName;
424
+ const defaultWorkingDir = existing.WORKING_DIR || os.homedir();
425
+ const workingDir = await ask(rl, `Working directory [${defaultWorkingDir}]: `);
426
+ env.WORKING_DIR = workingDir || defaultWorkingDir;
427
+ const defaultBotName = existing.BOT_NAME || "Deskmate";
428
+ const botName = await ask(rl, `Bot name [${defaultBotName}]: `);
429
+ env.BOT_NAME = botName || defaultBotName;
430
+ // Carry over other existing values that we don't prompt for
431
+ if (existing.LOG_LEVEL)
432
+ env.LOG_LEVEL = existing.LOG_LEVEL;
433
+ if (existing.REQUIRE_APPROVAL_FOR_ALL)
434
+ env.REQUIRE_APPROVAL_FOR_ALL = existing.REQUIRE_APPROVAL_FOR_ALL;
435
+ if (existing.ALLOWED_FOLDERS)
436
+ env.ALLOWED_FOLDERS = existing.ALLOWED_FOLDERS;
314
437
  // ----- Step 2: Write .env -----
315
- const envPath = path.join(process.cwd(), ".env");
316
438
  let envContent = "# Deskmate Configuration (generated by deskmate init)\n\n";
317
439
  for (const [key, value] of Object.entries(env)) {
318
440
  envContent += `${key}=${value}\n`;
@@ -322,20 +444,18 @@ async function runInitWizard() {
322
444
  if (!env.REQUIRE_APPROVAL_FOR_ALL)
323
445
  envContent += "REQUIRE_APPROVAL_FOR_ALL=false\n";
324
446
  try {
325
- let envExists = false;
326
- try {
327
- await fs.access(envPath);
328
- envExists = true;
329
- }
330
- catch {
331
- // does not exist
332
- }
333
- if (envExists) {
334
- console.log(`\nWARNING: .env file already exists at ${envPath}`);
335
- const newPath = envPath + ".new";
336
- await fs.writeFile(newPath, envContent, "utf-8");
337
- console.log(`Configuration saved to ${newPath}`);
338
- console.log("Review and rename it to .env when ready.");
447
+ if (hasExisting) {
448
+ const overwrite = await askYesNo(rl, "\nOverwrite existing .env with new values?");
449
+ if (overwrite) {
450
+ await fs.writeFile(envPath, envContent, "utf-8");
451
+ console.log(`Configuration saved to ${envPath}`);
452
+ }
453
+ else {
454
+ const newPath = envPath + ".new";
455
+ await fs.writeFile(newPath, envContent, "utf-8");
456
+ console.log(`Configuration saved to ${newPath}`);
457
+ console.log("Review and rename it to .env when ready.");
458
+ }
339
459
  }
340
460
  else {
341
461
  await fs.writeFile(envPath, envContent, "utf-8");
@@ -352,16 +472,15 @@ async function runInitWizard() {
352
472
  console.log("");
353
473
  const installService = await askYesNo(rl, "Install as background service?");
354
474
  if (installService) {
355
- // Determine project root (where package.json lives)
356
- const projectDir = path.resolve(__dirname, "..");
357
- const logsDir = path.join(projectDir, "logs");
475
+ const logsDir = path.join(paths.configDir, "logs");
358
476
  const runMode = "gateway";
477
+ const execStart = paths.execStart(runMode);
359
478
  try {
360
479
  if (platform === "macos") {
361
- await installMacosService(projectDir, logsDir, runMode);
480
+ await installMacosService(execStart, paths.configDir, logsDir);
362
481
  }
363
482
  else {
364
- await installLinuxService(projectDir, logsDir, runMode);
483
+ await installLinuxService(execStart, paths.configDir, logsDir);
365
484
  }
366
485
  }
367
486
  catch (err) {
@@ -383,5 +502,9 @@ async function runInitWizard() {
383
502
  }
384
503
  rl.close();
385
504
  console.log("\nSetup complete! Your bot is ready.");
505
+ if (paths.isGlobalInstall) {
506
+ console.log(`\nRun "deskmate" to start, or the background service is already running.`);
507
+ console.log(`Config: ${paths.configDir}/.env`);
508
+ }
386
509
  console.log("");
387
510
  }