jarvis-hq 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client-html.js +247 -8
- package/dist/context-window.js +46 -0
- package/dist/dictation.js +8 -6
- package/dist/jarvis-tools.js +182 -1
- package/dist/jarvis.js +9 -2
- package/dist/server-common.js +5 -1
- package/dist/server.js +15 -4
- package/dist/start-claude-code.js +44 -1
- package/package.json +2 -2
- package/dist/client.js +0 -118
- package/dist/ls.js +0 -46
- package/dist/progress.js +0 -33
- package/dist/start.js +0 -742
- package/dist/sync.js +0 -15
- package/dist/trophy.js +0 -7
package/README.md
CHANGED
|
@@ -368,7 +368,7 @@ Sessions are the core abstraction. A session is created when a `/chat` POST is r
|
|
|
368
368
|
|
|
369
369
|
Jarvis detects which harnesses are available at startup by checking for the `claude` CLI, the `@opencode-ai/sdk` package, and the `codex` CLI. It reports the available agents at `/agents`. A bot's `model` and `permissionMode` are applied to whichever harness runs its threads.
|
|
370
370
|
|
|
371
|
-
**Claude Code** (`claude-code`): Uses the `@anthropic-ai/claude-agent-sdk` `query()` function. Runs the `claude-opus-
|
|
371
|
+
**Claude Code** (`claude-code`): Uses the `@anthropic-ai/claude-agent-sdk` `query()` function. Runs the `claude-opus-5[1m]` model (Opus 5 with the 1M-token context window) in `default` permission mode. Supports `canUseTool` for per-tool permission prompts. Session transcripts are stored at `~/.claude/projects/<cwd>/<session-id>.jsonl`.
|
|
372
372
|
|
|
373
373
|
**Opencode** (`opencode`): Uses the `@opencode-ai/sdk`. Jarvis spawns an Opencode server process at startup (or connects to one already running on port 4096). Per-directory clients are maintained so sessions can be scoped to different repos simultaneously. Events are received via a persistent Opencode event stream (`client.event.subscribe()`). If the stream fails, it reconnects automatically after 2 seconds.
|
|
374
374
|
|
package/dist/client-html.js
CHANGED
|
@@ -72,6 +72,16 @@ exports.html = `<!DOCTYPE html>
|
|
|
72
72
|
.bar-title { flex: 1; min-width: 0; }
|
|
73
73
|
.bar-title h2 { font-size: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
74
74
|
.bar-title .sub { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
75
|
+
/* Context meter: how full the model's window is, in the thread's own header. */
|
|
76
|
+
.ctx { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: 11px; color: var(--muted); }
|
|
77
|
+
.ctx-model { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
78
|
+
.ctx-dots { display: flex; gap: 2px; flex: none; }
|
|
79
|
+
.ctx-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--border); }
|
|
80
|
+
.ctx-dots i.on { background: var(--muted); }
|
|
81
|
+
.ctx-num { flex: none; font-variant-numeric: tabular-nums; }
|
|
82
|
+
/* Past three quarters full the meter stops being background detail. */
|
|
83
|
+
.ctx.full .ctx-dots i.on { background: var(--danger); }
|
|
84
|
+
.ctx.full .ctx-num { color: var(--danger); }
|
|
75
85
|
|
|
76
86
|
.back {
|
|
77
87
|
display: inline-flex; align-items: center; gap: 5px; flex: none;
|
|
@@ -357,6 +367,20 @@ exports.html = `<!DOCTYPE html>
|
|
|
357
367
|
color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 150px; overflow: auto;
|
|
358
368
|
}
|
|
359
369
|
.perm .acts { display: flex; gap: 8px; }
|
|
370
|
+
.ask-questions { display: flex; flex-direction: column; gap: 16px; margin-bottom: 14px; }
|
|
371
|
+
.ask-q { display: flex; flex-direction: column; gap: 8px; }
|
|
372
|
+
.ask-q-text { font-size: 13.5px; font-weight: 500; }
|
|
373
|
+
.ask-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
374
|
+
.ask-opt-btn {
|
|
375
|
+
text-align: left; padding: 7px 12px; border-radius: 8px; font-size: 13px;
|
|
376
|
+
background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
|
|
377
|
+
cursor: pointer; box-shadow: none;
|
|
378
|
+
}
|
|
379
|
+
.ask-opt-btn:hover { border-color: var(--accent); }
|
|
380
|
+
.ask-opt-btn.selected { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); border-color: var(--accent); color: var(--text); }
|
|
381
|
+
.ask-opt-desc { font-size: 11.5px; color: var(--muted); margin-top: 1px; padding-left: 2px; }
|
|
382
|
+
.ask-other { width: 100%; padding: 7px 12px; border-radius: 8px; font-size: 13px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); outline: none; }
|
|
383
|
+
.ask-other:focus { border-color: var(--accent); }
|
|
360
384
|
|
|
361
385
|
/* --- Composer --- */
|
|
362
386
|
.composer { border-top: 1px solid var(--border); background: var(--bg); }
|
|
@@ -477,6 +501,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
477
501
|
box-shadow: var(--shadow-md); animation: rise .18s ease;
|
|
478
502
|
}
|
|
479
503
|
@keyframes rise { from { opacity: 0; transform: translate(-50%, 8px); } }
|
|
504
|
+
.toast-action {
|
|
505
|
+
margin-left: 10px; padding: 2px 10px; border-radius: 999px;
|
|
506
|
+
background: var(--bg); color: var(--text); border: none;
|
|
507
|
+
font-size: 13px; font-weight: 600; cursor: pointer;
|
|
508
|
+
}
|
|
480
509
|
|
|
481
510
|
@media (max-width: 640px) {
|
|
482
511
|
.wrap { padding: 0 16px; }
|
|
@@ -560,6 +589,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
560
589
|
<div class="bar-title">
|
|
561
590
|
<h2 id="thread-title"></h2>
|
|
562
591
|
<div class="sub" id="thread-sub"></div>
|
|
592
|
+
<div class="ctx" id="thread-ctx" hidden>
|
|
593
|
+
<span class="ctx-model" id="ctx-model"></span>
|
|
594
|
+
<span class="ctx-dots" id="ctx-dots"></span>
|
|
595
|
+
<span class="ctx-num" id="ctx-num"></span>
|
|
596
|
+
</div>
|
|
563
597
|
</div>
|
|
564
598
|
</div></div>
|
|
565
599
|
<div class="messages" id="messages"><div class="wrap narrow" id="messages-inner"></div></div>
|
|
@@ -1279,6 +1313,39 @@ exports.html = `<!DOCTYPE html>
|
|
|
1279
1313
|
load(startPath);
|
|
1280
1314
|
}
|
|
1281
1315
|
|
|
1316
|
+
// --- Context meter ---
|
|
1317
|
+
// What the model is carrying into its next turn, against what it can hold.
|
|
1318
|
+
// Ten dots read at a glance on a phone; the exact numbers sit beside them and
|
|
1319
|
+
// the full sentence is in the tooltip.
|
|
1320
|
+
|
|
1321
|
+
function fmtTokens(n) {
|
|
1322
|
+
if (!n) return "0";
|
|
1323
|
+
return n < 1000 ? String(n) : Math.round(n / 1000) + "k";
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function renderContext(ctx) {
|
|
1327
|
+
var box = $("thread-ctx");
|
|
1328
|
+
if (!ctx || !ctx.used || !ctx.window) { box.hidden = true; return; }
|
|
1329
|
+
var pct = Math.min(100, Math.round((ctx.used / ctx.window) * 100));
|
|
1330
|
+
// "/1000k" already says the window is the long one, so the meter drops the
|
|
1331
|
+
// "(1M context)" tail that would otherwise eat the line on a phone. Plain
|
|
1332
|
+
// string work, not a regex: this file is a template literal, and its
|
|
1333
|
+
// backslashes belong to the build, not to the browser.
|
|
1334
|
+
var name = ctx.label || ctx.model || "";
|
|
1335
|
+
var tail = name.indexOf(" (1M");
|
|
1336
|
+
$("ctx-model").textContent = tail === -1 ? name : name.slice(0, tail);
|
|
1337
|
+
$("ctx-num").textContent = fmtTokens(ctx.used) + "/" + fmtTokens(ctx.window) + " (" + pct + "%)";
|
|
1338
|
+
var dots = $("ctx-dots");
|
|
1339
|
+
dots.innerHTML = "";
|
|
1340
|
+
var lit = Math.min(10, Math.max(1, Math.ceil(pct / 10)));
|
|
1341
|
+
for (var i = 0; i < 10; i++) dots.appendChild(el("i", i < lit ? "on" : null));
|
|
1342
|
+
box.className = "ctx" + (pct >= 75 ? " full" : "");
|
|
1343
|
+
box.title = (ctx.label ? ctx.label + " \u2014 " : "")
|
|
1344
|
+
+ ctx.used.toLocaleString() + " of " + ctx.window.toLocaleString()
|
|
1345
|
+
+ " tokens in context (" + pct + "% used)";
|
|
1346
|
+
box.hidden = false;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1282
1349
|
// --- Thread ---
|
|
1283
1350
|
function openThread(thread) {
|
|
1284
1351
|
activeThread = thread;
|
|
@@ -1286,6 +1353,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1286
1353
|
$("thread-title").textContent = thread.title;
|
|
1287
1354
|
$("thread-sub").textContent = shortPath(thread.repoPath, 30);
|
|
1288
1355
|
$("thread-sub").title = thread.repoPath || "";
|
|
1356
|
+
renderContext(thread.context);
|
|
1289
1357
|
$("to-bot-label").textContent = threadBack === "jarvis" ? "Jarvis" : (activeBot ? activeBot.name : "Back");
|
|
1290
1358
|
var av = $("thread-avatar");
|
|
1291
1359
|
av.textContent = activeBot ? (activeBot.emoji || "\\u{1F916}") : "\\u{1F916}";
|
|
@@ -1529,7 +1597,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1529
1597
|
|
|
1530
1598
|
// Every event type the agent emits on the turn stream.
|
|
1531
1599
|
var STREAM_EVENTS = ["user_prompt", "assistant", "tool_use", "status", "permission_request", "aborted", "done", "result", "error",
|
|
1532
|
-
"delegation", "handoff", "project_created"];
|
|
1600
|
+
"delegation", "handoff", "project_created", "context"];
|
|
1533
1601
|
|
|
1534
1602
|
function attachStream(sid, onEvent) {
|
|
1535
1603
|
closeStream();
|
|
@@ -1563,6 +1631,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
1563
1631
|
appendTool(liveBubble, data.tool_name, data.tool_input);
|
|
1564
1632
|
break;
|
|
1565
1633
|
|
|
1634
|
+
case "context":
|
|
1635
|
+
if (activeThread) activeThread.context = data;
|
|
1636
|
+
renderContext(data);
|
|
1637
|
+
break;
|
|
1638
|
+
|
|
1566
1639
|
case "status":
|
|
1567
1640
|
if (state === "aborting") break;
|
|
1568
1641
|
if (data.status === "thinking") setActivity("Thinking\u2026");
|
|
@@ -1677,6 +1750,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1677
1750
|
}
|
|
1678
1751
|
|
|
1679
1752
|
function renderPermission(data) {
|
|
1753
|
+
if (data.toolName === "AskUserQuestion") { renderAskUser(data); return; }
|
|
1680
1754
|
var card = el("div", "perm");
|
|
1681
1755
|
card.appendChild(el("div", "head", "Allow " + data.toolName + "?"));
|
|
1682
1756
|
card.appendChild(el("pre", null, JSON.stringify(data.input, null, 2)));
|
|
@@ -1702,6 +1776,100 @@ exports.html = `<!DOCTYPE html>
|
|
|
1702
1776
|
scrollDown();
|
|
1703
1777
|
}
|
|
1704
1778
|
|
|
1779
|
+
function renderAskUser(data) {
|
|
1780
|
+
var input = data.input || {};
|
|
1781
|
+
var questions = Array.isArray(input.questions) ? input.questions : [];
|
|
1782
|
+
var answers = {}; // question text -> selected label(s)
|
|
1783
|
+
|
|
1784
|
+
var card = el("div", "perm");
|
|
1785
|
+
card.appendChild(el("div", "head", "Questions"));
|
|
1786
|
+
|
|
1787
|
+
var qsDiv = el("div", "ask-questions");
|
|
1788
|
+
|
|
1789
|
+
questions.forEach(function (q, qi) {
|
|
1790
|
+
answers[q.question] = q.multiSelect ? [] : null;
|
|
1791
|
+
|
|
1792
|
+
var qDiv = el("div", "ask-q");
|
|
1793
|
+
qDiv.appendChild(el("div", "ask-q-text", q.question));
|
|
1794
|
+
|
|
1795
|
+
var optsDiv = el("div", "ask-opts");
|
|
1796
|
+
|
|
1797
|
+
(q.options || []).forEach(function (opt) {
|
|
1798
|
+
var row = el("div");
|
|
1799
|
+
var btn = el("button", "ask-opt-btn", opt.label);
|
|
1800
|
+
btn.onclick = function () {
|
|
1801
|
+
if (q.multiSelect) {
|
|
1802
|
+
var arr = answers[q.question];
|
|
1803
|
+
var idx = arr.indexOf(opt.label);
|
|
1804
|
+
if (idx >= 0) { arr.splice(idx, 1); btn.classList.remove("selected"); }
|
|
1805
|
+
else { arr.push(opt.label); btn.classList.add("selected"); }
|
|
1806
|
+
} else {
|
|
1807
|
+
answers[q.question] = opt.label;
|
|
1808
|
+
optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
|
|
1809
|
+
btn.classList.add("selected");
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
row.appendChild(btn);
|
|
1813
|
+
if (opt.description) row.appendChild(el("div", "ask-opt-desc", opt.description));
|
|
1814
|
+
optsDiv.appendChild(row);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
var otherInput = el("input");
|
|
1818
|
+
otherInput.type = "text";
|
|
1819
|
+
otherInput.placeholder = "Other\u2026";
|
|
1820
|
+
otherInput.className = "ask-other";
|
|
1821
|
+
otherInput.oninput = function () {
|
|
1822
|
+
var val = otherInput.value.trim();
|
|
1823
|
+
if (q.multiSelect) {
|
|
1824
|
+
// handled on submit
|
|
1825
|
+
} else {
|
|
1826
|
+
if (val) {
|
|
1827
|
+
answers[q.question] = val;
|
|
1828
|
+
optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
|
|
1829
|
+
} else if (!answers[q.question] || typeof answers[q.question] === "string") {
|
|
1830
|
+
answers[q.question] = null;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
optsDiv.appendChild(otherInput);
|
|
1835
|
+
qDiv.appendChild(optsDiv);
|
|
1836
|
+
qsDiv.appendChild(qDiv);
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
card.appendChild(qsDiv);
|
|
1840
|
+
|
|
1841
|
+
var acts = el("div", "acts");
|
|
1842
|
+
var submit = el("button", "btn primary", "Submit");
|
|
1843
|
+
submit.onclick = function () {
|
|
1844
|
+
// Collect "other" text inputs for multi-select questions
|
|
1845
|
+
var qDivs = qsDiv.querySelectorAll(".ask-q");
|
|
1846
|
+
questions.forEach(function (q, qi) {
|
|
1847
|
+
if (q.multiSelect) {
|
|
1848
|
+
var inp = qDivs[qi].querySelector(".ask-other");
|
|
1849
|
+
var val = inp ? inp.value.trim() : "";
|
|
1850
|
+
if (val && answers[q.question].indexOf(val) === -1) answers[q.question].push(val);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
submit.disabled = true;
|
|
1854
|
+
api("/sessions/" + encodeURIComponent(sessionId) + "/permission", {
|
|
1855
|
+
method: "POST",
|
|
1856
|
+
body: { toolUseID: data.toolUseID, answers: answers }
|
|
1857
|
+
}).then(function () {
|
|
1858
|
+
var summary = questions.map(function (q) {
|
|
1859
|
+
var a = answers[q.question];
|
|
1860
|
+
return Array.isArray(a) ? a.join(", ") : (a || "\u2014");
|
|
1861
|
+
}).join(" \u00b7 ");
|
|
1862
|
+
card.replaceWith(el("div", "note", summary));
|
|
1863
|
+
pendingPerms = Math.max(0, pendingPerms - 1);
|
|
1864
|
+
if (!pendingPerms && state === "running") setActivity("Thinking\u2026");
|
|
1865
|
+
}).catch(showError);
|
|
1866
|
+
};
|
|
1867
|
+
acts.appendChild(submit);
|
|
1868
|
+
card.appendChild(acts);
|
|
1869
|
+
$("messages-inner").appendChild(card);
|
|
1870
|
+
scrollDown();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1705
1873
|
function finishTurn() {
|
|
1706
1874
|
closeStream();
|
|
1707
1875
|
liveBubble = null;
|
|
@@ -1870,6 +2038,70 @@ exports.html = `<!DOCTYPE html>
|
|
|
1870
2038
|
});
|
|
1871
2039
|
}
|
|
1872
2040
|
|
|
2041
|
+
// IndexedDB helpers — save the raw audio before the API call so a failed
|
|
2042
|
+
// transcription can be retried without re-recording.
|
|
2043
|
+
var _dictDb = null;
|
|
2044
|
+
function openDictationDb() {
|
|
2045
|
+
if (_dictDb) return Promise.resolve(_dictDb);
|
|
2046
|
+
return new Promise(function (resolve, reject) {
|
|
2047
|
+
var req = indexedDB.open("dictation", 1);
|
|
2048
|
+
req.onupgradeneeded = function () { req.result.createObjectStore("pending"); };
|
|
2049
|
+
req.onsuccess = function () { _dictDb = req.result; resolve(_dictDb); };
|
|
2050
|
+
req.onerror = function () { reject(req.error); };
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
function savePendingAudio(audio, mimeType) {
|
|
2054
|
+
return openDictationDb().then(function (db) {
|
|
2055
|
+
return new Promise(function (resolve, reject) {
|
|
2056
|
+
var tx = db.transaction("pending", "readwrite");
|
|
2057
|
+
tx.objectStore("pending").put({ audio: audio, mimeType: mimeType }, "clip");
|
|
2058
|
+
tx.oncomplete = resolve;
|
|
2059
|
+
tx.onerror = function () { reject(tx.error); };
|
|
2060
|
+
});
|
|
2061
|
+
}).catch(function () {}); // best-effort
|
|
2062
|
+
}
|
|
2063
|
+
function deletePendingAudio() {
|
|
2064
|
+
return openDictationDb().then(function (db) {
|
|
2065
|
+
return new Promise(function (resolve) {
|
|
2066
|
+
var tx = db.transaction("pending", "readwrite");
|
|
2067
|
+
tx.objectStore("pending").delete("clip");
|
|
2068
|
+
tx.oncomplete = resolve;
|
|
2069
|
+
tx.onerror = resolve;
|
|
2070
|
+
});
|
|
2071
|
+
}).catch(function () {});
|
|
2072
|
+
}
|
|
2073
|
+
function loadPendingAudio() {
|
|
2074
|
+
return openDictationDb().then(function (db) {
|
|
2075
|
+
return new Promise(function (resolve) {
|
|
2076
|
+
var req = db.transaction("pending", "readonly").objectStore("pending").get("clip");
|
|
2077
|
+
req.onsuccess = function () { resolve(req.result || null); };
|
|
2078
|
+
req.onerror = function () { resolve(null); };
|
|
2079
|
+
});
|
|
2080
|
+
}).catch(function () { return null; });
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function sendDictation(audio, mimeType, btn, textarea) {
|
|
2084
|
+
return api("/dictate", { method: "POST", body: { audio: audio, mimeType: mimeType } })
|
|
2085
|
+
.then(function (d) {
|
|
2086
|
+
deletePendingAudio();
|
|
2087
|
+
insertDictation(textarea, d.text);
|
|
2088
|
+
}).catch(function (err) {
|
|
2089
|
+
toast(err.message || "Dictation failed", {
|
|
2090
|
+
label: "Retry",
|
|
2091
|
+
fn: function () {
|
|
2092
|
+
loadPendingAudio().then(function (saved) {
|
|
2093
|
+
if (!saved) { toast("No saved clip to retry"); return; }
|
|
2094
|
+
btn.classList.add("busy");
|
|
2095
|
+
sendDictation(saved.audio, saved.mimeType, btn, textarea)
|
|
2096
|
+
.then(function () { btn.classList.remove("busy"); });
|
|
2097
|
+
});
|
|
2098
|
+
},
|
|
2099
|
+
});
|
|
2100
|
+
}).then(function () {
|
|
2101
|
+
btn.classList.remove("busy");
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1873
2105
|
function stopRecording() {
|
|
1874
2106
|
var rec = recording;
|
|
1875
2107
|
if (!rec) return;
|
|
@@ -1887,6 +2119,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1887
2119
|
if (!window.isSecureContext) {
|
|
1888
2120
|
toast("Dictation needs https or localhost"); return;
|
|
1889
2121
|
}
|
|
2122
|
+
deletePendingAudio();
|
|
1890
2123
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
|
|
1891
2124
|
var mime = recordingMime();
|
|
1892
2125
|
var recorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
|
|
@@ -1897,12 +2130,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
1897
2130
|
var blob = new Blob(chunks, { type: type });
|
|
1898
2131
|
if (blob.size < 1000) { btn.classList.remove("busy"); return; } // tapped twice by accident
|
|
1899
2132
|
blobToBase64(blob).then(function (audio) {
|
|
1900
|
-
return
|
|
1901
|
-
|
|
1902
|
-
|
|
2133
|
+
return savePendingAudio(audio, type).then(function () {
|
|
2134
|
+
return sendDictation(audio, type, btn, textarea);
|
|
2135
|
+
});
|
|
1903
2136
|
}).catch(function (err) {
|
|
1904
2137
|
toast(err.message || "Dictation failed");
|
|
1905
|
-
}).then(function () {
|
|
1906
2138
|
btn.classList.remove("busy");
|
|
1907
2139
|
});
|
|
1908
2140
|
};
|
|
@@ -1923,12 +2155,19 @@ exports.html = `<!DOCTYPE html>
|
|
|
1923
2155
|
};
|
|
1924
2156
|
}
|
|
1925
2157
|
|
|
1926
|
-
function toast(text) {
|
|
2158
|
+
function toast(text, action) {
|
|
1927
2159
|
var old = document.querySelector(".toast");
|
|
1928
2160
|
if (old) old.remove();
|
|
1929
2161
|
var t = el("div", "toast", text);
|
|
2162
|
+
if (action) {
|
|
2163
|
+
var btn = document.createElement("button");
|
|
2164
|
+
btn.className = "toast-action";
|
|
2165
|
+
btn.textContent = action.label;
|
|
2166
|
+
btn.onclick = function () { t.remove(); action.fn(); };
|
|
2167
|
+
t.appendChild(btn);
|
|
2168
|
+
}
|
|
1930
2169
|
document.body.appendChild(t);
|
|
1931
|
-
setTimeout(function () { t.remove(); }, 2400);
|
|
2170
|
+
setTimeout(function () { if (t.parentNode) t.remove(); }, action ? 7000 : 2400);
|
|
1932
2171
|
}
|
|
1933
2172
|
|
|
1934
2173
|
function copyText(text) {
|
|
@@ -2122,7 +2361,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
2122
2361
|
|
|
2123
2362
|
var model = field("Model", "optional", el("input"));
|
|
2124
2363
|
model.value = bot ? (bot.model || "") : "";
|
|
2125
|
-
model.placeholder = "claude-
|
|
2364
|
+
model.placeholder = "claude-opus-5[1m]";
|
|
2126
2365
|
|
|
2127
2366
|
var permissionMode = field("Permissions", null, el("select"));
|
|
2128
2367
|
[["ask-permissions", "Ask before each tool"], ["auto-approve", "Auto-approve tools"], ["plan", "Plan only (no edits)"]]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.contextWindowFor = contextWindowFor;
|
|
4
|
+
exports.tokensInContext = tokensInContext;
|
|
5
|
+
exports.modelLabel = modelLabel;
|
|
6
|
+
exports.contextUsage = contextUsage;
|
|
7
|
+
const MILLION = 1000000;
|
|
8
|
+
/** What Claude Code assumes for any model that does not ask for the long window. */
|
|
9
|
+
const DEFAULT_WINDOW = 200000;
|
|
10
|
+
/** The [1m] suffix is how the SDK is told to budget a 1M window; mirror that rule. */
|
|
11
|
+
function contextWindowFor(model) {
|
|
12
|
+
return model?.includes("[1m]") ? MILLION : DEFAULT_WINDOW;
|
|
13
|
+
}
|
|
14
|
+
/** Everything the next request resends, from one message's usage block. */
|
|
15
|
+
function tokensInContext(usage) {
|
|
16
|
+
if (!usage)
|
|
17
|
+
return 0;
|
|
18
|
+
const n = (v) => (typeof v === "number" && isFinite(v) && v > 0 ? v : 0);
|
|
19
|
+
return n(usage.input_tokens)
|
|
20
|
+
+ n(usage.cache_read_input_tokens)
|
|
21
|
+
+ n(usage.cache_creation_input_tokens)
|
|
22
|
+
+ n(usage.output_tokens);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* "claude-opus-5[1m]" -> "Opus 5 (1M context)", "claude-sonnet-4-6" ->
|
|
26
|
+
* "Sonnet 4.6". Derived rather than looked up, so a model released tomorrow
|
|
27
|
+
* still reads properly.
|
|
28
|
+
*/
|
|
29
|
+
function modelLabel(model) {
|
|
30
|
+
if (!model)
|
|
31
|
+
return "";
|
|
32
|
+
const long = model.includes("[1m]");
|
|
33
|
+
const parts = model.replace(/\[1m\]/g, "").replace(/^claude-/, "").split("-").filter(Boolean);
|
|
34
|
+
const family = parts.shift() ?? "";
|
|
35
|
+
const name = family.charAt(0).toUpperCase() + family.slice(1);
|
|
36
|
+
const version = parts.join(".");
|
|
37
|
+
return `${[name, version].filter(Boolean).join(" ")}${long ? " (1M context)" : ""}`;
|
|
38
|
+
}
|
|
39
|
+
function contextUsage(used, model, window) {
|
|
40
|
+
return {
|
|
41
|
+
used,
|
|
42
|
+
window: window && window > 0 ? window : contextWindowFor(model),
|
|
43
|
+
model: model ?? "",
|
|
44
|
+
label: modelLabel(model),
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/dictation.js
CHANGED
|
@@ -21,15 +21,17 @@ const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
|
21
21
|
const CLEANUP_MODEL = "gpt-4o-mini";
|
|
22
22
|
// Roughly two minutes of opus; anything longer is a mistake, not a message.
|
|
23
23
|
const MAX_AUDIO_BYTES = 8 * 1024 * 1024;
|
|
24
|
-
const CLEANUP_PROMPT = `You are
|
|
25
|
-
The user will provide a
|
|
24
|
+
const CLEANUP_PROMPT = `You are a voice transcription cleanup assistant.
|
|
25
|
+
The user will provide a raw voice transcript. Your job is light editing only — preserve their exact words and phrasing as much as possible.
|
|
26
26
|
|
|
27
27
|
Your task:
|
|
28
|
-
1.
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
28
|
+
1. Remove filler words ("uh", "um", "er", "like") and false starts.
|
|
29
|
+
2. If the user spells out a word (e.g., "t-h-r-e-s-h-o-l-d"), replace the previous misspelled or unclear attempt with the correctly spelled word and drop the spelling itself.
|
|
30
|
+
3. Resolve simple self-corrections (e.g., "delete the file, actually remove the folder" → "remove the folder").
|
|
31
|
+
4. Apply light markdown formatting where it clearly fits: wrap code, file names, variable names, and commands in backticks; use a bullet list if the user enumerates multiple items.
|
|
32
|
+
5. Output ONLY the cleaned transcript. No explanations, no quotes, no commentary.
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Do not paraphrase, summarize, reorder, or interpret — just clean up the words they said.`;
|
|
33
35
|
function dictationConfigured() {
|
|
34
36
|
return {
|
|
35
37
|
deepgram: Boolean(process.env.DEEPGRAM_API_KEY),
|
package/dist/jarvis-tools.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.JARVIS_TOOL_NAMES = exports.JARVIS_MCP_NAME = void 0;
|
|
4
4
|
exports.buildJarvisTools = buildJarvisTools;
|
|
5
5
|
const claude_agent_sdk_1 = require("@anthropic-ai/claude-agent-sdk");
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const util_1 = require("util");
|
|
6
8
|
const zod_1 = require("zod");
|
|
7
9
|
const server_common_1 = require("./server-common");
|
|
8
10
|
const bot_store_1 = require("./bot-store");
|
|
@@ -67,6 +69,32 @@ function buildJarvisTools(store, workspaceCwd) {
|
|
|
67
69
|
return fail(err?.message ?? "Could not update the project");
|
|
68
70
|
}
|
|
69
71
|
}),
|
|
72
|
+
(0, claude_agent_sdk_1.tool)("find_repos", "Search the user's GitHub for a repository: their own repos and every organisation they belong to. Use it when the user names a project you do not have and no workspace folder matches, before asking them for a URL — then pass the clone URL to create_project.", {
|
|
73
|
+
query: zod_1.z.string().optional().describe("Words to match against the repo name and description, e.g. 'relay' or 'expo client'. Omit to list everything."),
|
|
74
|
+
owner: zod_1.z.string().optional().describe("Restrict to one user or organisation login. Omit to search the user's account and all their orgs."),
|
|
75
|
+
limit: zod_1.z.number().optional().describe("Maximum repos to return (default 30)"),
|
|
76
|
+
}, async ({ query, owner, limit }) => {
|
|
77
|
+
try {
|
|
78
|
+
const { repos, searched, warnings } = await findRepos({ query, owner, limit: limit ?? 30 });
|
|
79
|
+
const note = warnings.length ? { incomplete_results: warnings, tell_the_user: "Say this to the user; it is something only they can fix." } : {};
|
|
80
|
+
if (!repos.length) {
|
|
81
|
+
return text(JSON.stringify({
|
|
82
|
+
repos: [],
|
|
83
|
+
searched_owners: searched,
|
|
84
|
+
...note,
|
|
85
|
+
next: `Nothing on GitHub matches ${query ? `"${query}"` : "that"}${owner ? ` under ${owner}` : ""}. Report any incomplete_results above, then ask the user for the repo URL.`,
|
|
86
|
+
}, null, 2));
|
|
87
|
+
}
|
|
88
|
+
return text(JSON.stringify({ repos, searched_owners: searched, ...note }, null, 2));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err instanceof GhSetupError) {
|
|
92
|
+
(0, server_common_1.emitEvent)(store, "github_unavailable", { kind: err.kind, message: err.message });
|
|
93
|
+
return fail(`GITHUB IS NOT SET UP ON THIS MACHINE. Tell the user exactly this, including the command, and do not ask them for a repo URL until you have:\n\n${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
return fail(`${err?.message ?? "Could not reach GitHub"}. Tell the user the search failed and why.`);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
70
98
|
(0, claude_agent_sdk_1.tool)("delegate", "Give a project's agent a bounded task and wait for its report, so you can combine it with other things. Blocks until the agent finishes (seconds to many minutes); call it for several projects in parallel when a request spans projects. Not for a request about a single project, however small — that is a handoff, so the user's follow-ups land in the project.", {
|
|
71
99
|
project: zod_1.z.string().describe("Project name or slug"),
|
|
72
100
|
task: zod_1.z.string().describe("The task, self-contained: what to do, what to report back, and any constraints such as 'do not merge'"),
|
|
@@ -121,7 +149,7 @@ function buildJarvisTools(store, workspaceCwd) {
|
|
|
121
149
|
});
|
|
122
150
|
}
|
|
123
151
|
/** Tool names as Claude Code sees them, for the allowed-tools list. */
|
|
124
|
-
exports.JARVIS_TOOL_NAMES = ["list_projects", "read_project", "create_project", "update_project", "delegate", "handoff"]
|
|
152
|
+
exports.JARVIS_TOOL_NAMES = ["list_projects", "read_project", "create_project", "update_project", "find_repos", "delegate", "handoff"]
|
|
125
153
|
.map((t) => `mcp__${exports.JARVIS_MCP_NAME}__${t}`);
|
|
126
154
|
function summary(p) {
|
|
127
155
|
return {
|
|
@@ -141,3 +169,156 @@ function titleFor(text) {
|
|
|
141
169
|
return undefined;
|
|
142
170
|
return line.length > 60 ? line.slice(0, 57).replace(/\s+\S*$/, "") + "…" : line;
|
|
143
171
|
}
|
|
172
|
+
// --- GitHub ---
|
|
173
|
+
// Jarvis has no shell, so `gh` is exposed as one read-only tool rather than as
|
|
174
|
+
// a command line: fixed subcommands, arguments passed as an argv array (never
|
|
175
|
+
// through a shell), and only what is needed to turn a name the user said into
|
|
176
|
+
// a repository to clone.
|
|
177
|
+
//
|
|
178
|
+
// gh lives on the machine, not in the conversation, so when it is missing,
|
|
179
|
+
// logged out or short a scope only the user can fix it. Those cases are
|
|
180
|
+
// separated from ordinary failures and carry the exact command to run, which
|
|
181
|
+
// find_repos tells Jarvis to pass on verbatim rather than quietly falling back
|
|
182
|
+
// to "what is the repo URL?".
|
|
183
|
+
const run = (0, util_1.promisify)(child_process_1.execFile);
|
|
184
|
+
/** A problem with the machine's gh install that the user has to fix. */
|
|
185
|
+
class GhSetupError extends Error {
|
|
186
|
+
constructor(kind, message) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.kind = kind;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const INSTALL_HINT = process.platform === "darwin"
|
|
192
|
+
? "install it with `brew install gh`"
|
|
193
|
+
: process.platform === "win32"
|
|
194
|
+
? "install it with `winget install --id GitHub.cli`"
|
|
195
|
+
: "install it from https://cli.github.com (on Debian/Ubuntu: `sudo apt install gh`)";
|
|
196
|
+
/**
|
|
197
|
+
* Confirms gh is installed and logged in before any real call, so a setup
|
|
198
|
+
* problem is reported once, precisely, instead of as a puzzling empty search.
|
|
199
|
+
* Successful results are cached; a failure is always re-checked, so the user
|
|
200
|
+
* can fix it and retry immediately.
|
|
201
|
+
*/
|
|
202
|
+
let authCache;
|
|
203
|
+
async function ghAuth() {
|
|
204
|
+
if (authCache && Date.now() - authCache.at < 10 * 60 * 1000)
|
|
205
|
+
return authCache;
|
|
206
|
+
let out;
|
|
207
|
+
try {
|
|
208
|
+
const r = await run("gh", ["auth", "status"], { timeout: 20000 });
|
|
209
|
+
out = r.stdout + r.stderr;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (err?.code === "ENOENT") {
|
|
213
|
+
throw new GhSetupError("missing", `The GitHub CLI (gh) is not installed on the machine running Jarvis, so I cannot search GitHub. To fix it, ${INSTALL_HINT}, then run \`gh auth login\`.`);
|
|
214
|
+
}
|
|
215
|
+
const detail = String(err?.stderr || err?.stdout || err?.message || "").trim();
|
|
216
|
+
if (/not logged|no accounts|gh auth login|authentication/i.test(detail) || typeof err?.code === "number") {
|
|
217
|
+
throw new GhSetupError("auth", "The GitHub CLI is installed but not logged in, so I cannot search GitHub. To fix it, run `gh auth login` on the machine running Jarvis (or set a GH_TOKEN with `repo` and `read:org` scopes) and ask me again.");
|
|
218
|
+
}
|
|
219
|
+
throw new GhSetupError("auth", `Could not check the GitHub CLI's login: ${firstLine(detail) || "unknown error"}. Try \`gh auth status\` on the machine running Jarvis.`);
|
|
220
|
+
}
|
|
221
|
+
const login = /account (\S+)/.exec(out)?.[1] ?? "";
|
|
222
|
+
const scopes = Array.from(out.matchAll(/'([^']+)'/g)).map((m) => m[1]);
|
|
223
|
+
if (!login) {
|
|
224
|
+
// gh exited 0 but named no account: a token with no user, or an unexpected format.
|
|
225
|
+
const me = await ghRaw(["api", "user", "--jq", ".login"]).catch(() => "");
|
|
226
|
+
if (!me.trim())
|
|
227
|
+
throw new GhSetupError("auth", "The GitHub CLI is not logged in to an account I can use. Run `gh auth login` on the machine running Jarvis and ask me again.");
|
|
228
|
+
authCache = { at: Date.now(), login: me.trim(), scopes };
|
|
229
|
+
return authCache;
|
|
230
|
+
}
|
|
231
|
+
authCache = { at: Date.now(), login, scopes };
|
|
232
|
+
return authCache;
|
|
233
|
+
}
|
|
234
|
+
/** The logins to search: the account itself plus every org it belongs to. */
|
|
235
|
+
let ownersCache;
|
|
236
|
+
async function accountOwners() {
|
|
237
|
+
if (ownersCache && Date.now() - ownersCache.at < 10 * 60 * 1000)
|
|
238
|
+
return ownersCache;
|
|
239
|
+
const { login, scopes } = await ghAuth();
|
|
240
|
+
const warnings = [];
|
|
241
|
+
let orgs = [];
|
|
242
|
+
try {
|
|
243
|
+
orgs = (await ghRaw(["api", "user/orgs", "--jq", ".[].login"])).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
warnings.push(`I could not list your GitHub organisations, so I only searched your personal repositories (${firstLine(err?.message) || "unknown error"}). Try \`gh auth status\` on the machine running Jarvis.`);
|
|
247
|
+
}
|
|
248
|
+
// Without the read:org scope the orgs endpoint does not fail — it quietly
|
|
249
|
+
// returns only public memberships, so a private org's repos look like they
|
|
250
|
+
// do not exist. Say so whenever the scope is absent.
|
|
251
|
+
if (scopes.length && !scopes.includes("read:org")) {
|
|
252
|
+
warnings.push(`The GitHub CLI's token has no \`read:org\` scope, so I can only see organisations you are a public member of${orgs.length ? ` (${orgs.join(", ")})` : ""} — private ones are invisible to me. To fix it, run \`gh auth refresh -s read:org\` on the machine running Jarvis.`);
|
|
253
|
+
}
|
|
254
|
+
ownersCache = { at: Date.now(), owners: [login, ...orgs], warnings };
|
|
255
|
+
return ownersCache;
|
|
256
|
+
}
|
|
257
|
+
async function findRepos({ query, owner, limit }) {
|
|
258
|
+
const warnings = [];
|
|
259
|
+
let owners;
|
|
260
|
+
if (owner) {
|
|
261
|
+
await ghAuth();
|
|
262
|
+
owners = [owner];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const found = await accountOwners();
|
|
266
|
+
owners = found.owners;
|
|
267
|
+
warnings.push(...found.warnings);
|
|
268
|
+
}
|
|
269
|
+
const perOwner = Math.max(limit, 100);
|
|
270
|
+
const searched = [];
|
|
271
|
+
const lists = await Promise.all(owners.map(async (o) => {
|
|
272
|
+
try {
|
|
273
|
+
const out = await ghRaw(["repo", "list", o, "--limit", String(perOwner), "--json", "nameWithOwner,description,url,isPrivate,primaryLanguage,updatedAt"]);
|
|
274
|
+
searched.push(o);
|
|
275
|
+
return JSON.parse(out);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
// One unreadable org should not sink the whole search, but the user
|
|
279
|
+
// should know their results are incomplete and why.
|
|
280
|
+
warnings.push(`Could not list repositories for "${o}": ${firstLine(err?.message) || "unknown error"}.`);
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
const terms = (query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
285
|
+
const repos = lists
|
|
286
|
+
.flat()
|
|
287
|
+
.map((r) => ({
|
|
288
|
+
nameWithOwner: r.nameWithOwner,
|
|
289
|
+
description: r.description ?? "",
|
|
290
|
+
url: r.url,
|
|
291
|
+
isPrivate: !!r.isPrivate,
|
|
292
|
+
language: r.primaryLanguage?.name ?? null,
|
|
293
|
+
updatedAt: r.updatedAt,
|
|
294
|
+
}))
|
|
295
|
+
.filter((r) => terms.every((t) => `${r.nameWithOwner} ${r.description}`.toLowerCase().includes(t)))
|
|
296
|
+
.sort((a, b) => score(b, terms) - score(a, terms) || b.updatedAt.localeCompare(a.updatedAt))
|
|
297
|
+
.slice(0, limit);
|
|
298
|
+
return { repos, searched, warnings };
|
|
299
|
+
}
|
|
300
|
+
/** A hit in the repo name beats one that only matched the description. */
|
|
301
|
+
function score(r, terms) {
|
|
302
|
+
const name = r.nameWithOwner.split("/")[1]?.toLowerCase() ?? "";
|
|
303
|
+
return terms.filter((t) => name.includes(t)).length;
|
|
304
|
+
}
|
|
305
|
+
/** One gh call. Setup problems are classified; everything else is the caller's to handle. */
|
|
306
|
+
async function ghRaw(args) {
|
|
307
|
+
try {
|
|
308
|
+
const { stdout } = await run("gh", args, { maxBuffer: 8 * 1024 * 1024, timeout: 30000 });
|
|
309
|
+
return stdout;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
if (err?.code === "ENOENT")
|
|
313
|
+
throw new GhSetupError("missing", `The GitHub CLI (gh) is not installed on the machine running Jarvis. To fix it, ${INSTALL_HINT}, then run \`gh auth login\`.`);
|
|
314
|
+
const detail = firstLine(String(err?.stderr || err?.message || ""));
|
|
315
|
+
if (/not logged|gh auth login|bad credentials|401/i.test(detail)) {
|
|
316
|
+
authCache = undefined;
|
|
317
|
+
throw new GhSetupError("auth", "The GitHub CLI's login has expired or was revoked. To fix it, run `gh auth login` on the machine running Jarvis and ask me again.");
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`gh ${args[0]} ${args[1] ?? ""}`.trim() + ` failed: ${detail || "unknown error"}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function firstLine(s) {
|
|
323
|
+
return String(s ?? "").trim().split("\n")[0].trim();
|
|
324
|
+
}
|