@appchy/jarvis 0.1.13 → 0.1.15

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/bin.js CHANGED
@@ -1,15 +1,20 @@
1
+ // src/bin.ts
2
+ import dotenv from "dotenv";
3
+ import fs9 from "fs";
4
+ import path9 from "path";
5
+
1
6
  // src/cli.ts
2
7
  import { Command } from "commander";
3
- import { spawn as spawn2, execSync } from "child_process";
4
- import fs7 from "fs";
5
- import path7 from "path";
6
- import os4 from "os";
8
+ import { spawn as spawn3, execSync as execSync2 } from "child_process";
9
+ import fs8 from "fs";
10
+ import path8 from "path";
11
+ import os5 from "os";
7
12
 
8
13
  // src/config.ts
9
14
  import fs from "fs";
10
15
  import path from "path";
11
16
  import os from "os";
12
- var CONFIG_DIR = path.join(os.homedir(), ".jarvis");
17
+ var CONFIG_DIR = process.env.JARVIS_CONFIG_DIR ?? path.join(os.homedir(), ".jarvis");
13
18
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
14
19
  function loadConfig() {
15
20
  try {
@@ -2556,7 +2561,7 @@ function createUpstreamClient(config) {
2556
2561
  if (!config.refreshToken) return null;
2557
2562
  return config.apiUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:") + "/refresh";
2558
2563
  }
2559
- function getTokenExpiry(token) {
2564
+ function getTokenExpiry2(token) {
2560
2565
  try {
2561
2566
  const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
2562
2567
  return payload.exp ?? null;
@@ -2567,7 +2572,7 @@ function createUpstreamClient(config) {
2567
2572
  function scheduleRefresh() {
2568
2573
  if (refreshTimer) clearTimeout(refreshTimer);
2569
2574
  if (!config.refreshToken) return;
2570
- const exp = getTokenExpiry(currentToken);
2575
+ const exp = getTokenExpiry2(currentToken);
2571
2576
  if (!exp) return;
2572
2577
  const msUntilExpiry = exp * 1e3 - Date.now();
2573
2578
  const refreshIn = Math.max(msUntilExpiry - REFRESH_BUFFER_MS, 0);
@@ -2627,6 +2632,7 @@ function createUpstreamClient(config) {
2627
2632
  }
2628
2633
  });
2629
2634
  ws.on("close", (code) => {
2635
+ console.error("[Upstream] Connection closed, code:", code);
2630
2636
  if (closed) return;
2631
2637
  if (code === 4001 || code === 4003) {
2632
2638
  if (config.refreshToken) {
@@ -2678,6 +2684,7 @@ function createUpstreamClient(config) {
2678
2684
  reconnectTimer = setTimeout(connect, 3e3);
2679
2685
  });
2680
2686
  ws.on("error", (err) => {
2687
+ console.error("[Upstream] Connection error:", err.message);
2681
2688
  logger.sys.error("[Upstream] Connection error", {
2682
2689
  error: err.message
2683
2690
  });
@@ -4796,26 +4803,453 @@ async function waitForAgent(port, maxAttempts = 30) {
4796
4803
  }
4797
4804
  }
4798
4805
 
4806
+ // src/service.ts
4807
+ import { execSync, spawn as spawn2 } from "child_process";
4808
+ import fs7 from "fs";
4809
+ import path7 from "path";
4810
+ import os4 from "os";
4811
+ function createServiceManager() {
4812
+ if (process.platform === "darwin") return new MacOSService();
4813
+ if (process.platform === "win32") return new WindowsService();
4814
+ if (process.platform === "linux") return new LinuxService();
4815
+ return new FallbackService();
4816
+ }
4817
+ var PLIST_LABEL = "com.appchy.jarvis";
4818
+ var PLIST_DIR = path7.join(os4.homedir(), "Library", "LaunchAgents");
4819
+ var PLIST_PATH = path7.join(PLIST_DIR, `${PLIST_LABEL}.plist`);
4820
+ function escapeXml(s) {
4821
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4822
+ }
4823
+ var MacOSService = class {
4824
+ install(opts) {
4825
+ const args = [
4826
+ opts.nodePath,
4827
+ opts.entryPath,
4828
+ "start",
4829
+ "--foreground",
4830
+ "--port",
4831
+ String(opts.port),
4832
+ "--workspace",
4833
+ opts.workspacePath
4834
+ ];
4835
+ if (!opts.upstream) {
4836
+ args.push("--no-upstream");
4837
+ }
4838
+ const logFile = path7.join(os4.homedir(), ".jarvis", "agent.log");
4839
+ const envPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
4840
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
4841
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4842
+ <plist version="1.0">
4843
+ <dict>
4844
+ <key>Label</key>
4845
+ <string>${PLIST_LABEL}</string>
4846
+
4847
+ <key>ProgramArguments</key>
4848
+ <array>
4849
+ ${args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n")}
4850
+ </array>
4851
+
4852
+ <key>WorkingDirectory</key>
4853
+ <string>${escapeXml(opts.workspacePath)}</string>
4854
+
4855
+ <key>EnvironmentVariables</key>
4856
+ <dict>
4857
+ <key>PATH</key>
4858
+ <string>${escapeXml(envPath)}</string>
4859
+ <key>HOME</key>
4860
+ <string>${escapeXml(os4.homedir())}</string>
4861
+ <key>NODE_NO_WARNINGS</key>
4862
+ <string>1</string>
4863
+ </dict>
4864
+
4865
+ <key>RunAtLoad</key>
4866
+ <true/>
4867
+
4868
+ <key>KeepAlive</key>
4869
+ <true/>
4870
+
4871
+ <key>ProcessType</key>
4872
+ <string>Interactive</string>
4873
+
4874
+ <key>ThrottleInterval</key>
4875
+ <integer>5</integer>
4876
+
4877
+ <key>StandardOutPath</key>
4878
+ <string>${escapeXml(logFile)}</string>
4879
+
4880
+ <key>StandardErrorPath</key>
4881
+ <string>${escapeXml(logFile)}</string>
4882
+ </dict>
4883
+ </plist>
4884
+ `;
4885
+ fs7.mkdirSync(PLIST_DIR, { recursive: true });
4886
+ fs7.mkdirSync(path7.dirname(logFile), { recursive: true });
4887
+ if (this.isInstalled()) {
4888
+ try {
4889
+ execSync(`launchctl unload -w "${PLIST_PATH}"`, { stdio: "ignore" });
4890
+ } catch {
4891
+ }
4892
+ }
4893
+ fs7.writeFileSync(PLIST_PATH, plist);
4894
+ execSync(`launchctl load -w "${PLIST_PATH}"`);
4895
+ }
4896
+ uninstall() {
4897
+ try {
4898
+ execSync(`launchctl unload -w "${PLIST_PATH}"`, { stdio: "ignore" });
4899
+ } catch {
4900
+ }
4901
+ try {
4902
+ fs7.unlinkSync(PLIST_PATH);
4903
+ } catch {
4904
+ }
4905
+ }
4906
+ isInstalled() {
4907
+ return fs7.existsSync(PLIST_PATH);
4908
+ }
4909
+ start() {
4910
+ try {
4911
+ execSync(`launchctl load -w "${PLIST_PATH}"`);
4912
+ } catch {
4913
+ execSync(`launchctl start ${PLIST_LABEL}`);
4914
+ }
4915
+ }
4916
+ stop() {
4917
+ try {
4918
+ execSync(`launchctl unload -w "${PLIST_PATH}"`, { stdio: "ignore" });
4919
+ } catch {
4920
+ }
4921
+ }
4922
+ status() {
4923
+ const installed = this.isInstalled();
4924
+ if (!installed) return { installed: false, running: false };
4925
+ try {
4926
+ const output = execSync(`launchctl list ${PLIST_LABEL}`, {
4927
+ encoding: "utf-8",
4928
+ stdio: ["pipe", "pipe", "ignore"]
4929
+ });
4930
+ const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
4931
+ const pid = pidMatch ? parseInt(pidMatch[1], 10) : void 0;
4932
+ return { installed: true, running: pid !== void 0, pid, os: "macOS" };
4933
+ } catch {
4934
+ return { installed: true, running: false, os: "macOS" };
4935
+ }
4936
+ }
4937
+ };
4938
+ var TASK_NAME = "JarvisAgent";
4939
+ var WindowsService = class {
4940
+ install(opts) {
4941
+ const args = [
4942
+ opts.entryPath,
4943
+ "start",
4944
+ "--foreground",
4945
+ "--port",
4946
+ String(opts.port),
4947
+ "--workspace",
4948
+ `"${opts.workspacePath}"`
4949
+ ];
4950
+ if (!opts.upstream) {
4951
+ args.push("--no-upstream");
4952
+ }
4953
+ const xml = `<?xml version="1.0" encoding="UTF-16"?>
4954
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
4955
+ <RegistrationInfo>
4956
+ <Description>Jarvis AI Agent \u2014 always-on background service</Description>
4957
+ </RegistrationInfo>
4958
+ <Triggers>
4959
+ <LogonTrigger>
4960
+ <Enabled>true</Enabled>
4961
+ </LogonTrigger>
4962
+ </Triggers>
4963
+ <Principals>
4964
+ <Principal id="Author">
4965
+ <LogonType>InteractiveToken</LogonType>
4966
+ <RunLevel>LeastPrivilege</RunLevel>
4967
+ </Principal>
4968
+ </Principals>
4969
+ <Settings>
4970
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
4971
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
4972
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
4973
+ <AllowHardTerminate>true</AllowHardTerminate>
4974
+ <StartWhenAvailable>true</StartWhenAvailable>
4975
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
4976
+ <AllowStartOnDemand>true</AllowStartOnDemand>
4977
+ <Enabled>true</Enabled>
4978
+ <Hidden>false</Hidden>
4979
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
4980
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
4981
+ <Priority>7</Priority>
4982
+ <RestartOnFailure>
4983
+ <Interval>PT1M</Interval>
4984
+ <Count>999</Count>
4985
+ </RestartOnFailure>
4986
+ </Settings>
4987
+ <Actions Context="Author">
4988
+ <Exec>
4989
+ <Command>${escapeXml(opts.nodePath)}</Command>
4990
+ <Arguments>${escapeXml(args.join(" "))}</Arguments>
4991
+ <WorkingDirectory>${escapeXml(opts.workspacePath)}</WorkingDirectory>
4992
+ </Exec>
4993
+ </Actions>
4994
+ </Task>
4995
+ `;
4996
+ const tmpDir = os4.tmpdir();
4997
+ const tmpFile = path7.join(tmpDir, `jarvis-task-${Date.now()}.xml`);
4998
+ fs7.writeFileSync(tmpFile, xml, { encoding: "utf-16le" });
4999
+ try {
5000
+ execSync(`schtasks /Create /TN "${TASK_NAME}" /XML "${tmpFile}" /F`, {
5001
+ stdio: "ignore"
5002
+ });
5003
+ } finally {
5004
+ try {
5005
+ fs7.unlinkSync(tmpFile);
5006
+ } catch {
5007
+ }
5008
+ }
5009
+ }
5010
+ uninstall() {
5011
+ try {
5012
+ execSync(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
5013
+ } catch {
5014
+ }
5015
+ }
5016
+ isInstalled() {
5017
+ try {
5018
+ execSync(`schtasks /Query /TN "${TASK_NAME}"`, { stdio: "ignore" });
5019
+ return true;
5020
+ } catch {
5021
+ return false;
5022
+ }
5023
+ }
5024
+ start() {
5025
+ execSync(`schtasks /Run /TN "${TASK_NAME}"`, { stdio: "ignore" });
5026
+ }
5027
+ stop() {
5028
+ execSync(`schtasks /End /TN "${TASK_NAME}"`, { stdio: "ignore" });
5029
+ }
5030
+ status() {
5031
+ if (!this.isInstalled()) return { installed: false, running: false };
5032
+ try {
5033
+ const output = execSync(`schtasks /Query /TN "${TASK_NAME}" /FO CSV /NH`, {
5034
+ encoding: "utf-8"
5035
+ });
5036
+ const running = output.includes("Running");
5037
+ return { installed: true, running, os: "windows" };
5038
+ } catch {
5039
+ return { installed: true, running: false, os: "windows" };
5040
+ }
5041
+ }
5042
+ };
5043
+ var SYSTEMD_DIR = path7.join(os4.homedir(), ".config", "systemd", "user");
5044
+ var UNIT_NAME = "jarvis.service";
5045
+ var UNIT_PATH = path7.join(SYSTEMD_DIR, UNIT_NAME);
5046
+ var LinuxService = class {
5047
+ install(opts) {
5048
+ const args = [
5049
+ opts.entryPath,
5050
+ "start",
5051
+ "--foreground",
5052
+ "--port",
5053
+ String(opts.port),
5054
+ "--workspace",
5055
+ opts.workspacePath
5056
+ ];
5057
+ if (!opts.upstream) {
5058
+ args.push("--no-upstream");
5059
+ }
5060
+ const logFile = path7.join(os4.homedir(), ".jarvis", "agent.log");
5061
+ const envPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
5062
+ const unit = `[Unit]
5063
+ Description=Jarvis AI Agent
5064
+ After=network.target
5065
+
5066
+ [Service]
5067
+ Type=simple
5068
+ ExecStart=${opts.nodePath} ${args.join(" ")}
5069
+ WorkingDirectory=${opts.workspacePath}
5070
+ Environment=PATH=${envPath}
5071
+ Environment=HOME=${os4.homedir()}
5072
+ Environment=NODE_NO_WARNINGS=1
5073
+ Restart=always
5074
+ RestartSec=5
5075
+ StandardOutput=append:${logFile}
5076
+ StandardError=append:${logFile}
5077
+
5078
+ [Install]
5079
+ WantedBy=default.target
5080
+ `;
5081
+ fs7.mkdirSync(SYSTEMD_DIR, { recursive: true });
5082
+ fs7.mkdirSync(path7.dirname(logFile), { recursive: true });
5083
+ if (this.isInstalled()) {
5084
+ try {
5085
+ execSync("systemctl --user stop jarvis.service", { stdio: "ignore" });
5086
+ } catch {
5087
+ }
5088
+ }
5089
+ fs7.writeFileSync(UNIT_PATH, unit);
5090
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
5091
+ execSync("systemctl --user enable jarvis.service", { stdio: "ignore" });
5092
+ execSync("systemctl --user start jarvis.service", { stdio: "ignore" });
5093
+ try {
5094
+ execSync(`loginctl enable-linger ${os4.userInfo().username}`, { stdio: "ignore" });
5095
+ } catch {
5096
+ }
5097
+ }
5098
+ uninstall() {
5099
+ try {
5100
+ execSync("systemctl --user stop jarvis.service", { stdio: "ignore" });
5101
+ } catch {
5102
+ }
5103
+ try {
5104
+ execSync("systemctl --user disable jarvis.service", { stdio: "ignore" });
5105
+ } catch {
5106
+ }
5107
+ try {
5108
+ fs7.unlinkSync(UNIT_PATH);
5109
+ } catch {
5110
+ }
5111
+ try {
5112
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
5113
+ } catch {
5114
+ }
5115
+ }
5116
+ isInstalled() {
5117
+ return fs7.existsSync(UNIT_PATH);
5118
+ }
5119
+ start() {
5120
+ execSync("systemctl --user start jarvis.service");
5121
+ }
5122
+ stop() {
5123
+ execSync("systemctl --user stop jarvis.service", { stdio: "ignore" });
5124
+ }
5125
+ status() {
5126
+ if (!this.isInstalled()) return { installed: false, running: false };
5127
+ try {
5128
+ const output = execSync("systemctl --user show jarvis.service --property=ActiveState,MainPID", {
5129
+ encoding: "utf-8"
5130
+ });
5131
+ const active = output.includes("ActiveState=active");
5132
+ const pidMatch = output.match(/MainPID=(\d+)/);
5133
+ const pid = pidMatch ? parseInt(pidMatch[1], 10) : void 0;
5134
+ return {
5135
+ installed: true,
5136
+ running: active && pid !== void 0 && pid > 0,
5137
+ pid: pid && pid > 0 ? pid : void 0,
5138
+ os: "linux"
5139
+ };
5140
+ } catch {
5141
+ return { installed: true, running: false, os: "linux" };
5142
+ }
5143
+ }
5144
+ };
5145
+ var FallbackService = class {
5146
+ constructor() {
5147
+ this.pidFile = path7.join(os4.homedir(), ".jarvis", "agent.pid");
5148
+ this.logFile = path7.join(os4.homedir(), ".jarvis", "agent.log");
5149
+ this.markerFile = path7.join(os4.homedir(), ".jarvis", "service-installed");
5150
+ }
5151
+ install(opts) {
5152
+ const jarvisDir = path7.join(os4.homedir(), ".jarvis");
5153
+ fs7.mkdirSync(jarvisDir, { recursive: true });
5154
+ this.stop();
5155
+ const args = [
5156
+ opts.entryPath,
5157
+ "start",
5158
+ "--foreground",
5159
+ "--port",
5160
+ String(opts.port),
5161
+ "--workspace",
5162
+ opts.workspacePath
5163
+ ];
5164
+ if (!opts.upstream) {
5165
+ args.push("--no-upstream");
5166
+ }
5167
+ const logFd = fs7.openSync(this.logFile, "a");
5168
+ const child = spawn2(opts.nodePath, args, {
5169
+ detached: true,
5170
+ stdio: ["ignore", logFd, logFd],
5171
+ cwd: opts.workspacePath,
5172
+ env: { ...process.env, NODE_NO_WARNINGS: "1" }
5173
+ });
5174
+ child.unref();
5175
+ fs7.closeSync(logFd);
5176
+ fs7.writeFileSync(this.pidFile, String(child.pid));
5177
+ fs7.writeFileSync(this.markerFile, JSON.stringify({
5178
+ nodePath: opts.nodePath,
5179
+ entryPath: opts.entryPath,
5180
+ port: opts.port,
5181
+ workspacePath: opts.workspacePath,
5182
+ upstream: opts.upstream
5183
+ }));
5184
+ console.log(
5185
+ `\x1B[33mNote:\x1B[0m OS-level service not available on ${process.platform}.`
5186
+ );
5187
+ console.log(" Agent is running as a background process (PID: " + child.pid + ").");
5188
+ console.log(" It will NOT auto-start on reboot or recover from crashes.");
5189
+ }
5190
+ uninstall() {
5191
+ this.stop();
5192
+ try {
5193
+ fs7.unlinkSync(this.markerFile);
5194
+ } catch {
5195
+ }
5196
+ }
5197
+ isInstalled() {
5198
+ return fs7.existsSync(this.markerFile);
5199
+ }
5200
+ start() {
5201
+ if (!this.isInstalled()) {
5202
+ throw new Error("Service not installed. Run 'jarvis install' first.");
5203
+ }
5204
+ const saved = JSON.parse(fs7.readFileSync(this.markerFile, "utf-8"));
5205
+ this.install(saved);
5206
+ }
5207
+ stop() {
5208
+ try {
5209
+ const pid = parseInt(fs7.readFileSync(this.pidFile, "utf-8").trim(), 10);
5210
+ if (!isNaN(pid)) {
5211
+ process.kill(pid, "SIGTERM");
5212
+ }
5213
+ } catch {
5214
+ }
5215
+ try {
5216
+ fs7.unlinkSync(this.pidFile);
5217
+ } catch {
5218
+ }
5219
+ }
5220
+ status() {
5221
+ if (!this.isInstalled()) return { installed: false, running: false };
5222
+ try {
5223
+ const pid = parseInt(fs7.readFileSync(this.pidFile, "utf-8").trim(), 10);
5224
+ if (isNaN(pid)) return { installed: true, running: false };
5225
+ process.kill(pid, 0);
5226
+ return { installed: true, running: true, pid };
5227
+ } catch {
5228
+ return { installed: true, running: false };
5229
+ }
5230
+ }
5231
+ };
5232
+
4799
5233
  // src/cli.ts
4800
5234
  import { createRequire } from "module";
4801
5235
  var _require = createRequire(import.meta.url);
4802
5236
  var PKG_VERSION = _require("../package.json").version ?? "dev";
4803
- var LOG_DIR = path7.join(os4.homedir(), ".jarvis");
4804
- var LOG_FILE = path7.join(LOG_DIR, "agent.log");
4805
- var PID_FILE = path7.join(LOG_DIR, "agent.pid");
5237
+ var LOG_DIR = path8.join(os5.homedir(), ".jarvis");
5238
+ var LOG_FILE = path8.join(LOG_DIR, "agent.log");
5239
+ var PID_FILE = path8.join(LOG_DIR, "agent.pid");
4806
5240
  function savePid(pid) {
4807
- fs7.mkdirSync(LOG_DIR, { recursive: true });
4808
- fs7.writeFileSync(PID_FILE, String(pid));
5241
+ fs8.mkdirSync(LOG_DIR, { recursive: true });
5242
+ fs8.writeFileSync(PID_FILE, String(pid));
4809
5243
  }
4810
5244
  function readPid() {
4811
5245
  try {
4812
- const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
5246
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
4813
5247
  if (isNaN(pid)) return null;
4814
5248
  try {
4815
5249
  process.kill(pid, 0);
4816
5250
  return pid;
4817
5251
  } catch {
4818
- fs7.unlinkSync(PID_FILE);
5252
+ fs8.unlinkSync(PID_FILE);
4819
5253
  return null;
4820
5254
  }
4821
5255
  } catch {
@@ -4824,7 +5258,7 @@ function readPid() {
4824
5258
  }
4825
5259
  function clearPid() {
4826
5260
  try {
4827
- fs7.unlinkSync(PID_FILE);
5261
+ fs8.unlinkSync(PID_FILE);
4828
5262
  } catch {
4829
5263
  }
4830
5264
  }
@@ -4874,7 +5308,7 @@ async function findFreePort() {
4874
5308
  async function browserAuth(appUrl) {
4875
5309
  const config = loadConfig();
4876
5310
  const callbackPort = await findFreePort();
4877
- const loginUrl = `${appUrl}/login?callback=cli&port=${callbackPort}`;
5311
+ const loginUrl = `${appUrl}/api/auth/cli?port=${callbackPort}`;
4878
5312
  console.log();
4879
5313
  console.log("Opening browser for sign in...");
4880
5314
  console.log(` If it doesn't open, visit: ${loginUrl}`);
@@ -4904,34 +5338,61 @@ async function browserAuth(appUrl) {
4904
5338
  return false;
4905
5339
  }
4906
5340
  }
4907
- var APP_URL = process.env.APP_URL ?? "https://jarvis.appchy.com" ?? "https://jarvis.appchy.com";
5341
+ function getAppUrl() {
5342
+ return process.env.APP_URL ?? "https://jarvis.appchy.com" ?? "https://jarvis.appchy.com";
5343
+ }
5344
+ function getTokenExpiry(token) {
5345
+ try {
5346
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
5347
+ return payload.exp ?? null;
5348
+ } catch {
5349
+ return null;
5350
+ }
5351
+ }
5352
+ function isTokenExpired(token, bufferMs = 6e4) {
5353
+ const exp = getTokenExpiry(token);
5354
+ if (!exp) return true;
5355
+ return exp * 1e3 - Date.now() < bufferMs;
5356
+ }
5357
+ async function tryRefreshToken(config) {
5358
+ if (!config.apiUrl || !config.refreshToken) return null;
5359
+ const refreshUrl = config.apiUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:") + "/refresh";
5360
+ try {
5361
+ const res = await fetch(refreshUrl, {
5362
+ method: "POST",
5363
+ headers: { "Content-Type": "application/json" },
5364
+ body: JSON.stringify({ refreshToken: config.refreshToken })
5365
+ });
5366
+ if (!res.ok) return null;
5367
+ const { token } = await res.json();
5368
+ return token;
5369
+ } catch {
5370
+ return null;
5371
+ }
5372
+ }
4908
5373
  async function ensureSetup() {
4909
5374
  const config = loadConfig();
4910
- if (config?.token || config?.anthropicApiKey || config?.useSubscription) {
5375
+ if (!config?.token && !config?.anthropicApiKey && !config?.useSubscription) {
5376
+ return browserAuth(getAppUrl());
5377
+ }
5378
+ if (config?.anthropicApiKey || config?.useSubscription) {
4911
5379
  return true;
4912
5380
  }
4913
- const ok = await browserAuth(APP_URL);
4914
- if (!ok) return false;
4915
- const pid = readPid();
4916
- if (pid) {
4917
- try {
4918
- process.kill(pid, "SIGTERM");
4919
- } catch {
5381
+ if (config?.token && isTokenExpired(config.token)) {
5382
+ console.log("Token expired, attempting refresh...");
5383
+ const newToken = await tryRefreshToken(config);
5384
+ if (newToken) {
5385
+ saveConfig({ ...config, token: newToken, connectedAt: (/* @__PURE__ */ new Date()).toISOString() });
5386
+ console.log("Token refreshed successfully.");
5387
+ return true;
4920
5388
  }
4921
- clearPid();
4922
- await new Promise((r) => setTimeout(r, 1e3));
5389
+ console.log("Token refresh failed. Re-authenticating...");
5390
+ return browserAuth(getAppUrl());
4923
5391
  }
4924
5392
  return true;
4925
5393
  }
4926
5394
  function createCli() {
4927
5395
  const program = new Command().name("jarvis").description("Jarvis local agent \u2014 runs Claude Code on your machine").version(PKG_VERSION);
4928
- program.argument("[message]", "Optional initial message to send").option("--model <id>", "Model (e.g., claude-opus-4-20250514)").option("--mode <mode>", "Permission mode: supervised|auto|plan|yolo").option("--thinking <config>", "Thinking: adaptive|enabled|disabled").option("--thinking-budget <n>", "Token budget when thinking=enabled").option("--max-turns <n>", "Max turns per task").option("-p, --port <port>", "Agent server port", "7862").option("-w, --workspace <path>", "Workspace root path").option("--no-server", "Connect to existing server, don't auto-start").action(async (message, opts) => {
4929
- if (message && program.commands.some((c) => c.name() === message)) {
4930
- return;
4931
- }
4932
- await ensureSetup();
4933
- await launchChat(opts);
4934
- });
4935
5396
  program.command("connect <token>").description("Connect to Jarvis cloud using a token from the web UI").option("-w, --workspace <path>", "Workspace root path for repo operations").action((token, opts) => {
4936
5397
  try {
4937
5398
  const parsed = parseConnectToken(token);
@@ -4958,86 +5419,133 @@ function createCli() {
4958
5419
  process.exit(1);
4959
5420
  }
4960
5421
  });
4961
- program.command("start").description("Start the local agent (runs as background daemon)").option("-p, --port <port>", "Local WS server port", "7862").option("-w, --workspace <path>", "Workspace root path").option("--api-key <key>", "Anthropic API key (for local-only use)").option("--no-upstream", "Don't connect to cloud (local-only mode)").option("--foreground", "Run in foreground (don't daemonize)").action(async (opts) => {
4962
- await ensureSetup();
4963
- const config = loadConfig();
4964
- const port = parseInt(opts.port, 10);
4965
- const alreadyRunning = await isPortInUse(port);
4966
- if (alreadyRunning) {
4967
- console.log(`Jarvis agent is already running on ws://127.0.0.1:${port}`);
4968
- console.log(`Use 'jarvis restart' to restart, or 'jarvis logs' to view logs.`);
4969
- return;
4970
- }
4971
- const workspacePath = opts.workspace ?? config?.workspacePath ?? process.cwd();
4972
- const userId = config?.userId ?? process.env.JARVIS_USER_ID ?? "local";
4973
- const explicitApiKey = opts.apiKey ?? config?.anthropicApiKey;
4974
- const useSubscription = !explicitApiKey;
4975
- const anthropicApiKey = explicitApiKey;
4976
- if (opts.foreground) {
4977
- await startAgent({
4978
- port,
4979
- workspacePath,
4980
- anthropicApiKey,
4981
- useSubscription,
4982
- userId,
4983
- upstream: (() => {
4984
- const apiUrl = process.env.JARVIS_UPSTREAM_URL ?? config?.apiUrl;
4985
- const token = process.env.JARVIS_UPSTREAM_TOKEN ?? config?.token;
4986
- if (opts.upstream && apiUrl && token) {
4987
- return { apiUrl, token, refreshToken: config?.refreshToken };
5422
+ program.command("start").description("Start the local agent (authenticates, cleans up stale processes, starts fresh)").option("-p, --port <port>", "Local WS server port", "7862").option("-w, --workspace <path>", "Workspace root path").option("--api-key <key>", "Anthropic API key (for local-only use)").option("--no-upstream", "Don't connect to cloud (local-only mode)").option("--foreground", "Run in foreground (don't daemonize)").action(
5423
+ async (opts) => {
5424
+ if (!opts.foreground) {
5425
+ const ok = await ensureSetup();
5426
+ if (!ok) {
5427
+ console.error("Setup failed. Cannot start agent.");
5428
+ process.exit(1);
5429
+ }
5430
+ }
5431
+ const config = loadConfig();
5432
+ const port = parseInt(opts.port, 10);
5433
+ const workspacePath = opts.workspace ?? config?.workspacePath ?? process.cwd();
5434
+ const userId = config?.userId ?? process.env.JARVIS_USER_ID ?? "local";
5435
+ const explicitApiKey = opts.apiKey ?? config?.anthropicApiKey;
5436
+ const useSubscription = !explicitApiKey;
5437
+ const anthropicApiKey = explicitApiKey;
5438
+ if (opts.foreground) {
5439
+ if (useSubscription && process.env.ANTHROPIC_API_KEY) {
5440
+ delete process.env.ANTHROPIC_API_KEY;
5441
+ }
5442
+ await startAgent({
5443
+ port,
5444
+ workspacePath,
5445
+ anthropicApiKey,
5446
+ useSubscription,
5447
+ userId,
5448
+ upstream: (() => {
5449
+ const apiUrl = process.env.JARVIS_UPSTREAM_URL ?? config?.apiUrl;
5450
+ const token = process.env.JARVIS_UPSTREAM_TOKEN ?? config?.token;
5451
+ if (opts.upstream && apiUrl && token) {
5452
+ return { apiUrl, token, refreshToken: config?.refreshToken };
5453
+ }
5454
+ return void 0;
5455
+ })()
5456
+ });
5457
+ return;
5458
+ }
5459
+ const service = createServiceManager();
5460
+ if (service.isInstalled()) {
5461
+ try {
5462
+ service.stop();
5463
+ } catch {
5464
+ }
5465
+ await new Promise((r) => setTimeout(r, 1e3));
5466
+ service.start();
5467
+ console.log("Jarvis agent started via OS service.");
5468
+ return;
5469
+ }
5470
+ const stalePid = readPid();
5471
+ if (stalePid) {
5472
+ try {
5473
+ process.kill(stalePid, "SIGTERM");
5474
+ } catch {
5475
+ }
5476
+ clearPid();
5477
+ }
5478
+ if (await isPortInUse(port)) {
5479
+ try {
5480
+ const output = execSync2(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
5481
+ if (output) {
5482
+ for (const p of output.split("\n")) {
5483
+ try {
5484
+ process.kill(parseInt(p, 10), "SIGTERM");
5485
+ } catch {
5486
+ }
5487
+ }
4988
5488
  }
4989
- return void 0;
4990
- })()
4991
- });
4992
- return;
4993
- }
4994
- fs7.mkdirSync(LOG_DIR, { recursive: true });
4995
- const logFd = fs7.openSync(LOG_FILE, "a");
4996
- const args = [
4997
- "start",
4998
- "--foreground",
4999
- "--port",
5000
- String(port),
5001
- "--workspace",
5002
- workspacePath
5003
- ];
5004
- if (!opts.upstream) {
5005
- args.push("--no-upstream");
5006
- }
5007
- if (anthropicApiKey) {
5008
- args.push("--api-key", anthropicApiKey);
5009
- }
5010
- const binPath = process.argv[1];
5011
- const cliRoot = path7.resolve(path7.dirname(binPath), "..");
5012
- const distEntry = path7.join(cliRoot, "dist", "bin.js");
5013
- const useDistEntry = fs7.existsSync(distEntry);
5014
- const child = spawn2(
5015
- process.execPath,
5016
- useDistEntry ? [distEntry, ...args] : [binPath, ...args],
5017
- {
5018
- detached: true,
5019
- stdio: ["ignore", logFd, logFd],
5020
- cwd: workspacePath,
5021
- env: { ...process.env, NODE_NO_WARNINGS: "1" }
5489
+ } catch {
5490
+ }
5491
+ await new Promise((r) => setTimeout(r, 1e3));
5492
+ }
5493
+ fs8.mkdirSync(LOG_DIR, { recursive: true });
5494
+ const logFd = fs8.openSync(LOG_FILE, "a");
5495
+ const args = [
5496
+ "start",
5497
+ "--foreground",
5498
+ "--port",
5499
+ String(port),
5500
+ "--workspace",
5501
+ workspacePath
5502
+ ];
5503
+ if (!opts.upstream) {
5504
+ args.push("--no-upstream");
5505
+ }
5506
+ if (anthropicApiKey) {
5507
+ args.push("--api-key", anthropicApiKey);
5508
+ }
5509
+ const binPath = process.argv[1];
5510
+ const cliRoot = path8.resolve(path8.dirname(binPath), "..");
5511
+ const distEntry = path8.join(cliRoot, "dist", "bin.js");
5512
+ const useDistEntry = fs8.existsSync(distEntry);
5513
+ const child = spawn3(
5514
+ process.execPath,
5515
+ useDistEntry ? [distEntry, ...args] : [binPath, ...args],
5516
+ {
5517
+ detached: true,
5518
+ stdio: ["ignore", logFd, logFd],
5519
+ cwd: workspacePath,
5520
+ env: {
5521
+ ...process.env,
5522
+ NODE_NO_WARNINGS: "1",
5523
+ // Strip API key when using subscription to prevent SDK from picking it up
5524
+ ...useSubscription ? { ANTHROPIC_API_KEY: "" } : {}
5525
+ }
5526
+ }
5527
+ );
5528
+ child.unref();
5529
+ fs8.closeSync(logFd);
5530
+ savePid(child.pid);
5531
+ console.log(`Jarvis agent started (PID: ${child.pid})`);
5532
+ console.log(` Local: ws://127.0.0.1:${port}`);
5533
+ console.log(` Workspace: ${workspacePath}`);
5534
+ console.log(` Logs: ${LOG_FILE}`);
5535
+ if (config?.apiUrl) {
5536
+ console.log(` Cloud: ${config.apiUrl}`);
5022
5537
  }
5023
- );
5024
- child.unref();
5025
- fs7.closeSync(logFd);
5026
- savePid(child.pid);
5027
- console.log(`Jarvis agent started (PID: ${child.pid})`);
5028
- console.log(` Local: ws://127.0.0.1:${port}`);
5029
- console.log(` Workspace: ${workspacePath}`);
5030
- console.log(` Logs: ${LOG_FILE}`);
5031
- if (config?.apiUrl) {
5032
- console.log(` Cloud: ${config.apiUrl}`);
5033
5538
  }
5034
- console.log();
5035
- console.log(`Commands:`);
5036
- console.log(` jarvis logs \u2014 View agent logs`);
5037
- console.log(` jarvis stop \u2014 Stop the agent`);
5038
- console.log(` jarvis restart \u2014 Restart the agent`);
5039
- });
5539
+ );
5040
5540
  program.command("stop").description("Stop the running agent").action(async () => {
5541
+ const service = createServiceManager();
5542
+ if (service.isInstalled()) {
5543
+ service.stop();
5544
+ clearPid();
5545
+ console.log("Agent stopped via OS service.");
5546
+ console.log("Use 'jarvis start' to re-enable, or 'jarvis uninstall' to remove auto-start.");
5547
+ return;
5548
+ }
5041
5549
  const pid = readPid();
5042
5550
  if (pid) {
5043
5551
  try {
@@ -5053,7 +5561,7 @@ function createCli() {
5053
5561
  const running = await isPortInUse(7862);
5054
5562
  if (running) {
5055
5563
  try {
5056
- const output = execSync("lsof -ti :7862", { encoding: "utf-8" }).trim();
5564
+ const output = execSync2("lsof -ti :7862", { encoding: "utf-8" }).trim();
5057
5565
  if (output) {
5058
5566
  const pids = output.split("\n");
5059
5567
  for (const p of pids) {
@@ -5070,27 +5578,16 @@ function createCli() {
5070
5578
  }
5071
5579
  console.log("No agent is running.");
5072
5580
  });
5073
- program.command("restart").description("Restart the agent").action(async () => {
5074
- const pid = readPid();
5075
- if (pid) {
5076
- try {
5077
- process.kill(pid, "SIGTERM");
5078
- } catch {
5079
- }
5080
- clearPid();
5081
- console.log(`Stopped agent (PID: ${pid})`);
5082
- await new Promise((r) => setTimeout(r, 1e3));
5083
- }
5084
- console.log("Starting agent...");
5581
+ program.command("restart").description("Restart the agent (alias for start \u2014 start always does a clean restart)").action(async () => {
5085
5582
  await program.parseAsync(["node", "jarvis", "start"]);
5086
5583
  });
5087
5584
  program.command("logs").description("View agent logs").option("-f, --follow", "Follow log output (like tail -f)", true).option("-n, --lines <n>", "Number of lines to show", "50").action((opts) => {
5088
- if (!fs7.existsSync(LOG_FILE)) {
5585
+ if (!fs8.existsSync(LOG_FILE)) {
5089
5586
  console.log("No log file found. Start the agent first: jarvis start");
5090
5587
  return;
5091
5588
  }
5092
5589
  if (opts.follow) {
5093
- const tail = spawn2("tail", ["-f", "-n", opts.lines, LOG_FILE], {
5590
+ const tail = spawn3("tail", ["-f", "-n", opts.lines, LOG_FILE], {
5094
5591
  stdio: "inherit"
5095
5592
  });
5096
5593
  process.on("SIGINT", () => {
@@ -5099,7 +5596,7 @@ function createCli() {
5099
5596
  });
5100
5597
  tail.on("exit", () => process.exit(0));
5101
5598
  } else {
5102
- const content = execSync(`tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: "utf-8" });
5599
+ const content = execSync2(`tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: "utf-8" });
5103
5600
  process.stdout.write(content);
5104
5601
  }
5105
5602
  });
@@ -5109,7 +5606,9 @@ function createCli() {
5109
5606
  const port = config?.port ?? 7862;
5110
5607
  const running = await isPortInUse(port);
5111
5608
  console.log(`Jarvis Agent v${PKG_VERSION}:`);
5112
- console.log(` Status: ${running ? `\x1B[32mrunning\x1B[0m` : `\x1B[31mstopped\x1B[0m`}${pid ? ` (PID: ${pid})` : ""}`);
5609
+ console.log(
5610
+ ` Status: ${running ? `\x1B[32mrunning\x1B[0m` : `\x1B[31mstopped\x1B[0m`}${pid ? ` (PID: ${pid})` : ""}`
5611
+ );
5113
5612
  console.log(` Port: ${port}`);
5114
5613
  console.log(` Config: ${getConfigPath()}`);
5115
5614
  if (config) {
@@ -5123,15 +5622,105 @@ function createCli() {
5123
5622
  } else {
5124
5623
  console.log(` Config: Not set up. Run 'jarvis connect <token>'`);
5125
5624
  }
5625
+ const svc = createServiceManager();
5626
+ const svcStatus = svc.status();
5627
+ if (svcStatus.installed) {
5628
+ const label = svcStatus.os ?? "unknown";
5629
+ const state2 = svcStatus.running ? `\x1B[32minstalled & running\x1B[0m (${label})` : `\x1B[33minstalled, stopped\x1B[0m (${label})`;
5630
+ console.log(` Service: ${state2}`);
5631
+ } else {
5632
+ console.log(` Service: not installed (run 'jarvis install' for always-on)`);
5633
+ }
5126
5634
  console.log(` Logs: ${LOG_FILE}`);
5127
5635
  });
5636
+ program.command("install").description("Install as OS service for auto-start on login and crash recovery").option("-p, --port <port>", "Local WS server port", "7862").option("-w, --workspace <path>", "Workspace root path").option("--no-upstream", "Don't connect to cloud").action(async (opts) => {
5637
+ await ensureSetup();
5638
+ const config = loadConfig();
5639
+ const port = parseInt(opts.port, 10);
5640
+ const workspacePath = opts.workspace ?? config?.workspacePath ?? process.cwd();
5641
+ const service = createServiceManager();
5642
+ const binPath = process.argv[1];
5643
+ const cliRoot = path8.resolve(path8.dirname(binPath), "..");
5644
+ const distEntry = path8.join(cliRoot, "dist", "bin.js");
5645
+ const entryPath = fs8.existsSync(distEntry) ? distEntry : binPath;
5646
+ if (!fs8.existsSync(distEntry)) {
5647
+ console.log("\x1B[33mWarning:\x1B[0m Using dev mode entry point (tsx).");
5648
+ console.log(" For reliability, build first: pnpm build");
5649
+ console.log();
5650
+ }
5651
+ const pid = readPid();
5652
+ if (pid) {
5653
+ try {
5654
+ process.kill(pid, "SIGTERM");
5655
+ } catch {
5656
+ }
5657
+ clearPid();
5658
+ }
5659
+ try {
5660
+ service.install({
5661
+ port,
5662
+ workspacePath,
5663
+ nodePath: process.execPath,
5664
+ entryPath,
5665
+ upstream: opts.upstream
5666
+ });
5667
+ } catch (err) {
5668
+ console.error(`Failed to install service: ${err instanceof Error ? err.message : err}`);
5669
+ process.exit(1);
5670
+ }
5671
+ const status = service.status();
5672
+ console.log(`Jarvis agent installed as OS service`);
5673
+ console.log(` Platform: ${status.os}`);
5674
+ console.log(` Port: ${port}`);
5675
+ console.log(` Workspace: ${workspacePath}`);
5676
+ console.log(` Auto-start on login: \x1B[32menabled\x1B[0m`);
5677
+ console.log(` Crash recovery: \x1B[32menabled\x1B[0m`);
5678
+ console.log();
5679
+ console.log(`Commands:`);
5680
+ console.log(` jarvis status \u2014 Check service status`);
5681
+ console.log(` jarvis uninstall \u2014 Remove OS service`);
5682
+ });
5683
+ program.command("uninstall").description("Remove OS service (stops auto-start and crash recovery)").action(() => {
5684
+ const service = createServiceManager();
5685
+ if (!service.isInstalled()) {
5686
+ console.log("No service is installed. Nothing to do.");
5687
+ return;
5688
+ }
5689
+ try {
5690
+ service.stop();
5691
+ } catch {
5692
+ }
5693
+ service.uninstall();
5694
+ clearPid();
5695
+ console.log("Jarvis OS service removed.");
5696
+ console.log("The agent will no longer auto-start on login or restart on crash.");
5697
+ });
5128
5698
  program.command("logout").description("Clear saved configuration").action(() => {
5129
5699
  clearConfig();
5130
5700
  console.log("Configuration cleared.");
5131
5701
  });
5702
+ program.command("chat", { isDefault: true, hidden: true }).description("Interactive TUI").option("--model <id>", "Model (e.g., claude-opus-4-20250514)").option("--mode <mode>", "Permission mode: supervised|auto|plan|yolo").option("--thinking <config>", "Thinking: adaptive|enabled|disabled").option("--thinking-budget <n>", "Token budget when thinking=enabled").option("--max-turns <n>", "Max turns per task").option("-p, --port <port>", "Agent server port", "7862").option("-w, --workspace <path>", "Workspace root path").option("--no-server", "Connect to existing server, don't auto-start").action(async (opts) => {
5703
+ await ensureSetup();
5704
+ await launchChat(opts);
5705
+ });
5132
5706
  return program;
5133
5707
  }
5134
5708
 
5135
5709
  // src/bin.ts
5710
+ function findEnv() {
5711
+ let dir = process.cwd();
5712
+ while (dir !== path9.dirname(dir)) {
5713
+ const envPath = path9.join(dir, ".env");
5714
+ if (fs9.existsSync(envPath)) return envPath;
5715
+ dir = path9.dirname(dir);
5716
+ }
5717
+ return void 0;
5718
+ }
5719
+ var envFile = findEnv();
5720
+ if (envFile) {
5721
+ const prev = process.env.ANTHROPIC_API_KEY;
5722
+ dotenv.config({ path: envFile });
5723
+ if (prev === void 0) delete process.env.ANTHROPIC_API_KEY;
5724
+ }
5136
5725
  createCli().parse();
5137
5726
  //# sourceMappingURL=bin.js.map