@appchy/jarvis 0.1.14 → 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,9 +1,14 @@
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";
@@ -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);
@@ -4798,26 +4803,453 @@ async function waitForAgent(port, maxAttempts = 30) {
4798
4803
  }
4799
4804
  }
4800
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
+
4801
5233
  // src/cli.ts
4802
5234
  import { createRequire } from "module";
4803
5235
  var _require = createRequire(import.meta.url);
4804
5236
  var PKG_VERSION = _require("../package.json").version ?? "dev";
4805
- var LOG_DIR = path7.join(os4.homedir(), ".jarvis");
4806
- var LOG_FILE = path7.join(LOG_DIR, "agent.log");
4807
- 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");
4808
5240
  function savePid(pid) {
4809
- fs7.mkdirSync(LOG_DIR, { recursive: true });
4810
- fs7.writeFileSync(PID_FILE, String(pid));
5241
+ fs8.mkdirSync(LOG_DIR, { recursive: true });
5242
+ fs8.writeFileSync(PID_FILE, String(pid));
4811
5243
  }
4812
5244
  function readPid() {
4813
5245
  try {
4814
- const pid = parseInt(fs7.readFileSync(PID_FILE, "utf-8").trim(), 10);
5246
+ const pid = parseInt(fs8.readFileSync(PID_FILE, "utf-8").trim(), 10);
4815
5247
  if (isNaN(pid)) return null;
4816
5248
  try {
4817
5249
  process.kill(pid, 0);
4818
5250
  return pid;
4819
5251
  } catch {
4820
- fs7.unlinkSync(PID_FILE);
5252
+ fs8.unlinkSync(PID_FILE);
4821
5253
  return null;
4822
5254
  }
4823
5255
  } catch {
@@ -4826,7 +5258,7 @@ function readPid() {
4826
5258
  }
4827
5259
  function clearPid() {
4828
5260
  try {
4829
- fs7.unlinkSync(PID_FILE);
5261
+ fs8.unlinkSync(PID_FILE);
4830
5262
  } catch {
4831
5263
  }
4832
5264
  }
@@ -4876,7 +5308,7 @@ async function findFreePort() {
4876
5308
  async function browserAuth(appUrl) {
4877
5309
  const config = loadConfig();
4878
5310
  const callbackPort = await findFreePort();
4879
- const loginUrl = `${appUrl}/login?callback=cli&port=${callbackPort}`;
5311
+ const loginUrl = `${appUrl}/api/auth/cli?port=${callbackPort}`;
4880
5312
  console.log();
4881
5313
  console.log("Opening browser for sign in...");
4882
5314
  console.log(` If it doesn't open, visit: ${loginUrl}`);
@@ -4906,22 +5338,56 @@ async function browserAuth(appUrl) {
4906
5338
  return false;
4907
5339
  }
4908
5340
  }
4909
- 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
+ }
4910
5373
  async function ensureSetup() {
4911
5374
  const config = loadConfig();
4912
- 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) {
4913
5379
  return true;
4914
5380
  }
4915
- const ok = await browserAuth(APP_URL);
4916
- if (!ok) return false;
4917
- const pid = readPid();
4918
- if (pid) {
4919
- try {
4920
- process.kill(pid, "SIGTERM");
4921
- } 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;
4922
5388
  }
4923
- clearPid();
4924
- await new Promise((r) => setTimeout(r, 1e3));
5389
+ console.log("Token refresh failed. Re-authenticating...");
5390
+ return browserAuth(getAppUrl());
4925
5391
  }
4926
5392
  return true;
4927
5393
  }
@@ -4953,98 +5419,133 @@ function createCli() {
4953
5419
  process.exit(1);
4954
5420
  }
4955
5421
  });
4956
- 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) => {
4957
- if (!opts.foreground) {
4958
- await ensureSetup();
4959
- }
4960
- const config = loadConfig();
4961
- const port = parseInt(opts.port, 10);
4962
- if (!opts.foreground) {
4963
- const alreadyRunning = await isPortInUse(port);
4964
- if (alreadyRunning) {
4965
- console.log(`Jarvis agent is already running on ws://127.0.0.1:${port}`);
4966
- console.log(`Use 'jarvis restart' to restart, or 'jarvis logs' to view logs.`);
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
+ });
4967
5457
  return;
4968
5458
  }
4969
- }
4970
- const workspacePath = opts.workspace ?? config?.workspacePath ?? process.cwd();
4971
- const userId = config?.userId ?? process.env.JARVIS_USER_ID ?? "local";
4972
- const explicitApiKey = opts.apiKey ?? config?.anthropicApiKey;
4973
- const useSubscription = !explicitApiKey;
4974
- const anthropicApiKey = explicitApiKey;
4975
- if (opts.foreground) {
4976
- if (useSubscription && process.env.ANTHROPIC_API_KEY) {
4977
- delete process.env.ANTHROPIC_API_KEY;
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;
4978
5469
  }
4979
- await startAgent({
4980
- port,
4981
- workspacePath,
4982
- anthropicApiKey,
4983
- useSubscription,
4984
- userId,
4985
- upstream: (() => {
4986
- const apiUrl = process.env.JARVIS_UPSTREAM_URL ?? config?.apiUrl;
4987
- const token = process.env.JARVIS_UPSTREAM_TOKEN ?? config?.token;
4988
- if (opts.upstream && apiUrl && token) {
4989
- return { apiUrl, token, refreshToken: config?.refreshToken };
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
+ }
4990
5488
  }
4991
- return void 0;
4992
- })()
4993
- });
4994
- return;
4995
- }
4996
- fs7.mkdirSync(LOG_DIR, { recursive: true });
4997
- const logFd = fs7.openSync(LOG_FILE, "a");
4998
- const args = [
4999
- "start",
5000
- "--foreground",
5001
- "--port",
5002
- String(port),
5003
- "--workspace",
5004
- workspacePath
5005
- ];
5006
- if (!opts.upstream) {
5007
- args.push("--no-upstream");
5008
- }
5009
- if (anthropicApiKey) {
5010
- args.push("--api-key", anthropicApiKey);
5011
- }
5012
- const binPath = process.argv[1];
5013
- const cliRoot = path7.resolve(path7.dirname(binPath), "..");
5014
- const distEntry = path7.join(cliRoot, "dist", "bin.js");
5015
- const useDistEntry = fs7.existsSync(distEntry);
5016
- const child = spawn2(
5017
- process.execPath,
5018
- useDistEntry ? [distEntry, ...args] : [binPath, ...args],
5019
- {
5020
- detached: true,
5021
- stdio: ["ignore", logFd, logFd],
5022
- cwd: workspacePath,
5023
- env: {
5024
- ...process.env,
5025
- NODE_NO_WARNINGS: "1",
5026
- // Strip API key when using subscription to prevent SDK from picking it up
5027
- ...useSubscription ? { ANTHROPIC_API_KEY: "" } : {}
5489
+ } catch {
5028
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}`);
5029
5537
  }
5030
- );
5031
- child.unref();
5032
- fs7.closeSync(logFd);
5033
- savePid(child.pid);
5034
- console.log(`Jarvis agent started (PID: ${child.pid})`);
5035
- console.log(` Local: ws://127.0.0.1:${port}`);
5036
- console.log(` Workspace: ${workspacePath}`);
5037
- console.log(` Logs: ${LOG_FILE}`);
5038
- if (config?.apiUrl) {
5039
- console.log(` Cloud: ${config.apiUrl}`);
5040
5538
  }
5041
- console.log();
5042
- console.log(`Commands:`);
5043
- console.log(` jarvis logs \u2014 View agent logs`);
5044
- console.log(` jarvis stop \u2014 Stop the agent`);
5045
- console.log(` jarvis restart \u2014 Restart the agent`);
5046
- });
5539
+ );
5047
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
+ }
5048
5549
  const pid = readPid();
5049
5550
  if (pid) {
5050
5551
  try {
@@ -5060,7 +5561,7 @@ function createCli() {
5060
5561
  const running = await isPortInUse(7862);
5061
5562
  if (running) {
5062
5563
  try {
5063
- const output = execSync("lsof -ti :7862", { encoding: "utf-8" }).trim();
5564
+ const output = execSync2("lsof -ti :7862", { encoding: "utf-8" }).trim();
5064
5565
  if (output) {
5065
5566
  const pids = output.split("\n");
5066
5567
  for (const p of pids) {
@@ -5077,27 +5578,16 @@ function createCli() {
5077
5578
  }
5078
5579
  console.log("No agent is running.");
5079
5580
  });
5080
- program.command("restart").description("Restart the agent").action(async () => {
5081
- const pid = readPid();
5082
- if (pid) {
5083
- try {
5084
- process.kill(pid, "SIGTERM");
5085
- } catch {
5086
- }
5087
- clearPid();
5088
- console.log(`Stopped agent (PID: ${pid})`);
5089
- await new Promise((r) => setTimeout(r, 1e3));
5090
- }
5091
- console.log("Starting agent...");
5581
+ program.command("restart").description("Restart the agent (alias for start \u2014 start always does a clean restart)").action(async () => {
5092
5582
  await program.parseAsync(["node", "jarvis", "start"]);
5093
5583
  });
5094
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) => {
5095
- if (!fs7.existsSync(LOG_FILE)) {
5585
+ if (!fs8.existsSync(LOG_FILE)) {
5096
5586
  console.log("No log file found. Start the agent first: jarvis start");
5097
5587
  return;
5098
5588
  }
5099
5589
  if (opts.follow) {
5100
- const tail = spawn2("tail", ["-f", "-n", opts.lines, LOG_FILE], {
5590
+ const tail = spawn3("tail", ["-f", "-n", opts.lines, LOG_FILE], {
5101
5591
  stdio: "inherit"
5102
5592
  });
5103
5593
  process.on("SIGINT", () => {
@@ -5106,7 +5596,7 @@ function createCli() {
5106
5596
  });
5107
5597
  tail.on("exit", () => process.exit(0));
5108
5598
  } else {
5109
- 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" });
5110
5600
  process.stdout.write(content);
5111
5601
  }
5112
5602
  });
@@ -5116,7 +5606,9 @@ function createCli() {
5116
5606
  const port = config?.port ?? 7862;
5117
5607
  const running = await isPortInUse(port);
5118
5608
  console.log(`Jarvis Agent v${PKG_VERSION}:`);
5119
- 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
+ );
5120
5612
  console.log(` Port: ${port}`);
5121
5613
  console.log(` Config: ${getConfigPath()}`);
5122
5614
  if (config) {
@@ -5130,8 +5622,79 @@ function createCli() {
5130
5622
  } else {
5131
5623
  console.log(` Config: Not set up. Run 'jarvis connect <token>'`);
5132
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
+ }
5133
5634
  console.log(` Logs: ${LOG_FILE}`);
5134
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
+ });
5135
5698
  program.command("logout").description("Clear saved configuration").action(() => {
5136
5699
  clearConfig();
5137
5700
  console.log("Configuration cleared.");
@@ -5144,5 +5707,20 @@ function createCli() {
5144
5707
  }
5145
5708
 
5146
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
+ }
5147
5725
  createCli().parse();
5148
5726
  //# sourceMappingURL=bin.js.map