junis 0.3.4 → 0.3.6

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/index.js CHANGED
@@ -1,92 +1,19 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
-
29
- // package.json
30
- var require_package = __commonJS({
31
- "package.json"(exports2, module2) {
32
- module2.exports = {
33
- name: "junis",
34
- version: "0.3.4",
35
- description: "One-line device control for AI agents",
36
- bin: {
37
- junis: "dist/cli/index.js"
38
- },
39
- scripts: {
40
- build: "tsup src/cli/index.ts --format cjs --dts --out-dir dist/cli && tsup src/server/mcp.ts --format cjs --out-dir dist/server && tsup src/server/stdio.ts --format cjs --out-dir dist/server",
41
- dev: "tsx src/cli/index.ts",
42
- "type-check": "tsc --noEmit",
43
- prepublishOnly: "npm run build"
44
- },
45
- dependencies: {
46
- "@inquirer/prompts": "^8.2.1",
47
- "@modelcontextprotocol/sdk": "^1.0.0",
48
- browserclaw: "^0.2.7",
49
- commander: "^12.0.0",
50
- execa: "^8.0.0",
51
- glob: "^11.0.0",
52
- "node-notifier": "^10.0.1",
53
- open: "^10.1.0",
54
- "playwright-core": ">=1.50.0",
55
- ws: "^8.18.0",
56
- zod: "^4.3.6"
57
- },
58
- devDependencies: {
59
- "@types/node": "^20.0.0",
60
- "@types/node-notifier": "^8.0.5",
61
- "@types/ws": "^8.5.0",
62
- tsup: "^8.0.0",
63
- tsx: "^4.0.0",
64
- typescript: "^5.0.0"
65
- },
66
- engines: {
67
- node: ">=18.0.0"
68
- },
69
- files: [
70
- "dist/"
71
- ]
72
- };
73
- }
74
- });
75
2
 
76
3
  // src/cli/index.ts
77
- var import_commander = require("commander");
78
- var import_prompts = require("@inquirer/prompts");
4
+ import { program } from "commander";
5
+ import { select } from "@inquirer/prompts";
79
6
 
80
7
  // src/cli/config.ts
81
- var import_fs = __toESM(require("fs"));
82
- var import_path = __toESM(require("path"));
83
- var import_os = __toESM(require("os"));
84
- var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".junis");
85
- var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ var CONFIG_DIR = path.join(os.homedir(), ".junis");
12
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
86
13
  function loadConfig() {
87
14
  try {
88
- if (!import_fs.default.existsSync(CONFIG_FILE)) return null;
89
- const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
15
+ if (!fs.existsSync(CONFIG_FILE)) return null;
16
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
90
17
  return JSON.parse(raw);
91
18
  } catch {
92
19
  return null;
@@ -94,9 +21,9 @@ function loadConfig() {
94
21
  }
95
22
  function saveConfig(config) {
96
23
  try {
97
- import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
98
- import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
99
- import_fs.default.chmodSync(CONFIG_FILE, 384);
24
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
25
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
26
+ fs.chmodSync(CONFIG_FILE, 384);
100
27
  } catch (err) {
101
28
  console.error(`
102
29
  \u274C Failed to save config file: ${err.message}`);
@@ -105,13 +32,13 @@ function saveConfig(config) {
105
32
  }
106
33
  }
107
34
  function clearConfig() {
108
- if (import_fs.default.existsSync(CONFIG_FILE)) {
109
- import_fs.default.unlinkSync(CONFIG_FILE);
35
+ if (fs.existsSync(CONFIG_FILE)) {
36
+ fs.unlinkSync(CONFIG_FILE);
110
37
  }
111
38
  }
112
39
 
113
40
  // src/cli/auth.ts
114
- var import_open = __toESM(require("open"));
41
+ import open from "open";
115
42
  var JUNIS_API = process.env.JUNIS_API_URL ?? "https://junis.ai";
116
43
  var JUNIS_WEB = (() => {
117
44
  if (process.env.JUNIS_WEB_URL) return process.env.JUNIS_WEB_URL;
@@ -157,7 +84,7 @@ async function authenticate(deviceName, platform3, onBrowserOpen, onWaiting, exi
157
84
  const verificationUri = JUNIS_WEB ? startData.verification_uri.replace(/^https?:\/\/[^/]+/, JUNIS_WEB) : startData.verification_uri;
158
85
  onBrowserOpen?.(verificationUri);
159
86
  try {
160
- await (0, import_open.default)(verificationUri);
87
+ await open(verificationUri);
161
88
  } catch {
162
89
  console.warn(`
163
90
  \u26A0\uFE0F Could not open browser automatically. Please open the following URL manually:
@@ -199,12 +126,45 @@ async function authenticate(deviceName, platform3, onBrowserOpen, onWaiting, exi
199
126
  }
200
127
  throw new Error("Authentication timed out (5 min). Please try again.");
201
128
  }
129
+ async function ensureTeam(deviceKey, token) {
130
+ let res;
131
+ try {
132
+ res = await fetch(`${JUNIS_API}/api/auth/device/ensure-team`, {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ Authorization: `Bearer ${token}`
137
+ },
138
+ body: JSON.stringify({ device_key: deviceKey })
139
+ });
140
+ } catch {
141
+ throw new Error("Network error: cannot reach server");
142
+ }
143
+ if (res.status === 401) {
144
+ return { status: "auth_expired" };
145
+ }
146
+ if (res.status === 404) {
147
+ return { status: "device_not_found" };
148
+ }
149
+ if (!res.ok) {
150
+ throw new Error(`ensure-team failed: ${res.status}`);
151
+ }
152
+ const data = await res.json();
153
+ return {
154
+ status: data.status,
155
+ // "ready" or "created"
156
+ organization_id: data.organization_id,
157
+ agent_id: data.agent_id,
158
+ agent_name: data.agent_name,
159
+ token: data.token
160
+ };
161
+ }
202
162
  function sleep(ms) {
203
163
  return new Promise((resolve) => setTimeout(resolve, ms));
204
164
  }
205
165
 
206
166
  // src/relay/client.ts
207
- var import_ws = __toESM(require("ws"));
167
+ import WebSocket from "ws";
208
168
  var JUNIS_WS = (() => {
209
169
  if (process.env.JUNIS_WS_URL) return process.env.JUNIS_WS_URL;
210
170
  const apiUrl = process.env.JUNIS_API_URL ?? "https://junis.ai";
@@ -230,7 +190,7 @@ var RelayClient = class {
230
190
  if (this.destroyed) return;
231
191
  const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
232
192
  console.log(`\u{1F517} Connecting to relay server...`);
233
- const ws = new import_ws.default(url, {
193
+ const ws = new WebSocket(url, {
234
194
  headers: { Authorization: `Bearer ${this.config.token}` }
235
195
  });
236
196
  this.ws = ws;
@@ -280,6 +240,13 @@ var RelayClient = class {
280
240
  }
281
241
  return;
282
242
  }
243
+ if (code === 4004 || code === 4005) {
244
+ this.destroyed = true;
245
+ console.error(
246
+ "\n\u274C Device or team configuration is invalid. Run `npx junis --reset` to re-authenticate."
247
+ );
248
+ process.exit(1);
249
+ }
283
250
  console.log(`\u26A0\uFE0F Disconnected. Reconnecting in ${this.reconnectDelay / 1e3}s...`);
284
251
  setTimeout(() => this.connect(), this.reconnectDelay);
285
252
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
@@ -295,7 +262,7 @@ var RelayClient = class {
295
262
  this.connect();
296
263
  }
297
264
  send(data) {
298
- if (this.ws?.readyState === import_ws.default.OPEN) {
265
+ if (this.ws?.readyState === WebSocket.OPEN) {
299
266
  this.ws.send(JSON.stringify(data));
300
267
  }
301
268
  }
@@ -323,17 +290,17 @@ var RelayClient = class {
323
290
  };
324
291
 
325
292
  // src/server/mcp.ts
326
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
327
- var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
328
- var import_http = require("http");
293
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
294
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
295
+ import { createServer } from "http";
329
296
 
330
297
  // src/tools/filesystem.ts
331
- var import_child_process = require("child_process");
332
- var import_util = require("util");
333
- var import_promises = __toESM(require("fs/promises"));
334
- var import_path2 = __toESM(require("path"));
335
- var import_glob = require("glob");
336
- var import_zod = require("zod");
298
+ import { exec, execFile } from "child_process";
299
+ import { promisify } from "util";
300
+ import fs2 from "fs/promises";
301
+ import path2 from "path";
302
+ import { glob } from "glob";
303
+ import { z } from "zod";
337
304
 
338
305
  // src/server/permissions.ts
339
306
  var toolPermissions = {
@@ -371,9 +338,9 @@ var toolPermissions = {
371
338
  cron_delete: "confirm",
372
339
  edit_block: "confirm",
373
340
  kill_process: "confirm",
374
- // 시스템 변경 — 허용
375
- execute_command: "confirm",
376
- write_file: "confirm"
341
+ // 시스템 변경 — 기본 차단 (PDF 7.3절)
342
+ execute_command: "deny",
343
+ write_file: "deny"
377
344
  };
378
345
  function checkPermission(toolName) {
379
346
  const level = toolPermissions[toolName];
@@ -385,8 +352,8 @@ function checkPermission(toolName) {
385
352
  }
386
353
 
387
354
  // src/tools/filesystem.ts
388
- var execAsync = (0, import_util.promisify)(import_child_process.exec);
389
- var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
355
+ var execAsync = promisify(exec);
356
+ var execFileAsync = promisify(execFile);
390
357
  var FilesystemTools = class {
391
358
  register(server) {
392
359
  server.tool(
@@ -408,14 +375,14 @@ var FilesystemTools = class {
408
375
  "- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
409
376
  ].join("\n"),
410
377
  {
411
- command: import_zod.z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
412
- timeout_ms: import_zod.z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
413
- background: import_zod.z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
378
+ command: z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
379
+ timeout_ms: z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
380
+ background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
414
381
  },
415
382
  async ({ command, timeout_ms, background }) => {
416
383
  checkPermission("execute_command");
417
384
  if (background) {
418
- (0, import_child_process.exec)(command);
385
+ exec(command);
419
386
  return { content: [{ type: "text", text: "Background execution started" }] };
420
387
  }
421
388
  try {
@@ -449,12 +416,12 @@ ${error.stderr ?? ""}`
449
416
  "For searching within files, prefer search_code instead. For listing directory contents, use list_directory."
450
417
  ].join("\n"),
451
418
  {
452
- path: import_zod.z.string().describe("Absolute or relative file path to read"),
453
- encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("'utf-8' for text files (default), 'base64' for binary files (images, PDFs, archives)")
419
+ path: z.string().describe("Absolute or relative file path to read"),
420
+ encoding: z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("'utf-8' for text files (default), 'base64' for binary files (images, PDFs, archives)")
454
421
  },
455
422
  async ({ path: filePath, encoding }) => {
456
423
  try {
457
- const content = await import_promises.default.readFile(filePath, encoding);
424
+ const content = await fs2.readFile(filePath, encoding);
458
425
  return { content: [{ type: "text", text: content }] };
459
426
  } catch (err) {
460
427
  const e = err;
@@ -474,13 +441,13 @@ ${error.stderr ?? ""}`
474
441
  "Prefer edit_block over write_file for existing files \u2014 it's safer and preserves unmodified content."
475
442
  ].join("\n"),
476
443
  {
477
- path: import_zod.z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
478
- content: import_zod.z.string().describe("Complete file content. This replaces the entire file.")
444
+ path: z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
445
+ content: z.string().describe("Complete file content. This replaces the entire file.")
479
446
  },
480
447
  async ({ path: filePath, content }) => {
481
448
  checkPermission("write_file");
482
- await import_promises.default.mkdir(import_path2.default.dirname(filePath), { recursive: true });
483
- await import_promises.default.writeFile(filePath, content, "utf-8");
449
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
450
+ await fs2.writeFile(filePath, content, "utf-8");
484
451
  return { content: [{ type: "text", text: "File saved" }] };
485
452
  }
486
453
  );
@@ -491,11 +458,11 @@ ${error.stderr ?? ""}`
491
458
  "Use this to explore project structure before reading or modifying files."
492
459
  ].join("\n"),
493
460
  {
494
- path: import_zod.z.string().describe("Directory path to list")
461
+ path: z.string().describe("Directory path to list")
495
462
  },
496
463
  async ({ path: dirPath }) => {
497
464
  try {
498
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
465
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
499
466
  const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
500
467
  return { content: [{ type: "text", text: lines.join("\n") }] };
501
468
  } catch (err) {
@@ -516,9 +483,9 @@ ${error.stderr ?? ""}`
516
483
  "Returns matching lines with file paths and line numbers for precise navigation."
517
484
  ].join("\n"),
518
485
  {
519
- pattern: import_zod.z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
520
- directory: import_zod.z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
521
- file_pattern: import_zod.z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
486
+ pattern: z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
487
+ directory: z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
488
+ file_pattern: z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
522
489
  },
523
490
  async ({ pattern, directory, file_pattern }) => {
524
491
  try {
@@ -529,13 +496,13 @@ ${error.stderr ?? ""}`
529
496
  );
530
497
  return { content: [{ type: "text", text: stdout || "No results" }] };
531
498
  } catch {
532
- const safeDirectory = import_path2.default.resolve(directory);
533
- const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
499
+ const safeDirectory = path2.resolve(directory);
500
+ const files = await glob(file_pattern, { cwd: safeDirectory });
534
501
  const results = [];
535
502
  for (const file of files.slice(0, 100)) {
536
503
  try {
537
- const content = await import_promises.default.readFile(
538
- import_path2.default.join(safeDirectory, file),
504
+ const content = await fs2.readFile(
505
+ path2.join(safeDirectory, file),
539
506
  "utf-8"
540
507
  );
541
508
  const lines = content.split("\n");
@@ -572,8 +539,8 @@ ${error.stderr ?? ""}`
572
539
  "SAFETY: Only kill processes the user explicitly identifies. Never kill system-critical processes (init, systemd, loginwindow, WindowServer) without explicit instruction."
573
540
  ].join("\n"),
574
541
  {
575
- pid: import_zod.z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
576
- signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
542
+ pid: z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
543
+ signal: z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
577
544
  },
578
545
  async ({ pid, signal }) => {
579
546
  const isWindows = process.platform === "win32";
@@ -629,13 +596,13 @@ ${error.stderr ?? ""}`
629
596
  "Prefer this over write_file for modifying existing files \u2014 it only changes what you specify and preserves the rest."
630
597
  ].join("\n"),
631
598
  {
632
- path: import_zod.z.string().describe("Path to the file to edit. The file must already exist."),
633
- old_string: import_zod.z.string().describe("The exact text to find and replace. Must match character-for-character including whitespace and newlines. Include enough context for uniqueness."),
634
- new_string: import_zod.z.string().describe("The replacement text. Use empty string to delete the matched text."),
635
- replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace ALL matches. If false (default), require exactly one match (errors on ambiguous multiple matches).")
599
+ path: z.string().describe("Path to the file to edit. The file must already exist."),
600
+ old_string: z.string().describe("The exact text to find and replace. Must match character-for-character including whitespace and newlines. Include enough context for uniqueness."),
601
+ new_string: z.string().describe("The replacement text. Use empty string to delete the matched text."),
602
+ replace_all: z.boolean().optional().default(false).describe("If true, replace ALL matches. If false (default), require exactly one match (errors on ambiguous multiple matches).")
636
603
  },
637
604
  async ({ path: filePath, old_string, new_string, replace_all }) => {
638
- const content = await import_promises.default.readFile(filePath, "utf-8");
605
+ const content = await fs2.readFile(filePath, "utf-8");
639
606
  if (!content.includes(old_string)) {
640
607
  throw new Error(`old_string not found in file: ${filePath}`);
641
608
  }
@@ -659,7 +626,7 @@ ${error.stderr ?? ""}`
659
626
  result = content.replace(old_string, new_string);
660
627
  replaced = 1;
661
628
  }
662
- await import_promises.default.writeFile(filePath, result, "utf-8");
629
+ await fs2.writeFile(filePath, result, "utf-8");
663
630
  return {
664
631
  content: [{ type: "text", text: `Replaced (${replaced} occurrence(s) changed)` }]
665
632
  };
@@ -674,9 +641,9 @@ ${error.stderr ?? ""}`
674
641
  "Duplicate commands are automatically detected and rejected. Use cron_list to see existing jobs."
675
642
  ].join("\n"),
676
643
  {
677
- schedule: import_zod.z.string().describe("Cron schedule expression (5 fields: minute hour day month weekday). Examples: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am)"),
678
- command: import_zod.z.string().describe("Shell command to execute on schedule"),
679
- label: import_zod.z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
644
+ schedule: z.string().describe("Cron schedule expression (5 fields: minute hour day month weekday). Examples: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am)"),
645
+ command: z.string().describe("Shell command to execute on schedule"),
646
+ label: z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
680
647
  },
681
648
  async ({ schedule, command, label }) => {
682
649
  try {
@@ -698,9 +665,9 @@ ${error.stderr ?? ""}`
698
665
  `;
699
666
  const updated = existing.trimEnd() + "\n" + newEntry;
700
667
  const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
701
- await import_promises.default.writeFile(tmpFile, updated, "utf-8");
668
+ await fs2.writeFile(tmpFile, updated, "utf-8");
702
669
  await execAsync(`crontab ${tmpFile}`);
703
- await import_promises.default.unlink(tmpFile).catch(() => {
670
+ await fs2.unlink(tmpFile).catch(() => {
704
671
  });
705
672
  return {
706
673
  content: [{ type: "text", text: `\u2705 Cron job created:
@@ -767,8 +734,8 @@ ${error.stderr ?? ""}`
767
734
  "cron_delete",
768
735
  "Delete a scheduled cron job by its ID (from cron_list output) or by matching command string. Associated comment labels are automatically cleaned up.",
769
736
  {
770
- id: import_zod.z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
771
- command: import_zod.z.string().optional().describe("Delete all jobs matching this command string")
737
+ id: z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
738
+ command: z.string().optional().describe("Delete all jobs matching this command string")
772
739
  },
773
740
  async ({ id, command }) => {
774
741
  if (!id && !command) {
@@ -805,9 +772,9 @@ ${error.stderr ?? ""}`
805
772
  }
806
773
  const updated2 = filtered2.join("\n");
807
774
  const tmpFile2 = `/tmp/junis_crontab_${Date.now()}.txt`;
808
- await import_promises.default.writeFile(tmpFile2, updated2, "utf-8");
775
+ await fs2.writeFile(tmpFile2, updated2, "utf-8");
809
776
  await execAsync(`crontab ${tmpFile2}`);
810
- await import_promises.default.unlink(tmpFile2).catch(() => {
777
+ await fs2.unlink(tmpFile2).catch(() => {
811
778
  });
812
779
  return { content: [{ type: "text", text: `\u2705 Deleted cron job matching: ${command}` }] };
813
780
  }
@@ -832,9 +799,9 @@ ${error.stderr ?? ""}`
832
799
  const filtered = lines.filter((_, i) => i < target.lineStart || i > target.lineEnd);
833
800
  const updated = filtered.join("\n");
834
801
  const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
835
- await import_promises.default.writeFile(tmpFile, updated, "utf-8");
802
+ await fs2.writeFile(tmpFile, updated, "utf-8");
836
803
  await execAsync(`crontab ${tmpFile}`);
837
- await import_promises.default.unlink(tmpFile).catch(() => {
804
+ await fs2.unlink(tmpFile).catch(() => {
838
805
  });
839
806
  return { content: [{ type: "text", text: `\u2705 Deleted cron job #${id}` }] };
840
807
  } catch (err) {
@@ -849,9 +816,9 @@ ${error.stderr ?? ""}`
849
816
  };
850
817
 
851
818
  // src/tools/browser.ts
852
- var import_browserclaw = require("browserclaw");
853
- var import_promises2 = __toESM(require("fs/promises"));
854
- var import_zod2 = require("zod");
819
+ import { BrowserClaw } from "browserclaw";
820
+ import fs3 from "fs/promises";
821
+ import { z as z2 } from "zod";
855
822
  var BrowserTools = class {
856
823
  browser = null;
857
824
  page = null;
@@ -892,11 +859,11 @@ var BrowserTools = class {
892
859
  "Always call browser_stop when done to release system resources."
893
860
  ].join("\n"),
894
861
  {
895
- mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
896
- headless: import_zod2.z.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
897
- cdpUrl: import_zod2.z.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
898
- profile: import_zod2.z.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
899
- allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
862
+ mode: z2.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
863
+ headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
864
+ cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
865
+ profile: z2.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
866
+ allowInternal: z2.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
900
867
  },
901
868
  ({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
902
869
  if (this.browser) {
@@ -904,9 +871,9 @@ var BrowserTools = class {
904
871
  }
905
872
  if (mode === "remote-cdp") {
906
873
  if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
907
- this.browser = await import_browserclaw.BrowserClaw.connect(cdpUrl, { allowInternal });
874
+ this.browser = await BrowserClaw.connect(cdpUrl, { allowInternal });
908
875
  } else {
909
- this.browser = await import_browserclaw.BrowserClaw.launch({
876
+ this.browser = await BrowserClaw.launch({
910
877
  headless,
911
878
  profileName: profile,
912
879
  allowInternal
@@ -928,7 +895,7 @@ var BrowserTools = class {
928
895
  "browser_navigate",
929
896
  "Navigate the browser to a URL. Automatically opens a new tab if the browser is started but no page exists yet. Waits for the page to load before returning.",
930
897
  {
931
- url: import_zod2.z.string().describe("Full URL to navigate to (include https://)")
898
+ url: z2.string().describe("Full URL to navigate to (include https://)")
932
899
  },
933
900
  ({ url }) => this.withLock(async () => {
934
901
  if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
@@ -952,8 +919,8 @@ var BrowserTools = class {
952
919
  "Prefer this over browser_screenshot for understanding page structure \u2014 it's faster, structured, and machine-readable."
953
920
  ].join("\n"),
954
921
  {
955
- interactive: import_zod2.z.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
956
- compact: import_zod2.z.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
922
+ interactive: z2.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
923
+ compact: z2.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
957
924
  },
958
925
  ({ interactive, compact }) => this.withLock(async () => {
959
926
  const result = await requirePage().snapshot({ interactive, compact });
@@ -975,9 +942,9 @@ ${refList}`
975
942
  "browser_click",
976
943
  "Click an element by its ref number from browser_snapshot. Always call browser_snapshot first to get current refs \u2014 they change after page updates.",
977
944
  {
978
- ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
979
- doubleClick: import_zod2.z.boolean().optional().default(false).describe("Double-click instead of single click"),
980
- button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
945
+ ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
946
+ doubleClick: z2.boolean().optional().default(false).describe("Double-click instead of single click"),
947
+ button: z2.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
981
948
  },
982
949
  ({ ref, doubleClick, button }) => this.withLock(async () => {
983
950
  await requirePage().click(ref, { doubleClick, button });
@@ -988,10 +955,10 @@ ${refList}`
988
955
  "browser_type",
989
956
  "Type text into an input element by ref number. Use 'submit=true' to press Enter after typing (e.g. for search forms). Use 'slowly=true' for sites requiring keystroke-by-keystroke input.",
990
957
  {
991
- ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
992
- text: import_zod2.z.string().describe("Text to type into the element"),
993
- submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
994
- slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
958
+ ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
959
+ text: z2.string().describe("Text to type into the element"),
960
+ submit: z2.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
961
+ slowly: z2.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
995
962
  },
996
963
  ({ ref, text, submit, slowly }) => this.withLock(async () => {
997
964
  await requirePage().type(ref, text, { submit, slowly });
@@ -1002,10 +969,10 @@ ${refList}`
1002
969
  "browser_fill",
1003
970
  "Fill multiple form fields at once \u2014 more efficient than calling browser_type repeatedly. Each field needs a ref from browser_snapshot.",
1004
971
  {
1005
- fields: import_zod2.z.array(import_zod2.z.object({
1006
- ref: import_zod2.z.string(),
1007
- type: import_zod2.z.enum(["text", "checkbox", "radio"]),
1008
- value: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()])
972
+ fields: z2.array(z2.object({
973
+ ref: z2.string(),
974
+ type: z2.enum(["text", "checkbox", "radio"]),
975
+ value: z2.union([z2.string(), z2.boolean()])
1009
976
  })).describe("Array of {ref, type, value}. type='text': value is string. type='checkbox'/'radio': value is boolean.")
1010
977
  },
1011
978
  ({ fields }) => this.withLock(async () => {
@@ -1017,8 +984,8 @@ ${refList}`
1017
984
  "browser_select",
1018
985
  "Select one or more options from a dropdown/select element. Values should match the option value attributes, not display text.",
1019
986
  {
1020
- ref: import_zod2.z.string().describe("Ref of the <select> element from browser_snapshot"),
1021
- values: import_zod2.z.array(import_zod2.z.string()).describe("Option value(s) to select")
987
+ ref: z2.string().describe("Ref of the <select> element from browser_snapshot"),
988
+ values: z2.array(z2.string()).describe("Option value(s) to select")
1022
989
  },
1023
990
  ({ ref, values }) => this.withLock(async () => {
1024
991
  await requirePage().select(ref, ...values);
@@ -1029,7 +996,7 @@ ${refList}`
1029
996
  "browser_press",
1030
997
  "Press a keyboard key or key combination. Use for shortcuts (e.g. 'Control+a', 'Escape'), form submission ('Enter'), or navigation ('Tab'). Does not require a specific element ref.",
1031
998
  {
1032
- key: import_zod2.z.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
999
+ key: z2.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
1033
1000
  },
1034
1001
  ({ key }) => this.withLock(async () => {
1035
1002
  await requirePage().press(key);
@@ -1040,7 +1007,7 @@ ${refList}`
1040
1007
  "browser_hover",
1041
1008
  "Move the mouse cursor over an element by ref. Use to trigger hover menus, tooltips, or dropdown previews before clicking.",
1042
1009
  {
1043
- ref: import_zod2.z.string().describe("Element ref from browser_snapshot")
1010
+ ref: z2.string().describe("Element ref from browser_snapshot")
1044
1011
  },
1045
1012
  ({ ref }) => this.withLock(async () => {
1046
1013
  await requirePage().hover(ref);
@@ -1051,8 +1018,8 @@ ${refList}`
1051
1018
  "browser_drag",
1052
1019
  "Drag an element from startRef to endRef. Both refs must come from a recent browser_snapshot. Use for drag-and-drop interfaces, sliders, or reorderable lists.",
1053
1020
  {
1054
- startRef: import_zod2.z.string().describe("Source element ref to drag from"),
1055
- endRef: import_zod2.z.string().describe("Target element ref to drag to")
1021
+ startRef: z2.string().describe("Source element ref to drag from"),
1022
+ endRef: z2.string().describe("Target element ref to drag to")
1056
1023
  },
1057
1024
  ({ startRef, endRef }) => this.withLock(async () => {
1058
1025
  await requirePage().drag(startRef, endRef);
@@ -1063,8 +1030,8 @@ ${refList}`
1063
1030
  "browser_upload",
1064
1031
  "Upload local files to a file input element (<input type='file'>). The ref must point to a file input from browser_snapshot.",
1065
1032
  {
1066
- ref: import_zod2.z.string().describe("Ref of the file input element from browser_snapshot"),
1067
- paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) on the local device to upload")
1033
+ ref: z2.string().describe("Ref of the file input element from browser_snapshot"),
1034
+ paths: z2.array(z2.string()).describe("Absolute file path(s) on the local device to upload")
1068
1035
  },
1069
1036
  ({ ref, paths }) => this.withLock(async () => {
1070
1037
  await requirePage().uploadFile(ref, paths);
@@ -1080,14 +1047,14 @@ ${refList}`
1080
1047
  "Use browser_screenshot only when visual layout matters (charts, images, styling, visual verification)."
1081
1048
  ].join("\n"),
1082
1049
  {
1083
- path: import_zod2.z.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
1084
- fullPage: import_zod2.z.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
1085
- ref: import_zod2.z.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
1050
+ path: z2.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
1051
+ fullPage: z2.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
1052
+ ref: z2.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
1086
1053
  },
1087
1054
  ({ path: path4, fullPage, ref }) => this.withLock(async () => {
1088
1055
  const buffer = await requirePage().screenshot({ fullPage, ref });
1089
1056
  if (path4) {
1090
- await import_promises2.default.writeFile(path4, buffer);
1057
+ await fs3.writeFile(path4, buffer);
1091
1058
  return { content: [{ type: "text", text: `Screenshot saved: ${path4}` }] };
1092
1059
  }
1093
1060
  return {
@@ -1103,11 +1070,11 @@ ${refList}`
1103
1070
  "browser_pdf",
1104
1071
  "Save the current page as a PDF file. Renders the full page including below-the-fold content. Useful for archiving, sharing, or offline reading.",
1105
1072
  {
1106
- path: import_zod2.z.string().describe("Output file path (.pdf)")
1073
+ path: z2.string().describe("Output file path (.pdf)")
1107
1074
  },
1108
1075
  ({ path: path4 }) => this.withLock(async () => {
1109
1076
  const buffer = await requirePage().pdf();
1110
- await import_promises2.default.writeFile(path4, buffer);
1077
+ await fs3.writeFile(path4, buffer);
1111
1078
  return { content: [{ type: "text", text: `PDF saved: ${path4}` }] };
1112
1079
  })
1113
1080
  );
@@ -1120,7 +1087,7 @@ ${refList}`
1120
1087
  "Wrap complex logic in an IIFE: (function(){ ... })()"
1121
1088
  ].join("\n"),
1122
1089
  {
1123
- code: import_zod2.z.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
1090
+ code: z2.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
1124
1091
  },
1125
1092
  ({ code }) => this.withLock(async () => {
1126
1093
  try {
@@ -1147,11 +1114,11 @@ ${refList}`
1147
1114
  "OPTIONS (use one): 'text' (wait for text to appear), 'textGone' (wait for text to disappear), 'url' (URL matches glob), 'loadState' (page load state), 'timeMs' (fixed delay as last resort)."
1148
1115
  ].join("\n"),
1149
1116
  {
1150
- text: import_zod2.z.string().optional().describe("Wait until this text appears on the page"),
1151
- textGone: import_zod2.z.string().optional().describe("Wait until this text disappears from the page"),
1152
- url: import_zod2.z.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
1153
- loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
1154
- timeMs: import_zod2.z.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
1117
+ text: z2.string().optional().describe("Wait until this text appears on the page"),
1118
+ textGone: z2.string().optional().describe("Wait until this text disappears from the page"),
1119
+ url: z2.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
1120
+ loadState: z2.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
1121
+ timeMs: z2.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
1155
1122
  },
1156
1123
  ({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
1157
1124
  const condition = {};
@@ -1168,14 +1135,14 @@ ${refList}`
1168
1135
  "browser_cookies",
1169
1136
  "Manage browser cookies: get all cookies, set a specific cookie, or clear all cookies. Useful for authentication state, session management, or testing.",
1170
1137
  {
1171
- action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
1172
- cookie: import_zod2.z.object({
1173
- name: import_zod2.z.string(),
1174
- value: import_zod2.z.string(),
1175
- domain: import_zod2.z.string().optional(),
1176
- path: import_zod2.z.string().optional(),
1177
- httpOnly: import_zod2.z.boolean().optional(),
1178
- secure: import_zod2.z.boolean().optional()
1138
+ action: z2.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
1139
+ cookie: z2.object({
1140
+ name: z2.string(),
1141
+ value: z2.string(),
1142
+ domain: z2.string().optional(),
1143
+ path: z2.string().optional(),
1144
+ httpOnly: z2.boolean().optional(),
1145
+ secure: z2.boolean().optional()
1179
1146
  }).optional().describe("Cookie data (required for 'set' action)")
1180
1147
  },
1181
1148
  ({ action, cookie }) => this.withLock(async () => {
@@ -1197,10 +1164,10 @@ ${refList}`
1197
1164
  "browser_storage",
1198
1165
  "Read, write, or clear browser localStorage/sessionStorage. Useful for managing client-side state, authentication tokens, or application preferences.",
1199
1166
  {
1200
- action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
1201
- kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
1202
- key: import_zod2.z.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
1203
- value: import_zod2.z.string().optional().describe("Value to store (required for 'set' action)")
1167
+ action: z2.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
1168
+ kind: z2.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
1169
+ key: z2.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
1170
+ value: z2.string().optional().describe("Value to store (required for 'set' action)")
1204
1171
  },
1205
1172
  ({ action, kind, key, value }) => this.withLock(async () => {
1206
1173
  const page = requirePage();
@@ -1228,16 +1195,16 @@ ${refList}`
1228
1195
  "The 'accept' and 'promptText' params are only used with action='arm'."
1229
1196
  ].join("\n"),
1230
1197
  {
1231
- action: import_zod2.z.enum(["arm", "wait"]).describe(
1198
+ action: z2.enum(["arm", "wait"]).describe(
1232
1199
  "'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
1233
1200
  ),
1234
- accept: import_zod2.z.boolean().optional().default(true).describe(
1201
+ accept: z2.boolean().optional().default(true).describe(
1235
1202
  "Accept (true) or dismiss (false) the dialog. Only used with action='arm'."
1236
1203
  ),
1237
- promptText: import_zod2.z.string().optional().describe(
1204
+ promptText: z2.string().optional().describe(
1238
1205
  "Text to enter if the dialog is a prompt. Only used with action='arm'."
1239
1206
  ),
1240
- timeoutMs: import_zod2.z.number().optional().describe(
1207
+ timeoutMs: z2.number().optional().describe(
1241
1208
  "Timeout in ms for 'wait' action (default: 30000). Increase for slow-loading dialogs."
1242
1209
  )
1243
1210
  },
@@ -1269,13 +1236,13 @@ ${refList}`
1269
1236
  };
1270
1237
 
1271
1238
  // src/tools/notebook.ts
1272
- var import_zod3 = require("zod");
1273
- var import_promises3 = __toESM(require("fs/promises"));
1274
- var import_child_process2 = require("child_process");
1275
- var import_util2 = require("util");
1276
- var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
1239
+ import { z as z3 } from "zod";
1240
+ import fs4 from "fs/promises";
1241
+ import { exec as exec2 } from "child_process";
1242
+ import { promisify as promisify2 } from "util";
1243
+ var execAsync2 = promisify2(exec2);
1277
1244
  async function readNotebook(filePath) {
1278
- const raw = await import_promises3.default.readFile(filePath, "utf-8");
1245
+ const raw = await fs4.readFile(filePath, "utf-8");
1279
1246
  try {
1280
1247
  return JSON.parse(raw);
1281
1248
  } catch {
@@ -1283,14 +1250,14 @@ async function readNotebook(filePath) {
1283
1250
  }
1284
1251
  }
1285
1252
  async function writeNotebook(filePath, nb) {
1286
- await import_promises3.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
1253
+ await fs4.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
1287
1254
  }
1288
1255
  var NotebookTools = class {
1289
1256
  register(server) {
1290
1257
  server.tool(
1291
1258
  "notebook_read",
1292
1259
  "Read a Jupyter notebook (.ipynb) and return all cells with their types (code/markdown), source content, and output counts. Use this to understand notebook structure before making edits.",
1293
- { path: import_zod3.z.string().describe("Path to the .ipynb notebook file") },
1260
+ { path: z3.string().describe("Path to the .ipynb notebook file") },
1294
1261
  async ({ path: filePath }) => {
1295
1262
  const nb = await readNotebook(filePath);
1296
1263
  const cells = nb.cells.map((cell, i) => ({
@@ -1308,9 +1275,9 @@ var NotebookTools = class {
1308
1275
  "notebook_edit_cell",
1309
1276
  "Replace the source code of a specific cell in a Jupyter notebook. Use notebook_read first to identify the correct cell index (0-based). Existing outputs for the cell are preserved \u2014 use notebook_execute to re-run.",
1310
1277
  {
1311
- path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1312
- cell_index: import_zod3.z.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
1313
- source: import_zod3.z.string().describe("New source code/content for the cell (replaces entire cell content)")
1278
+ path: z3.string().describe("Path to the .ipynb notebook file"),
1279
+ cell_index: z3.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
1280
+ source: z3.string().describe("New source code/content for the cell (replaces entire cell content)")
1314
1281
  },
1315
1282
  async ({ path: filePath, cell_index, source }) => {
1316
1283
  const nb = await readNotebook(filePath);
@@ -1333,8 +1300,8 @@ var NotebookTools = class {
1333
1300
  "If execution fails on a cell, the error is captured in the cell output and subsequent cells may not execute."
1334
1301
  ].join("\n"),
1335
1302
  {
1336
- path: import_zod3.z.string().describe("Path to the .ipynb notebook file to execute"),
1337
- timeout: import_zod3.z.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
1303
+ path: z3.string().describe("Path to the .ipynb notebook file to execute"),
1304
+ timeout: z3.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
1338
1305
  },
1339
1306
  async ({ path: filePath, timeout }) => {
1340
1307
  const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
@@ -1365,10 +1332,10 @@ var NotebookTools = class {
1365
1332
  "notebook_add_cell",
1366
1333
  "Insert a new cell into a Jupyter notebook. If position is omitted, the cell is appended at the end. Use cell_type='code' for executable Python cells, 'markdown' for documentation/text cells.",
1367
1334
  {
1368
- path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1369
- cell_type: import_zod3.z.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
1370
- source: import_zod3.z.string().describe("Cell source content (Python code or Markdown text)"),
1371
- position: import_zod3.z.number().optional().describe("Insert position (0-based index). Omit to append at the end. If position exceeds cell count, appends at end with a warning.")
1335
+ path: z3.string().describe("Path to the .ipynb notebook file"),
1336
+ cell_type: z3.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
1337
+ source: z3.string().describe("Cell source content (Python code or Markdown text)"),
1338
+ position: z3.number().optional().describe("Insert position (0-based index). Omit to append at the end. If position exceeds cell count, appends at end with a warning.")
1372
1339
  },
1373
1340
  async ({ path: filePath, cell_type: cellType, source, position }) => {
1374
1341
  const nb = await readNotebook(filePath);
@@ -1401,8 +1368,8 @@ var NotebookTools = class {
1401
1368
  "notebook_delete_cell",
1402
1369
  "Delete a cell from a Jupyter notebook by its 0-based index. Use notebook_read first to verify the cell content before deletion. This action cannot be undone.",
1403
1370
  {
1404
- path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1405
- cell_index: import_zod3.z.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
1371
+ path: z3.string().describe("Path to the .ipynb notebook file"),
1372
+ cell_index: z3.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
1406
1373
  },
1407
1374
  async ({ path: filePath, cell_index }) => {
1408
1375
  const nb = await readNotebook(filePath);
@@ -1418,11 +1385,11 @@ var NotebookTools = class {
1418
1385
  };
1419
1386
 
1420
1387
  // src/tools/device.ts
1421
- var import_child_process3 = require("child_process");
1422
- var import_util3 = require("util");
1423
- var import_zod4 = require("zod");
1424
- var import_node_notifier = __toESM(require("node-notifier"));
1425
- var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
1388
+ import { exec as exec3 } from "child_process";
1389
+ import { promisify as promisify3 } from "util";
1390
+ import { z as z4 } from "zod";
1391
+ import notifier from "node-notifier";
1392
+ var execAsync3 = promisify3(exec3);
1426
1393
  var screenRecordPid = null;
1427
1394
  function platform() {
1428
1395
  if (process.platform === "darwin") return "mac";
@@ -1440,7 +1407,7 @@ var DeviceTools = class {
1440
1407
  "Requires a connected camera with OS permissions granted. If output_path is provided, the file is also saved to disk."
1441
1408
  ].join("\n"),
1442
1409
  {
1443
- output_path: import_zod4.z.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
1410
+ output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
1444
1411
  },
1445
1412
  async ({ output_path }) => {
1446
1413
  const p = platform();
@@ -1478,13 +1445,13 @@ Please check if a camera is connected.` }],
1478
1445
  "notification_send",
1479
1446
  "Send a native OS notification (banner/toast) to the user's desktop. Use for task completion alerts, reminders, or important status updates. The notification appears even when the terminal is not focused.",
1480
1447
  {
1481
- title: import_zod4.z.string().describe("Notification title (displayed prominently)"),
1482
- message: import_zod4.z.string().describe("Notification body text")
1448
+ title: z4.string().describe("Notification title (displayed prominently)"),
1449
+ message: z4.string().describe("Notification body text")
1483
1450
  },
1484
1451
  async ({ title, message }) => {
1485
1452
  try {
1486
1453
  await new Promise((resolve, reject) => {
1487
- import_node_notifier.default.notify(
1454
+ notifier.notify(
1488
1455
  { title, message },
1489
1456
  (err) => {
1490
1457
  if (err) reject(err);
@@ -1516,7 +1483,7 @@ Please check if a camera is connected.` }],
1516
1483
  "clipboard_write",
1517
1484
  "Write text to the system clipboard, replacing its current contents. Use to prepare content for the user to paste elsewhere.",
1518
1485
  {
1519
- text: import_zod4.z.string().describe("Text to copy to the clipboard")
1486
+ text: z4.string().describe("Text to copy to the clipboard")
1520
1487
  },
1521
1488
  async ({ text }) => {
1522
1489
  const p = platform();
@@ -1538,8 +1505,8 @@ Please check if a camera is connected.` }],
1538
1505
  "Platform-specific: macOS (screencapture -v), Windows/Linux (ffmpeg)."
1539
1506
  ].join("\n"),
1540
1507
  {
1541
- action: import_zod4.z.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
1542
- output_path: import_zod4.z.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
1508
+ action: z4.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
1509
+ output_path: z4.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
1543
1510
  },
1544
1511
  async ({ action, output_path }) => {
1545
1512
  const p = platform();
@@ -1604,7 +1571,7 @@ Please check if a camera is connected.` }],
1604
1571
  "audio_play",
1605
1572
  "Play an audio file through the device's speakers. Supports MP3, WAV, AAC, and other common formats. Playback is synchronous \u2014 the tool returns after playback completes. Platform-specific: macOS (afplay), Windows/Linux (ffplay).",
1606
1573
  {
1607
- file_path: import_zod4.z.string().describe("Absolute path to the audio file to play")
1574
+ file_path: z4.string().describe("Absolute path to the audio file to play")
1608
1575
  },
1609
1576
  async ({ file_path }) => {
1610
1577
  const p = platform();
@@ -1621,12 +1588,12 @@ Please check if a camera is connected.` }],
1621
1588
  };
1622
1589
 
1623
1590
  // src/setup/peekaboo-installer.ts
1624
- var import_child_process4 = require("child_process");
1625
- var import_util4 = require("util");
1626
- var import_os2 = require("os");
1627
- var execFileAsync2 = (0, import_util4.promisify)(import_child_process4.execFile);
1591
+ import { execFile as execFile2 } from "child_process";
1592
+ import { promisify as promisify4 } from "util";
1593
+ import { platform as platform2 } from "os";
1594
+ var execFileAsync2 = promisify4(execFile2);
1628
1595
  async function ensurePeekaboo() {
1629
- if ((0, import_os2.platform)() !== "darwin") return false;
1596
+ if (platform2() !== "darwin") return false;
1630
1597
  try {
1631
1598
  await execFileAsync2("which", ["peekaboo"]);
1632
1599
  return true;
@@ -1646,9 +1613,9 @@ async function ensurePeekaboo() {
1646
1613
  }
1647
1614
 
1648
1615
  // src/tools/desktop.ts
1649
- var import_execa = require("execa");
1650
- var import_zod5 = require("zod");
1651
- var import_fs2 = __toESM(require("fs"));
1616
+ import { execa } from "execa";
1617
+ import { z as z5 } from "zod";
1618
+ import fs5 from "fs";
1652
1619
  var APP_BLACKLIST = /* @__PURE__ */ new Set([
1653
1620
  "Terminal",
1654
1621
  "iTerm2",
@@ -1661,7 +1628,7 @@ var MAX_CONSECUTIVE_FAILURES = 2;
1661
1628
  async function peekaboo(args) {
1662
1629
  consecutiveFailures = 0;
1663
1630
  try {
1664
- const { stdout } = await (0, import_execa.execa)("peekaboo", [...args, "--json-output"]);
1631
+ const { stdout } = await execa("peekaboo", [...args, "--json-output"]);
1665
1632
  consecutiveFailures = 0;
1666
1633
  return JSON.parse(stdout);
1667
1634
  } catch (err) {
@@ -1691,7 +1658,7 @@ var DesktopTools = class {
1691
1658
  "SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
1692
1659
  ].join("\n"),
1693
1660
  {
1694
- app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari', 'Notes', 'Google Chrome'). Omit for the frontmost app.")
1661
+ app: z5.string().optional().describe("App name to target (e.g. 'Safari', 'Notes', 'Google Chrome'). Omit for the frontmost app.")
1695
1662
  },
1696
1663
  async ({ app }) => {
1697
1664
  checkBlacklist(app);
@@ -1725,10 +1692,10 @@ var DesktopTools = class {
1725
1692
  "SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger automatic safety stop."
1726
1693
  ].join("\n"),
1727
1694
  {
1728
- on: import_zod5.z.string().describe("Element label, accessibility ID, or 'x,y' coordinates to click"),
1729
- app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari')"),
1730
- snapshot: import_zod5.z.string().optional().describe("snapshotId from desktop_see for cached interaction (240x faster)"),
1731
- doubleClick: import_zod5.z.boolean().optional().default(false).describe("Double-click instead of single click")
1695
+ on: z5.string().describe("Element label, accessibility ID, or 'x,y' coordinates to click"),
1696
+ app: z5.string().optional().describe("App name to target (e.g. 'Safari')"),
1697
+ snapshot: z5.string().optional().describe("snapshotId from desktop_see for cached interaction (240x faster)"),
1698
+ doubleClick: z5.boolean().optional().default(false).describe("Double-click instead of single click")
1732
1699
  },
1733
1700
  async ({ on, app, snapshot, doubleClick }) => {
1734
1701
  checkBlacklist(app);
@@ -1750,8 +1717,8 @@ var DesktopTools = class {
1750
1717
  "SAFETY: Terminal, iTerm, and Finder are blocked. Use desktop_see first to verify the correct element is focused."
1751
1718
  ].join("\n"),
1752
1719
  {
1753
- text: import_zod5.z.string().describe("Text to type into the focused element"),
1754
- app: import_zod5.z.string().optional().describe("App name to focus before typing")
1720
+ text: z5.string().describe("Text to type into the focused element"),
1721
+ app: z5.string().optional().describe("App name to focus before typing")
1755
1722
  },
1756
1723
  async ({ text, app }) => {
1757
1724
  checkBlacklist(app);
@@ -1773,8 +1740,8 @@ var DesktopTools = class {
1773
1740
  "SAFETY: Terminal, iTerm, and Finder are blocked."
1774
1741
  ].join("\n"),
1775
1742
  {
1776
- keys: import_zod5.z.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape', 'cmd,option,i')"),
1777
- app: import_zod5.z.string().optional().describe("App name to target")
1743
+ keys: z5.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape', 'cmd,option,i')"),
1744
+ app: z5.string().optional().describe("App name to target")
1778
1745
  },
1779
1746
  async ({ keys, app }) => {
1780
1747
  checkBlacklist(app);
@@ -1790,10 +1757,10 @@ var DesktopTools = class {
1790
1757
  "desktop_scroll",
1791
1758
  "Scroll within a macOS application or specific UI element. Use 'ticks' to control scroll distance (default: 3). Can target a specific element by label or ID with the 'on' parameter.",
1792
1759
  {
1793
- direction: import_zod5.z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
1794
- ticks: import_zod5.z.number().optional().default(3).describe("Number of scroll ticks (default: 3). Higher = more scrolling."),
1795
- on: import_zod5.z.string().optional().describe("Element label or ID to scroll within (from desktop_see). Omit to scroll the active area."),
1796
- app: import_zod5.z.string().optional().describe("App name to target")
1760
+ direction: z5.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
1761
+ ticks: z5.number().optional().default(3).describe("Number of scroll ticks (default: 3). Higher = more scrolling."),
1762
+ on: z5.string().optional().describe("Element label or ID to scroll within (from desktop_see). Omit to scroll the active area."),
1763
+ app: z5.string().optional().describe("App name to target")
1797
1764
  },
1798
1765
  async ({ direction, ticks, on, app }) => {
1799
1766
  checkBlacklist(app);
@@ -1812,7 +1779,7 @@ var DesktopTools = class {
1812
1779
  {},
1813
1780
  async () => {
1814
1781
  try {
1815
- const { stdout } = await (0, import_execa.execa)("peekaboo", ["list", "apps", "--json"]);
1782
+ const { stdout } = await execa("peekaboo", ["list", "apps", "--json"]);
1816
1783
  return {
1817
1784
  content: [{ type: "text", text: stdout }]
1818
1785
  };
@@ -1826,21 +1793,21 @@ var DesktopTools = class {
1826
1793
  "desktop_list_windows",
1827
1794
  "List all open windows on macOS, optionally filtered by app name. If no app is specified, lists windows for the frontmost application. Useful for identifying which windows are available for automation.",
1828
1795
  {
1829
- app: import_zod5.z.string().optional().describe("Filter by app name. Omit to query the frontmost app.")
1796
+ app: z5.string().optional().describe("Filter by app name. Omit to query the frontmost app.")
1830
1797
  },
1831
1798
  async ({ app }) => {
1832
1799
  checkBlacklist(app);
1833
1800
  try {
1834
1801
  let targetApp = app;
1835
1802
  if (!targetApp) {
1836
- const { stdout: stdout2 } = await (0, import_execa.execa)("osascript", [
1803
+ const { stdout: stdout2 } = await execa("osascript", [
1837
1804
  "-e",
1838
1805
  'tell application "System Events" to get name of first application process whose frontmost is true'
1839
1806
  ]);
1840
1807
  targetApp = stdout2.trim();
1841
1808
  }
1842
1809
  const args = ["list", "windows", "--app", targetApp, "--json"];
1843
- const { stdout } = await (0, import_execa.execa)("peekaboo", args);
1810
+ const { stdout } = await execa("peekaboo", args);
1844
1811
  return {
1845
1812
  content: [{ type: "text", text: stdout }]
1846
1813
  };
@@ -1859,8 +1826,8 @@ var DesktopTools = class {
1859
1826
  "Prefer desktop_see (Accessibility Tree) for understanding UI structure \u2014 use screenshot only when visual appearance matters (layouts, images, colors)."
1860
1827
  ].join("\n"),
1861
1828
  {
1862
- app: import_zod5.z.string().optional().describe("Capture a specific app's window (by name)"),
1863
- mode: import_zod5.z.enum(["screen", "window"]).optional().default("screen").describe("'screen': full display capture, 'window': specific app window only")
1829
+ app: z5.string().optional().describe("Capture a specific app's window (by name)"),
1830
+ mode: z5.enum(["screen", "window"]).optional().default("screen").describe("'screen': full display capture, 'window': specific app window only")
1864
1831
  },
1865
1832
  async ({ app, mode }) => {
1866
1833
  checkBlacklist(app);
@@ -1871,7 +1838,7 @@ var DesktopTools = class {
1871
1838
  const files = data?.files;
1872
1839
  const filePath = files?.[0]?.path;
1873
1840
  if (filePath) {
1874
- const imageBuffer = import_fs2.default.readFileSync(filePath);
1841
+ const imageBuffer = fs5.readFileSync(filePath);
1875
1842
  return {
1876
1843
  content: [{
1877
1844
  type: "image",
@@ -1894,15 +1861,15 @@ var DesktopTools = class {
1894
1861
  "The target app must be running and accessible."
1895
1862
  ].join("\n"),
1896
1863
  {
1897
- path: import_zod5.z.array(import_zod5.z.string()).describe("Menu path as array (e.g. ['File', 'Save'], ['Edit', 'Find', 'Find...'])"),
1898
- app: import_zod5.z.string().optional().describe("App name to target. Omit for the frontmost app.")
1864
+ path: z5.array(z5.string()).describe("Menu path as array (e.g. ['File', 'Save'], ['Edit', 'Find', 'Find...'])"),
1865
+ app: z5.string().optional().describe("App name to target. Omit for the frontmost app.")
1899
1866
  },
1900
1867
  async ({ path: path4, app }) => {
1901
1868
  checkBlacklist(app);
1902
1869
  const args = ["menu", "click", "--path", path4.join(" > ")];
1903
1870
  if (app) args.push("--app", app);
1904
1871
  try {
1905
- const { stdout } = await (0, import_execa.execa)("peekaboo", args);
1872
+ const { stdout } = await execa("peekaboo", args);
1906
1873
  return {
1907
1874
  content: [{ type: "text", text: stdout || "Menu click executed" }]
1908
1875
  };
@@ -1920,7 +1887,7 @@ var mcpPort = 3e3;
1920
1887
  var globalBrowserTools = null;
1921
1888
  var desktopToolsEnabled = false;
1922
1889
  function createMcpServer() {
1923
- const server = new import_mcp.McpServer({
1890
+ const server = new McpServer({
1924
1891
  name: "junis",
1925
1892
  version: "0.1.0"
1926
1893
  });
@@ -2050,7 +2017,7 @@ async function startMCPServer(port) {
2050
2017
  console.log("\u2705 Peekaboo available \u2014 desktop tools enabled");
2051
2018
  }
2052
2019
  let resolvedPort = port;
2053
- const httpServer = (0, import_http.createServer)(
2020
+ const httpServer = createServer(
2054
2021
  async (req, res) => {
2055
2022
  try {
2056
2023
  if (req.method === "OPTIONS") {
@@ -2064,7 +2031,7 @@ async function startMCPServer(port) {
2064
2031
  if (url === "/mcp") {
2065
2032
  if (req.method === "POST") {
2066
2033
  const mcpServer = createMcpServer();
2067
- const transport = new import_streamableHttp.StreamableHTTPServerTransport({
2034
+ const transport = new StreamableHTTPServerTransport({
2068
2035
  sessionIdGenerator: void 0
2069
2036
  // stateless
2070
2037
  });
@@ -2215,10 +2182,11 @@ async function handleMCPRequest(id, payload) {
2215
2182
  }
2216
2183
 
2217
2184
  // src/server/stdio.ts
2218
- var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
2219
- var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
2185
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2186
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2187
+ import { fileURLToPath } from "url";
2220
2188
  async function startStdioServer() {
2221
- const server = new import_mcp2.McpServer({ name: "junis", version: "0.1.0" });
2189
+ const server = new McpServer2({ name: "junis", version: "0.1.0" });
2222
2190
  const fsTools = new FilesystemTools();
2223
2191
  fsTools.register(server);
2224
2192
  const browserTools = new BrowserTools();
@@ -2228,60 +2196,64 @@ async function startStdioServer() {
2228
2196
  notebookTools.register(server);
2229
2197
  const deviceTools = new DeviceTools();
2230
2198
  deviceTools.register(server);
2231
- const transport = new import_stdio.StdioServerTransport();
2199
+ const transport = new StdioServerTransport();
2232
2200
  await server.connect(transport);
2233
2201
  process.on("SIGINT", async () => {
2234
2202
  await browserTools.cleanup();
2235
2203
  process.exit(0);
2236
2204
  });
2237
2205
  }
2238
- if (require.main === module) {
2206
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
2239
2207
  startStdioServer().catch(console.error);
2240
2208
  }
2241
2209
 
2210
+ // src/cli/index.ts
2211
+ import { execSync as execSync2 } from "child_process";
2212
+ import { createRequire } from "module";
2213
+
2242
2214
  // src/cli/daemon.ts
2243
- var import_fs3 = __toESM(require("fs"));
2244
- var import_path3 = __toESM(require("path"));
2245
- var import_os3 = __toESM(require("os"));
2246
- var import_child_process5 = require("child_process");
2247
- var CONFIG_DIR2 = import_path3.default.join(import_os3.default.homedir(), ".junis");
2248
- var PID_FILE = import_path3.default.join(CONFIG_DIR2, "junis.pid");
2249
- var LOG_DIR = import_path3.default.join(CONFIG_DIR2, "logs");
2250
- var LOG_FILE = import_path3.default.join(LOG_DIR, "junis.log");
2251
- var PLIST_PATH = import_path3.default.join(
2252
- import_os3.default.homedir(),
2215
+ import fs6 from "fs";
2216
+ import path3 from "path";
2217
+ import os2 from "os";
2218
+ import { execSync, spawn } from "child_process";
2219
+ var CONFIG_DIR2 = path3.join(os2.homedir(), ".junis");
2220
+ var PID_FILE = path3.join(CONFIG_DIR2, "junis.pid");
2221
+ var LOG_DIR = path3.join(CONFIG_DIR2, "logs");
2222
+ var LOG_FILE = path3.join(LOG_DIR, "junis.log");
2223
+ var PLIST_PATH = path3.join(
2224
+ os2.homedir(),
2253
2225
  "Library/LaunchAgents/ai.junis.plist"
2254
2226
  );
2255
- var SYSTEMD_PATH = import_path3.default.join(
2256
- import_os3.default.homedir(),
2227
+ var SYSTEMD_PATH = path3.join(
2228
+ os2.homedir(),
2257
2229
  ".config/systemd/user/junis.service"
2258
2230
  );
2259
2231
  function isRunning() {
2260
2232
  try {
2261
- if (!import_fs3.default.existsSync(PID_FILE)) return { running: false };
2262
- const pid = parseInt(import_fs3.default.readFileSync(PID_FILE, "utf-8").trim(), 10);
2233
+ if (!fs6.existsSync(PID_FILE)) return { running: false };
2234
+ const pid = parseInt(fs6.readFileSync(PID_FILE, "utf-8").trim(), 10);
2263
2235
  if (isNaN(pid)) return { running: false };
2264
2236
  process.kill(pid, 0);
2265
2237
  return { running: true, pid };
2266
2238
  } catch {
2267
2239
  try {
2268
- import_fs3.default.unlinkSync(PID_FILE);
2240
+ fs6.unlinkSync(PID_FILE);
2269
2241
  } catch {
2270
2242
  }
2271
2243
  return { running: false };
2272
2244
  }
2273
2245
  }
2274
2246
  function writePid(pid) {
2275
- import_fs3.default.mkdirSync(CONFIG_DIR2, { recursive: true });
2276
- import_fs3.default.writeFileSync(PID_FILE, String(pid), "utf-8");
2247
+ fs6.mkdirSync(CONFIG_DIR2, { recursive: true });
2248
+ fs6.writeFileSync(PID_FILE, String(pid), "utf-8");
2277
2249
  }
2278
2250
  function startDaemon(port) {
2279
- import_fs3.default.mkdirSync(LOG_DIR, { recursive: true });
2251
+ fs6.mkdirSync(LOG_DIR, { recursive: true });
2280
2252
  const nodePath = process.execPath;
2281
2253
  const scriptPath = process.argv[1];
2282
- const out = import_fs3.default.openSync(LOG_FILE, "a");
2283
- const err = import_fs3.default.openSync(LOG_FILE, "a");
2284
- const child = (0, import_child_process5.spawn)(nodePath, [scriptPath, "start", "--daemon", "--port", String(port)], {
2254
+ const out = fs6.openSync(LOG_FILE, "a");
2255
+ const err = fs6.openSync(LOG_FILE, "a");
2256
+ const child = spawn(nodePath, [scriptPath, "start", "--daemon", "--port", String(port)], {
2285
2257
  detached: true,
2286
2258
  stdio: ["ignore", out, err],
2287
2259
  env: { ...process.env }
@@ -2297,7 +2269,7 @@ function stopDaemon() {
2297
2269
  try {
2298
2270
  process.kill(pid, "SIGTERM");
2299
2271
  try {
2300
- import_fs3.default.unlinkSync(PID_FILE);
2272
+ fs6.unlinkSync(PID_FILE);
2301
2273
  } catch {
2302
2274
  }
2303
2275
  return true;
@@ -2331,7 +2303,7 @@ var ServiceManager = class {
2331
2303
  <key>EnvironmentVariables</key>
2332
2304
  <dict>
2333
2305
  <key>HOME</key>
2334
- <string>${import_os3.default.homedir()}</string>
2306
+ <string>${os2.homedir()}</string>
2335
2307
  <key>PATH</key>
2336
2308
  <string>${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}</string>
2337
2309
  ${process.env.JUNIS_API_URL ? `<key>JUNIS_API_URL</key>
@@ -2351,12 +2323,12 @@ var ServiceManager = class {
2351
2323
  <string>${LOG_FILE}</string>
2352
2324
  </dict>
2353
2325
  </plist>`;
2354
- import_fs3.default.mkdirSync(import_path3.default.dirname(PLIST_PATH), { recursive: true });
2355
- import_fs3.default.mkdirSync(LOG_DIR, { recursive: true });
2356
- import_fs3.default.writeFileSync(PLIST_PATH, plist, "utf-8");
2326
+ fs6.mkdirSync(path3.dirname(PLIST_PATH), { recursive: true });
2327
+ fs6.mkdirSync(LOG_DIR, { recursive: true });
2328
+ fs6.writeFileSync(PLIST_PATH, plist, "utf-8");
2357
2329
  try {
2358
- (0, import_child_process5.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
2359
- (0, import_child_process5.execSync)(`launchctl load "${PLIST_PATH}"`);
2330
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
2331
+ execSync(`launchctl load "${PLIST_PATH}"`);
2360
2332
  } catch (e) {
2361
2333
  throw new Error(`launchctl load failed: ${e.message}`);
2362
2334
  }
@@ -2369,7 +2341,7 @@ After=network.target
2369
2341
  ExecStart=${nodePath} ${scriptPath} start --daemon
2370
2342
  Restart=always
2371
2343
  RestartSec=5
2372
- Environment=HOME=${import_os3.default.homedir()}
2344
+ Environment=HOME=${os2.homedir()}
2373
2345
  Environment=PATH=${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}
2374
2346
  ${process.env.JUNIS_API_URL ? `Environment=JUNIS_API_URL=${process.env.JUNIS_API_URL}` : ""}
2375
2347
  ${process.env.JUNIS_WS_URL ? `Environment=JUNIS_WS_URL=${process.env.JUNIS_WS_URL}` : ""}
@@ -2379,14 +2351,14 @@ StandardError=append:${LOG_FILE}
2379
2351
 
2380
2352
  [Install]
2381
2353
  WantedBy=default.target`;
2382
- import_fs3.default.mkdirSync(import_path3.default.dirname(SYSTEMD_PATH), { recursive: true });
2383
- import_fs3.default.mkdirSync(LOG_DIR, { recursive: true });
2384
- import_fs3.default.writeFileSync(SYSTEMD_PATH, unit, "utf-8");
2385
- (0, import_child_process5.execSync)("systemctl --user daemon-reload");
2386
- (0, import_child_process5.execSync)("systemctl --user enable junis");
2387
- (0, import_child_process5.execSync)("systemctl --user start junis");
2354
+ fs6.mkdirSync(path3.dirname(SYSTEMD_PATH), { recursive: true });
2355
+ fs6.mkdirSync(LOG_DIR, { recursive: true });
2356
+ fs6.writeFileSync(SYSTEMD_PATH, unit, "utf-8");
2357
+ execSync("systemctl --user daemon-reload");
2358
+ execSync("systemctl --user enable junis");
2359
+ execSync("systemctl --user start junis");
2388
2360
  } else {
2389
- (0, import_child_process5.execSync)(
2361
+ execSync(
2390
2362
  `schtasks /Create /F /TN "Junis" /TR "${nodePath} ${scriptPath} start --daemon" /SC ONLOGON /RL HIGHEST`
2391
2363
  );
2392
2364
  }
@@ -2394,21 +2366,21 @@ WantedBy=default.target`;
2394
2366
  async uninstall() {
2395
2367
  if (this.platform === "mac") {
2396
2368
  try {
2397
- (0, import_child_process5.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
2398
- if (import_fs3.default.existsSync(PLIST_PATH)) import_fs3.default.unlinkSync(PLIST_PATH);
2369
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
2370
+ if (fs6.existsSync(PLIST_PATH)) fs6.unlinkSync(PLIST_PATH);
2399
2371
  } catch {
2400
2372
  }
2401
2373
  } else if (this.platform === "linux") {
2402
2374
  try {
2403
- (0, import_child_process5.execSync)("systemctl --user stop junis 2>/dev/null || true");
2404
- (0, import_child_process5.execSync)("systemctl --user disable junis 2>/dev/null || true");
2405
- if (import_fs3.default.existsSync(SYSTEMD_PATH)) import_fs3.default.unlinkSync(SYSTEMD_PATH);
2406
- (0, import_child_process5.execSync)("systemctl --user daemon-reload 2>/dev/null || true");
2375
+ execSync("systemctl --user stop junis 2>/dev/null || true");
2376
+ execSync("systemctl --user disable junis 2>/dev/null || true");
2377
+ if (fs6.existsSync(SYSTEMD_PATH)) fs6.unlinkSync(SYSTEMD_PATH);
2378
+ execSync("systemctl --user daemon-reload 2>/dev/null || true");
2407
2379
  } catch {
2408
2380
  }
2409
2381
  } else {
2410
2382
  try {
2411
- (0, import_child_process5.execSync)('schtasks /Delete /F /TN "Junis" 2>nul || true');
2383
+ execSync('schtasks /Delete /F /TN "Junis" 2>nul || true');
2412
2384
  } catch {
2413
2385
  }
2414
2386
  }
@@ -2416,13 +2388,13 @@ WantedBy=default.target`;
2416
2388
  };
2417
2389
 
2418
2390
  // src/cli/index.ts
2419
- var { version } = require_package();
2420
- import_commander.program.name("junis").description("MCP server for full device control by AI").version(version);
2391
+ var _require = createRequire(import.meta.url);
2392
+ var { version } = _require("../../package.json");
2393
+ program.name("junis").description("MCP server for full device control by AI").version(version);
2421
2394
  function getSystemInfo() {
2422
2395
  const platform3 = process.platform;
2423
2396
  if (platform3 === "darwin") {
2424
2397
  try {
2425
- const { execSync: execSync2 } = require("child_process");
2426
2398
  const sw = execSync2("sw_vers -productVersion", { encoding: "utf8" }).trim();
2427
2399
  const hw = execSync2("sysctl -n machdep.cpu.brand_string", { encoding: "utf8" }).trim();
2428
2400
  return `macOS ${sw} (${hw})`;
@@ -2466,6 +2438,27 @@ function printStep1(port) {
2466
2438
  console.log(` \u25C9 Local MCP endpoint ........... http://localhost:${port}/mcp`);
2467
2439
  console.log("");
2468
2440
  }
2441
+ async function checkEnsureTeam(config, label) {
2442
+ try {
2443
+ const result = await ensureTeam(config.device_key, config.token);
2444
+ if (result.status === "ready") {
2445
+ return config;
2446
+ }
2447
+ if (result.status === "created") {
2448
+ console.log(`[${label}] Team restored automatically`);
2449
+ if (result.token) {
2450
+ config.token = result.token;
2451
+ saveConfig(config);
2452
+ }
2453
+ return config;
2454
+ }
2455
+ console.log(`[${label}] ${result.status} \u2014 re-authentication required`);
2456
+ clearConfig();
2457
+ return null;
2458
+ } catch {
2459
+ return config;
2460
+ }
2461
+ }
2469
2462
  async function runForeground(config, port) {
2470
2463
  const deviceName = config.device_name;
2471
2464
  const platformName = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
@@ -2553,7 +2546,7 @@ async function runBackground(config, port) {
2553
2546
  console.log("");
2554
2547
  process.exit(0);
2555
2548
  }
2556
- import_commander.program.command("start", { isDefault: true }).description("Start Junis agent connection").option("--local", "Run local MCP server only (no cloud connection)").option("--port <number>", "Port number", "3000").option("--reset", "Clear existing credentials and re-login").option("--daemon", "Run in daemon mode (internal, used by launchd/systemd)").option("--foreground", "Run in foreground mode (no prompt)").option("--stdio", "Run as stdio transport (for MCP client integration)").action(async (options) => {
2549
+ program.command("start", { isDefault: true }).description("Start Junis agent connection").option("--local", "Run local MCP server only (no cloud connection)").option("--port <number>", "Port number", "3000").option("--reset", "Clear existing credentials and re-login").option("--daemon", "Run in daemon mode (internal, used by launchd/systemd)").option("--foreground", "Run in foreground mode (no prompt)").option("--stdio", "Run as stdio transport (for MCP client integration)").action(async (options) => {
2557
2550
  const port = parseInt(options.port, 10);
2558
2551
  if (options.stdio) {
2559
2552
  await startStdioServer();
@@ -2620,6 +2613,39 @@ import_commander.program.command("start", { isDefault: true }).description("Star
2620
2613
  console.log("");
2621
2614
  }
2622
2615
  } else {
2616
+ const checked = await checkEnsureTeam(config2, "junis");
2617
+ if (!checked) {
2618
+ let waitingPrinted = false;
2619
+ const authResult = await authenticate(
2620
+ deviceName2,
2621
+ platformName2,
2622
+ (uri) => {
2623
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2624
+ console.log(" STEP 2 \xB7 Connect to Junis Cloud");
2625
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2626
+ console.log(" Opening browser...");
2627
+ console.log(` \u2192 ${uri}`);
2628
+ process.stdout.write(" Waiting for login \xB7");
2629
+ },
2630
+ () => {
2631
+ if (!waitingPrinted) {
2632
+ waitingPrinted = true;
2633
+ } else {
2634
+ process.stdout.write("\xB7");
2635
+ }
2636
+ }
2637
+ );
2638
+ console.log("");
2639
+ console.log(` \u2705 Authenticated as ${authResult.email ?? "your account"}`);
2640
+ console.log("");
2641
+ config2 = {
2642
+ device_key: authResult.device_key,
2643
+ token: authResult.token,
2644
+ device_name: deviceName2,
2645
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2646
+ };
2647
+ saveConfig(config2);
2648
+ }
2623
2649
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2624
2650
  console.log(" STEP 3 \xB7 Register Device");
2625
2651
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -2644,6 +2670,12 @@ import_commander.program.command("start", { isDefault: true }).description("Star
2644
2670
  console.error("\u274C No credentials found. Run npx junis first.");
2645
2671
  process.exit(1);
2646
2672
  }
2673
+ const checked = await checkEnsureTeam(config2, "junis daemon");
2674
+ if (!checked) {
2675
+ console.error("\u274C Device or team invalid. Run npx junis --reset to re-authenticate.");
2676
+ process.exit(1);
2677
+ }
2678
+ config2 = checked;
2647
2679
  const deviceName2 = config2.device_name;
2648
2680
  const platformName2 = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
2649
2681
  const actualPort = await startMCPServer(port);
@@ -2693,7 +2725,7 @@ import_commander.program.command("start", { isDefault: true }).description("Star
2693
2725
  console.log(" To stop: npx junis stop");
2694
2726
  return;
2695
2727
  }
2696
- const mode = await (0, import_prompts.select)({
2728
+ const mode = await select({
2697
2729
  message: "Select run mode:",
2698
2730
  choices: [
2699
2731
  {
@@ -2767,6 +2799,39 @@ import_commander.program.command("start", { isDefault: true }).description("Star
2767
2799
  console.log("");
2768
2800
  }
2769
2801
  } else {
2802
+ const checked = await checkEnsureTeam(config, "junis");
2803
+ if (!checked) {
2804
+ let waitingPrinted = false;
2805
+ const authResult = await authenticate(
2806
+ deviceName,
2807
+ platformName,
2808
+ (uri) => {
2809
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2810
+ console.log(" STEP 2 \xB7 Connect to Junis Cloud");
2811
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2812
+ console.log(" Opening browser...");
2813
+ console.log(` \u2192 ${uri}`);
2814
+ process.stdout.write(" Waiting for login \xB7");
2815
+ },
2816
+ () => {
2817
+ if (!waitingPrinted) {
2818
+ waitingPrinted = true;
2819
+ } else {
2820
+ process.stdout.write("\xB7");
2821
+ }
2822
+ }
2823
+ );
2824
+ console.log("");
2825
+ console.log(` \u2705 Authenticated as ${authResult.email ?? "your account"}`);
2826
+ console.log("");
2827
+ config = {
2828
+ device_key: authResult.device_key,
2829
+ token: authResult.token,
2830
+ device_name: deviceName,
2831
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2832
+ };
2833
+ saveConfig(config);
2834
+ }
2770
2835
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2771
2836
  console.log(" STEP 3 \xB7 Register Device");
2772
2837
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -2782,7 +2847,7 @@ import_commander.program.command("start", { isDefault: true }).description("Star
2782
2847
  await runBackground(config, port);
2783
2848
  }
2784
2849
  });
2785
- import_commander.program.command("stop").description("Stop background service and disable auto-start").action(async () => {
2850
+ program.command("stop").description("Stop background service and disable auto-start").action(async () => {
2786
2851
  const stopped = stopDaemon();
2787
2852
  const svc = new ServiceManager();
2788
2853
  let serviceUninstalled = false;
@@ -2798,11 +2863,11 @@ import_commander.program.command("stop").description("Stop background service an
2798
2863
  console.log("\u2139\uFE0F No running Junis process found.");
2799
2864
  }
2800
2865
  });
2801
- import_commander.program.command("logout").description("Clear authentication credentials").action(() => {
2866
+ program.command("logout").description("Clear authentication credentials").action(() => {
2802
2867
  clearConfig();
2803
2868
  console.log("\u2705 Authentication credentials cleared");
2804
2869
  });
2805
- import_commander.program.command("status").description("Check current status").action(() => {
2870
+ program.command("status").description("Check current status").action(() => {
2806
2871
  const config = loadConfig();
2807
2872
  const { running, pid } = isRunning();
2808
2873
  if (!config) {
@@ -2817,7 +2882,7 @@ import_commander.program.command("status").description("Check current status").a
2817
2882
  console.log(" To start: npx junis");
2818
2883
  }
2819
2884
  });
2820
- import_commander.program.addHelpText("after", `
2885
+ program.addHelpText("after", `
2821
2886
  Examples:
2822
2887
  npx junis Interactive mode (foreground/background)
2823
2888
  npx junis --local Local MCP server only (no cloud)
@@ -2835,4 +2900,4 @@ MCP Client Config (Claude Code, Claude Desktop, Codex, etc.):
2835
2900
  }
2836
2901
  }
2837
2902
  `);
2838
- import_commander.program.parse();
2903
+ program.parse();