@appchy/jarvis 0.1.7 → 0.1.9

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
@@ -400,6 +400,7 @@ function claudeCodeProvider(config = {}) {
400
400
  } : {},
401
401
  ...config.disallowedTools?.length ? { disallowedTools: config.disallowedTools } : {},
402
402
  ...config.abortController ? { abortController: config.abortController } : {},
403
+ ...config.thinking ? { thinking: config.thinking } : {},
403
404
  // Session resume/persistence options
404
405
  ...config.session?.sessionId ? { sessionId: config.session.sessionId } : {},
405
406
  ...config.session?.resume ? { resume: config.session.resume } : {},
@@ -569,6 +570,7 @@ function claudeCodeProvider(config = {}) {
569
570
  } : {},
570
571
  ...config.disallowedTools?.length ? { disallowedTools: config.disallowedTools } : {},
571
572
  ...config.abortController ? { abortController: config.abortController } : {},
573
+ ...config.thinking ? { thinking: config.thinking } : {},
572
574
  // Session resume/persistence options
573
575
  ...config.session?.sessionId ? { sessionId: config.session.sessionId } : {},
574
576
  ...config.session?.resume ? { resume: config.session.resume } : {},
@@ -2018,6 +2020,8 @@ function createWsProgressHandlers(deps) {
2018
2020
  let currentThinkingId = null;
2019
2021
  let currentTextId = null;
2020
2022
  let currentTextContent = "";
2023
+ let planFileContent = null;
2024
+ let planFilePath = null;
2021
2025
  async function onMessage(msg) {
2022
2026
  broadcast({ type: "agent:onMessage", taskId, message: msg });
2023
2027
  }
@@ -2045,10 +2049,27 @@ function createWsProgressHandlers(deps) {
2045
2049
  const now = (/* @__PURE__ */ new Date()).toISOString();
2046
2050
  if (toolName === "askUser") return waitForInput(input);
2047
2051
  if (toolName === "ExitPlanMode") {
2052
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2053
+ if (planFileContent) {
2054
+ await onMessage({
2055
+ id: `plan-${crypto2.randomUUID()}`,
2056
+ role: "assistant",
2057
+ type: "ui",
2058
+ content: planFileContent,
2059
+ ui: {
2060
+ uri: "ui://agent/plan",
2061
+ title: "Plan",
2062
+ data: { content: planFileContent, path: planFilePath }
2063
+ },
2064
+ isPartial: false,
2065
+ createdAt: now2
2066
+ });
2067
+ }
2048
2068
  return waitForInput({
2049
2069
  ...input,
2050
2070
  type: "plan",
2051
- reason: input.input?.allowedPrompts || "Plan ready for review"
2071
+ reason: input.input?.allowedPrompts || "Plan ready for review",
2072
+ output: planFileContent ? { content: planFileContent, path: planFilePath } : input.output
2052
2073
  });
2053
2074
  }
2054
2075
  if (toolName === "EnterPlanMode") {
@@ -2108,6 +2129,11 @@ function createWsProgressHandlers(deps) {
2108
2129
  isComplete: false
2109
2130
  });
2110
2131
  }
2132
+ if (block.type === "tool_use" && block.name === "Write" && typeof block.input?.file_path === "string" && block.input.file_path.endsWith(".md")) {
2133
+ const input = block.input;
2134
+ planFileContent = input.content ?? null;
2135
+ planFilePath = input.file_path ?? null;
2136
+ }
2111
2137
  if (block.type === "tool_use" && block.id && block.name) {
2112
2138
  if (currentTextId) {
2113
2139
  broadcast({
@@ -2233,6 +2259,8 @@ function createAgent(deps) {
2233
2259
  worktreePath: wsPath,
2234
2260
  messageCount: msg.messages.length
2235
2261
  });
2262
+ const askModeTools = msg.permissionMode === "ask" ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
2263
+ const allDisallowed = [...msg.disallowedTools ?? [], ...askModeTools];
2236
2264
  const provider2 = claudeCodeProvider({
2237
2265
  ...deps.useSubscription ? { useSubscription: true } : { apiKey: deps.anthropicApiKey },
2238
2266
  systemPrompt: systemMsg?.content ?? "",
@@ -2247,7 +2275,8 @@ function createAgent(deps) {
2247
2275
  onProgress: handlers.handleProgress,
2248
2276
  onUserInput: handlers.handleUserInput,
2249
2277
  permissionMode: msg.permissionMode === "plan" ? "plan" : "default",
2250
- ...msg.disallowedTools?.length ? { disallowedTools: msg.disallowedTools } : {},
2278
+ ...allDisallowed.length ? { disallowedTools: allDisallowed } : {},
2279
+ ...msg.thinking ? { thinking: msg.thinking } : {},
2251
2280
  ...msg.session ? { session: msg.session } : {}
2252
2281
  });
2253
2282
  const ctx = { requestId: taskId, userId: deps.userId };
@@ -2303,7 +2332,7 @@ function createAgent(deps) {
2303
2332
  deps.broadcast({
2304
2333
  type: "agent:output",
2305
2334
  taskId,
2306
- output: { success: false, content: "", error: "Aborted" }
2335
+ output: { success: false, content: "", error: "Aborted", sessionId: msg.session?.resume }
2307
2336
  });
2308
2337
  } else {
2309
2338
  const error = err instanceof Error ? err.message : String(err);
@@ -2519,17 +2548,77 @@ function createUpstreamClient(config) {
2519
2548
  let ws = null;
2520
2549
  let messageHandler = null;
2521
2550
  let reconnectTimer = null;
2551
+ let refreshTimer = null;
2522
2552
  let closed = false;
2523
2553
  let authFailures = 0;
2554
+ let retries = 0;
2555
+ let currentToken = config.token;
2524
2556
  const MAX_AUTH_RETRIES = 3;
2557
+ const MAX_RETRIES = 3;
2558
+ const REFRESH_BUFFER_MS = 5 * 60 * 1e3;
2559
+ function getRefreshUrl() {
2560
+ if (config.refreshUrl) return config.refreshUrl;
2561
+ if (!config.refreshToken) return null;
2562
+ return config.apiUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:") + "/refresh";
2563
+ }
2564
+ function getTokenExpiry(token) {
2565
+ try {
2566
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
2567
+ return payload.exp ?? null;
2568
+ } catch {
2569
+ return null;
2570
+ }
2571
+ }
2572
+ function scheduleRefresh() {
2573
+ if (refreshTimer) clearTimeout(refreshTimer);
2574
+ if (!config.refreshToken) return;
2575
+ const exp = getTokenExpiry(currentToken);
2576
+ if (!exp) return;
2577
+ const msUntilExpiry = exp * 1e3 - Date.now();
2578
+ const refreshIn = Math.max(msUntilExpiry - REFRESH_BUFFER_MS, 0);
2579
+ logger.sys.info("[Upstream] Token refresh scheduled", {
2580
+ expiresIn: `${Math.round(msUntilExpiry / 1e3)}s`,
2581
+ refreshIn: `${Math.round(refreshIn / 1e3)}s`
2582
+ });
2583
+ refreshTimer = setTimeout(refreshToken, refreshIn);
2584
+ }
2585
+ async function refreshToken() {
2586
+ const url = getRefreshUrl();
2587
+ if (!url || !config.refreshToken) return false;
2588
+ try {
2589
+ logger.sys.info("[Upstream] Refreshing token...");
2590
+ const res = await fetch(url, {
2591
+ method: "POST",
2592
+ headers: { "Content-Type": "application/json" },
2593
+ body: JSON.stringify({ refreshToken: config.refreshToken })
2594
+ });
2595
+ if (!res.ok) {
2596
+ logger.sys.error("[Upstream] Token refresh failed", { status: res.status });
2597
+ return false;
2598
+ }
2599
+ const { token } = await res.json();
2600
+ currentToken = token;
2601
+ logger.sys.info("[Upstream] Token refreshed successfully");
2602
+ ws?.close();
2603
+ scheduleRefresh();
2604
+ return true;
2605
+ } catch (err) {
2606
+ logger.sys.error("[Upstream] Token refresh error", {
2607
+ error: err instanceof Error ? err.message : String(err)
2608
+ });
2609
+ return false;
2610
+ }
2611
+ }
2525
2612
  function connect() {
2526
2613
  if (closed) return;
2527
2614
  ws = new WebSocket2(config.apiUrl, {
2528
- headers: { authorization: `Bearer ${config.token}` }
2615
+ headers: { authorization: `Bearer ${currentToken}` }
2529
2616
  });
2530
2617
  ws.on("open", () => {
2531
2618
  authFailures = 0;
2619
+ retries = 0;
2532
2620
  logger.sys.info("[Upstream] Connected to cloud", { apiUrl: config.apiUrl });
2621
+ scheduleRefresh();
2533
2622
  });
2534
2623
  ws.on("message", (raw) => {
2535
2624
  try {
@@ -2545,6 +2634,24 @@ function createUpstreamClient(config) {
2545
2634
  ws.on("close", (code) => {
2546
2635
  if (closed) return;
2547
2636
  if (code === 4001 || code === 4003) {
2637
+ if (config.refreshToken) {
2638
+ refreshToken().then((ok) => {
2639
+ if (ok) {
2640
+ reconnectTimer = setTimeout(connect, 1e3);
2641
+ } else {
2642
+ authFailures++;
2643
+ if (authFailures >= MAX_AUTH_RETRIES) {
2644
+ logger.sys.error("[Upstream] Auth failed after max retries, giving up", {
2645
+ code,
2646
+ attempts: authFailures
2647
+ });
2648
+ return;
2649
+ }
2650
+ reconnectTimer = setTimeout(connect, 3e3);
2651
+ }
2652
+ });
2653
+ return;
2654
+ }
2548
2655
  authFailures++;
2549
2656
  if (authFailures >= MAX_AUTH_RETRIES) {
2550
2657
  logger.sys.error("[Upstream] Auth failed after max retries, giving up", {
@@ -2558,8 +2665,21 @@ function createUpstreamClient(config) {
2558
2665
  attempt: authFailures,
2559
2666
  maxRetries: MAX_AUTH_RETRIES
2560
2667
  });
2668
+ reconnectTimer = setTimeout(connect, 3e3);
2669
+ return;
2670
+ }
2671
+ retries++;
2672
+ if (retries >= MAX_RETRIES) {
2673
+ logger.sys.error("[Upstream] Max reconnect attempts reached, giving up", {
2674
+ attempts: retries
2675
+ });
2676
+ return;
2561
2677
  }
2562
- logger.sys.info("[Upstream] Disconnected, reconnecting in 3s...");
2678
+ logger.sys.info("[Upstream] Disconnected, reconnecting in 3s...", {
2679
+ code,
2680
+ attempt: retries,
2681
+ maxRetries: MAX_RETRIES
2682
+ });
2563
2683
  reconnectTimer = setTimeout(connect, 3e3);
2564
2684
  });
2565
2685
  ws.on("error", (err) => {
@@ -2582,6 +2702,7 @@ function createUpstreamClient(config) {
2582
2702
  function close() {
2583
2703
  closed = true;
2584
2704
  if (reconnectTimer) clearTimeout(reconnectTimer);
2705
+ if (refreshTimer) clearTimeout(refreshTimer);
2585
2706
  ws?.close();
2586
2707
  }
2587
2708
  return { connect, send, onMessage, isConnected, close };
@@ -4712,8 +4833,99 @@ function clearPid() {
4712
4833
  } catch {
4713
4834
  }
4714
4835
  }
4836
+ async function waitForBrowserAuth(port) {
4837
+ const http = await import("http");
4838
+ return new Promise((resolve, reject) => {
4839
+ const timeout = setTimeout(() => {
4840
+ server.close();
4841
+ reject(new Error("Browser authentication timed out (2 minutes)"));
4842
+ }, 12e4);
4843
+ const server = http.createServer((req, res) => {
4844
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
4845
+ if (url.pathname === "/callback") {
4846
+ const token = url.searchParams.get("token");
4847
+ res.setHeader("Access-Control-Allow-Origin", "*");
4848
+ res.setHeader("Access-Control-Allow-Methods", "GET");
4849
+ if (token) {
4850
+ res.writeHead(200, { "Content-Type": "text/plain" });
4851
+ res.end("OK");
4852
+ clearTimeout(timeout);
4853
+ server.close();
4854
+ resolve(token);
4855
+ } else {
4856
+ res.writeHead(400, { "Content-Type": "text/plain" });
4857
+ res.end("Missing token");
4858
+ }
4859
+ return;
4860
+ }
4861
+ res.writeHead(404);
4862
+ res.end();
4863
+ });
4864
+ server.listen(port, "127.0.0.1");
4865
+ });
4866
+ }
4867
+ async function findFreePort() {
4868
+ const net = await import("net");
4869
+ return new Promise((resolve, reject) => {
4870
+ const server = net.createServer();
4871
+ server.listen(0, "127.0.0.1", () => {
4872
+ const addr = server.address();
4873
+ const port = typeof addr === "object" && addr ? addr.port : 0;
4874
+ server.close(() => resolve(port));
4875
+ });
4876
+ server.on("error", reject);
4877
+ });
4878
+ }
4879
+ async function browserAuth(appUrl) {
4880
+ const config = loadConfig();
4881
+ const callbackPort = await findFreePort();
4882
+ const loginUrl = `${appUrl}/login?callback=cli&port=${callbackPort}`;
4883
+ console.log();
4884
+ console.log("Opening browser for sign in...");
4885
+ console.log(` If it doesn't open, visit: ${loginUrl}`);
4886
+ console.log();
4887
+ const { exec: exec3 } = await import("child_process");
4888
+ const platform = process.platform;
4889
+ const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
4890
+ exec3(`${openCmd} "${loginUrl}"`);
4891
+ console.log("Waiting for authentication...");
4892
+ try {
4893
+ const token = await waitForBrowserAuth(callbackPort);
4894
+ const parsed = parseConnectToken(token);
4895
+ saveConfig({
4896
+ ...config,
4897
+ apiUrl: parsed.apiUrl,
4898
+ token: parsed.jwt,
4899
+ refreshToken: parsed.refreshToken,
4900
+ userId: parsed.userId,
4901
+ envId: parsed.envId,
4902
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
4903
+ });
4904
+ console.log();
4905
+ console.log(`Signed in as ${parsed.userId}`);
4906
+ return true;
4907
+ } catch (err) {
4908
+ console.error(`Authentication failed: ${err instanceof Error ? err.message : err}`);
4909
+ return false;
4910
+ }
4911
+ }
4912
+ var DEFAULT_APP_URL = process.env.APP_URL ?? "http://localhost:3000";
4913
+ async function ensureSetup() {
4914
+ const config = loadConfig();
4915
+ if (config?.token || config?.anthropicApiKey || config?.useSubscription) {
4916
+ return true;
4917
+ }
4918
+ return browserAuth(config?.apiUrl ?? DEFAULT_APP_URL);
4919
+ }
4715
4920
  function createCli() {
4716
4921
  const program = new Command().name("jarvis").description("Jarvis local agent \u2014 runs Claude Code on your machine").version(PKG_VERSION);
4922
+ 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) => {
4923
+ if (message && program.commands.some((c) => c.name() === message)) {
4924
+ return;
4925
+ }
4926
+ await ensureSetup();
4927
+ await launchChat(opts);
4928
+ });
4717
4929
  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) => {
4718
4930
  try {
4719
4931
  const parsed = parseConnectToken(token);
@@ -4722,6 +4934,7 @@ function createCli() {
4722
4934
  ...existing,
4723
4935
  apiUrl: parsed.apiUrl,
4724
4936
  token: parsed.jwt,
4937
+ refreshToken: parsed.refreshToken,
4725
4938
  userId: parsed.userId,
4726
4939
  envId: parsed.envId,
4727
4940
  ...opts.workspace ? { workspacePath: opts.workspace } : {},
@@ -4740,6 +4953,7 @@ function createCli() {
4740
4953
  }
4741
4954
  });
4742
4955
  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) => {
4956
+ await ensureSetup();
4743
4957
  const config = loadConfig();
4744
4958
  const port = parseInt(opts.port, 10);
4745
4959
  const alreadyRunning = await isPortInUse(port);
@@ -4761,7 +4975,14 @@ function createCli() {
4761
4975
  anthropicApiKey,
4762
4976
  useSubscription: !anthropicApiKey,
4763
4977
  userId,
4764
- upstream: opts.upstream && config?.apiUrl && config?.token ? { apiUrl: config.apiUrl, token: config.token } : void 0
4978
+ upstream: (() => {
4979
+ const apiUrl = process.env.JARVIS_UPSTREAM_URL ?? config?.apiUrl;
4980
+ const token = process.env.JARVIS_UPSTREAM_TOKEN ?? config?.token;
4981
+ if (opts.upstream && apiUrl && token) {
4982
+ return { apiUrl, token, refreshToken: config?.refreshToken };
4983
+ }
4984
+ return void 0;
4985
+ })()
4765
4986
  });
4766
4987
  return;
4767
4988
  }
@@ -4877,9 +5098,6 @@ function createCli() {
4877
5098
  process.stdout.write(content);
4878
5099
  }
4879
5100
  });
4880
- program.command("chat").description("Interactive TUI \u2014 chat with Jarvis in your terminal").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) => {
4881
- await launchChat(opts);
4882
- });
4883
5101
  program.command("status").description("Show agent configuration and connection status").action(async () => {
4884
5102
  const config = loadConfig();
4885
5103
  const pid = readPid();