@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 +227 -9
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
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
|
-
...
|
|
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 ${
|
|
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:
|
|
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();
|