oharness 0.7.0 → 0.8.0
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/browser-host.d.ts +10 -0
- package/dist/bin/browser-host.js +11 -0
- package/dist/bin/browser-host.js.map +1 -0
- package/dist/browser-host/main.d.ts +2 -0
- package/dist/browser-host/main.js +113 -0
- package/dist/browser-host/main.js.map +1 -0
- package/dist/ui/commands.d.ts +25 -0
- package/dist/ui/commands.js +64 -0
- package/dist/ui/commands.js.map +1 -0
- package/dist/ui/main.js +2 -23
- package/dist/ui/main.js.map +1 -1
- package/dist/ui/render.d.ts +12 -0
- package/dist/ui/render.js +23 -3
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/tui/app.d.ts +44 -0
- package/dist/ui/tui/app.js +191 -4
- package/dist/ui/tui/app.js.map +1 -1
- package/dist/ui/tui/layout.d.ts +13 -0
- package/dist/ui/tui/layout.js +64 -2
- package/dist/ui/tui/layout.js.map +1 -1
- package/extension/README.md +58 -0
- package/extension/_locales/de/messages.json +83 -0
- package/extension/_locales/en/messages.json +83 -0
- package/extension/_locales/fr/messages.json +83 -0
- package/extension/_locales/ja/messages.json +83 -0
- package/extension/_locales/ko/messages.json +83 -0
- package/extension/_locales/zh_CN/messages.json +83 -0
- package/extension/background.js +380 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +39 -0
- package/extension/popup.css +142 -0
- package/extension/popup.html +43 -0
- package/extension/popup.js +101 -0
- package/extension/sidepanel.css +123 -0
- package/extension/sidepanel.html +30 -0
- package/extension/sidepanel.js +126 -0
- package/package.json +8 -6
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toolbar popup: whether OHarness can reach this browser, which tabs it is
|
|
3
|
+
* driving, and a way to take them all back at once.
|
|
4
|
+
*
|
|
5
|
+
* Everything shown comes from the background worker; this page holds no state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const t = (key, ...subs) => chrome.i18n.getMessage(key, subs) || key;
|
|
9
|
+
|
|
10
|
+
for (const node of document.querySelectorAll("[data-i18n]")) {
|
|
11
|
+
node.textContent = t(node.dataset.i18n);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const $ = (id) => document.getElementById(id);
|
|
15
|
+
|
|
16
|
+
function render(state) {
|
|
17
|
+
const driving = state.tabs.length > 0;
|
|
18
|
+
let mode = "missing";
|
|
19
|
+
if (state.connected) mode = driving ? "driving" : state.clients > 0 ? "ready" : "waiting";
|
|
20
|
+
|
|
21
|
+
$("status").dataset.state = mode;
|
|
22
|
+
$("headline").textContent = t(`state_${mode}`);
|
|
23
|
+
$("detail").textContent =
|
|
24
|
+
mode === "driving" ? t("detailDriving", String(state.tabs.length))
|
|
25
|
+
: mode === "ready" ? t("detailReady", String(state.clients))
|
|
26
|
+
: mode === "waiting" ? t("detailWaiting")
|
|
27
|
+
: t("detailMissing");
|
|
28
|
+
|
|
29
|
+
$("setup").hidden = mode !== "missing";
|
|
30
|
+
$("error").textContent = state.error ?? "";
|
|
31
|
+
|
|
32
|
+
$("driving").hidden = !driving;
|
|
33
|
+
// One heading per conversation, in the colour of its tab group.
|
|
34
|
+
const byGroup = new Map();
|
|
35
|
+
for (const tab of state.tabs) {
|
|
36
|
+
const key = tab.group ?? "";
|
|
37
|
+
if (!byGroup.has(key)) byGroup.set(key, { color: tab.color, tabs: [] });
|
|
38
|
+
byGroup.get(key).tabs.push(tab);
|
|
39
|
+
}
|
|
40
|
+
const list = $("tabs");
|
|
41
|
+
list.replaceChildren(
|
|
42
|
+
...[...byGroup].flatMap(([name, group]) => {
|
|
43
|
+
const heading = document.createElement("li");
|
|
44
|
+
heading.className = "group";
|
|
45
|
+
const swatch = document.createElement("span");
|
|
46
|
+
swatch.className = "swatch";
|
|
47
|
+
swatch.style.background = GROUP_COLORS[group.color] ?? "var(--idle)";
|
|
48
|
+
heading.append(swatch, document.createTextNode(name || t("ungrouped")));
|
|
49
|
+
return [heading, ...group.tabs.map(tabRow)];
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Chrome's tab group colours, as Chrome draws them. */
|
|
55
|
+
const GROUP_COLORS = {
|
|
56
|
+
grey: "#5f6368",
|
|
57
|
+
blue: "#1a73e8",
|
|
58
|
+
red: "#d93025",
|
|
59
|
+
yellow: "#f9ab00",
|
|
60
|
+
green: "#188038",
|
|
61
|
+
pink: "#d01884",
|
|
62
|
+
purple: "#a142f4",
|
|
63
|
+
cyan: "#007b83",
|
|
64
|
+
orange: "#fa903e",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function tabRow(tab) {
|
|
68
|
+
const item = document.createElement("li");
|
|
69
|
+
const button = document.createElement("button");
|
|
70
|
+
button.className = "tab";
|
|
71
|
+
const title = document.createElement("span");
|
|
72
|
+
title.textContent = tab.title || tab.url;
|
|
73
|
+
const url = document.createElement("span");
|
|
74
|
+
url.className = "url";
|
|
75
|
+
url.textContent = tab.url;
|
|
76
|
+
button.append(title, url);
|
|
77
|
+
button.addEventListener("click", () => {
|
|
78
|
+
void chrome.tabs.update(tab.tabId, { active: true });
|
|
79
|
+
void chrome.tabs.get(tab.tabId).then((found) => chrome.windows.update(found.windowId, { focused: true }));
|
|
80
|
+
});
|
|
81
|
+
item.append(button);
|
|
82
|
+
return item;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function ask(type) {
|
|
86
|
+
const state = await chrome.runtime.sendMessage({ type });
|
|
87
|
+
if (state) render(state);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$("release").addEventListener("click", () => void ask("release"));
|
|
91
|
+
$("retry").addEventListener("click", () => void ask("reconnect"));
|
|
92
|
+
// Opened from here because Chrome only opens a side panel in answer to a click.
|
|
93
|
+
$("panel").addEventListener("click", async () => {
|
|
94
|
+
const win = await chrome.windows.getCurrent();
|
|
95
|
+
await chrome.sidePanel.open({ windowId: win.id });
|
|
96
|
+
window.close();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
void ask("status");
|
|
100
|
+
// The popup stays open while the agent works; keep it current.
|
|
101
|
+
setInterval(() => void ask("status"), 1500);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* The app's palette (packages/server/src/theme.ts), like the popup. */
|
|
3
|
+
--bg: #ffffff;
|
|
4
|
+
--fill: #f7f7f8;
|
|
5
|
+
--fill-strong: #00000012;
|
|
6
|
+
--text: #1a1a1a;
|
|
7
|
+
--muted: #6b7280;
|
|
8
|
+
--accent: #111827;
|
|
9
|
+
--accent-text: #ffffff;
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #0e1013;
|
|
16
|
+
--fill: #15171c;
|
|
17
|
+
--fill-strong: #ffffff1a;
|
|
18
|
+
--text: #e6e6e6;
|
|
19
|
+
--muted: #8b93a1;
|
|
20
|
+
--accent: #e5e7eb;
|
|
21
|
+
--accent-text: #0e1013;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
|
|
27
|
+
html, body { height: 100%; }
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
margin: 0;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
color: var(--text);
|
|
35
|
+
font: 13px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* A thin strip above the page: which OHarness, and the one thing the panel
|
|
39
|
+
can do that the page cannot — hand it the tab in front of you. */
|
|
40
|
+
#bar {
|
|
41
|
+
flex: none;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
height: 38px;
|
|
46
|
+
padding: 0 8px 0 12px;
|
|
47
|
+
background: var(--fill);
|
|
48
|
+
}
|
|
49
|
+
#bar[hidden] { display: none; }
|
|
50
|
+
|
|
51
|
+
#name { flex: 1; min-width: 0; font-weight: 600; font-size: 12.5px; }
|
|
52
|
+
#server:not([hidden]) + #name { display: none; }
|
|
53
|
+
|
|
54
|
+
#server {
|
|
55
|
+
flex: 1;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
height: 26px;
|
|
58
|
+
padding: 0 6px;
|
|
59
|
+
border: 0;
|
|
60
|
+
border-radius: 7px;
|
|
61
|
+
background: var(--fill-strong);
|
|
62
|
+
color: var(--text);
|
|
63
|
+
font: inherit;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
}
|
|
66
|
+
#server:focus-visible { outline: none; filter: brightness(1.05); }
|
|
67
|
+
|
|
68
|
+
#adopt {
|
|
69
|
+
flex: none;
|
|
70
|
+
height: 26px;
|
|
71
|
+
padding: 0 10px;
|
|
72
|
+
border: 0;
|
|
73
|
+
border-radius: 7px;
|
|
74
|
+
background: var(--accent);
|
|
75
|
+
color: var(--accent-text);
|
|
76
|
+
font: inherit;
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
#adopt:disabled { opacity: .35; cursor: default; }
|
|
82
|
+
#adopt:focus-visible { outline: none; filter: brightness(1.1); }
|
|
83
|
+
|
|
84
|
+
#frame { flex: 1; width: 100%; border: 0; background: var(--bg); }
|
|
85
|
+
#frame[hidden] { display: none; }
|
|
86
|
+
|
|
87
|
+
#empty {
|
|
88
|
+
flex: 1;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
padding: 24px;
|
|
95
|
+
text-align: center;
|
|
96
|
+
}
|
|
97
|
+
#empty[hidden] { display: none; }
|
|
98
|
+
#empty h1 { margin: 6px 0 0; font-size: 15px; }
|
|
99
|
+
#empty p { margin: 0; max-width: 280px; color: var(--muted); }
|
|
100
|
+
#empty ol { margin: 0; padding-left: 18px; max-width: 280px; text-align: left; color: var(--muted); font-size: 12px; }
|
|
101
|
+
#empty ol[hidden] { display: none; }
|
|
102
|
+
#empty code {
|
|
103
|
+
display: inline-block;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
padding: 1px 5px;
|
|
106
|
+
border-radius: 5px;
|
|
107
|
+
background: var(--fill-strong);
|
|
108
|
+
font: 11.5px ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#retry {
|
|
112
|
+
margin-top: 4px;
|
|
113
|
+
height: 30px;
|
|
114
|
+
padding: 0 14px;
|
|
115
|
+
border: 0;
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
background: var(--fill-strong);
|
|
118
|
+
color: var(--text);
|
|
119
|
+
font: inherit;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
#help { color: var(--muted); font-size: 12px; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<link rel="stylesheet" href="sidepanel.css">
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<header id="bar" hidden>
|
|
9
|
+
<select id="server" hidden></select>
|
|
10
|
+
<span id="name">OHarness</span>
|
|
11
|
+
<button id="adopt" type="button" data-i18n="panelAdopt"></button>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<iframe id="frame" hidden allow="clipboard-read; clipboard-write"></iframe>
|
|
15
|
+
|
|
16
|
+
<section id="empty" hidden>
|
|
17
|
+
<img src="icons/icon-48.png" width="40" height="40" alt="">
|
|
18
|
+
<h1 id="empty-title"></h1>
|
|
19
|
+
<p id="empty-body"></p>
|
|
20
|
+
<ol id="empty-steps" hidden>
|
|
21
|
+
<li data-i18n="setupDesktop"></li>
|
|
22
|
+
<li><span data-i18n="setupCli"></span> <code>oharness-browser-host install</code></li>
|
|
23
|
+
</ol>
|
|
24
|
+
<button id="retry" type="button" data-i18n="retry"></button>
|
|
25
|
+
<a id="help" href="https://oharness.ai/extension" target="_blank" rel="noopener" data-i18n="panelHelp"></a>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<script src="sidepanel.js"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The side panel: OHarness's own page, beside the page you are on.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here is a second chat. The panel frames the page the local OHarness
|
|
5
|
+
* already serves — the desktop app's, or `oharness web`'s — so every
|
|
6
|
+
* conversation, approval and setting is the same one you would see there.
|
|
7
|
+
* What the panel adds is what only the browser knows: which tab you are on,
|
|
8
|
+
* so moving to a conversation's tab group shows that conversation, and the
|
|
9
|
+
* tab in front of you can be handed to it.
|
|
10
|
+
*
|
|
11
|
+
* Where the page is comes from the background worker, which hears it from the
|
|
12
|
+
* native host, which heard it from the server. The page's token travels with
|
|
13
|
+
* it and stays in memory; the server only lets this extension frame it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const t = (key, ...subs) => chrome.i18n.getMessage(key, subs) || key;
|
|
17
|
+
for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
|
|
18
|
+
|
|
19
|
+
const $ = (id) => document.getElementById(id);
|
|
20
|
+
const frame = $("frame");
|
|
21
|
+
const picker = $("server");
|
|
22
|
+
const adopt = $("adopt");
|
|
23
|
+
|
|
24
|
+
/** The server the frame shows, by URL. */
|
|
25
|
+
let shown = null;
|
|
26
|
+
let servers = [];
|
|
27
|
+
|
|
28
|
+
/** The desktop app first: it is the one a person leaves open. */
|
|
29
|
+
function preferred(list) {
|
|
30
|
+
return list.find((server) => server.kind === "desktop") ?? list[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function render(state) {
|
|
34
|
+
servers = state.servers ?? [];
|
|
35
|
+
if (!state.connected || servers.length === 0) {
|
|
36
|
+
shown = null;
|
|
37
|
+
frame.hidden = true;
|
|
38
|
+
frame.removeAttribute("src");
|
|
39
|
+
$("bar").hidden = true;
|
|
40
|
+
$("empty").hidden = false;
|
|
41
|
+
$("empty-title").textContent = t(state.connected ? "panelNoApp" : "state_missing");
|
|
42
|
+
$("empty-body").textContent = t(state.connected ? "panelNoAppBody" : "detailMissing");
|
|
43
|
+
$("empty-steps").hidden = state.connected;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Keep what is shown while it is still there; otherwise the preferred one.
|
|
48
|
+
const next = servers.find((server) => server.url === shown) ?? preferred(servers);
|
|
49
|
+
picker.hidden = servers.length < 2;
|
|
50
|
+
picker.replaceChildren(
|
|
51
|
+
...servers.map((server) => {
|
|
52
|
+
const option = document.createElement("option");
|
|
53
|
+
option.value = server.url;
|
|
54
|
+
option.textContent = `${t(server.kind === "desktop" ? "panelDesktop" : "panelWeb")} · ${new URL(server.url).port}`;
|
|
55
|
+
option.selected = server === next;
|
|
56
|
+
return option;
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
$("bar").hidden = false;
|
|
60
|
+
$("empty").hidden = true;
|
|
61
|
+
if (next.url !== shown) show(next);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function show(server) {
|
|
65
|
+
shown = server.url;
|
|
66
|
+
frame.hidden = false;
|
|
67
|
+
frame.src = `${server.url}/?token=${encodeURIComponent(server.token)}&embed=sidepanel`;
|
|
68
|
+
void paintAdopt();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Say something to the framed page, and only to it. */
|
|
72
|
+
function tellPage(message) {
|
|
73
|
+
if (!shown || frame.hidden) return;
|
|
74
|
+
frame.contentWindow?.postMessage(message, new URL(shown).origin);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* "Use this tab" is for a page the agent could work on: an ordinary web page,
|
|
79
|
+
* not a browser page and not OHarness itself.
|
|
80
|
+
*/
|
|
81
|
+
async function paintAdopt() {
|
|
82
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
83
|
+
const url = tab?.url ?? "";
|
|
84
|
+
const usable = /^https?:/i.test(url) && !servers.some((server) => url.startsWith(server.url));
|
|
85
|
+
adopt.disabled = !usable;
|
|
86
|
+
adopt.title = usable ? t("panelAdoptHint", new URL(url).host) : t("panelAdoptNone");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
adopt.addEventListener("click", async () => {
|
|
90
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
91
|
+
if (!tab || typeof tab.id !== "number") return;
|
|
92
|
+
adopt.disabled = true;
|
|
93
|
+
tellPage({ type: "adopt-tab", tabId: tab.id });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
picker.addEventListener("change", () => {
|
|
97
|
+
const server = servers.find((entry) => entry.url === picker.value);
|
|
98
|
+
if (server) show(server);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
$("retry").addEventListener("click", async () => {
|
|
102
|
+
render((await chrome.runtime.sendMessage({ type: "reconnect" })) ?? {});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// From the framed page: what it did with a tab it was handed.
|
|
106
|
+
window.addEventListener("message", (event) => {
|
|
107
|
+
if (!shown || event.source !== frame.contentWindow || event.origin !== new URL(shown).origin) return;
|
|
108
|
+
if (event.data?.type === "adopted") void paintAdopt();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// From the background worker: servers coming and going, and the person moving
|
|
112
|
+
// into a conversation's tab group.
|
|
113
|
+
chrome.runtime.onMessage.addListener((message, sender) => {
|
|
114
|
+
if (sender.id !== chrome.runtime.id) return;
|
|
115
|
+
if (message?.type === "servers") render({ connected: message.connected, servers: message.servers });
|
|
116
|
+
if (message?.type === "focus-session" && typeof message.id === "string") {
|
|
117
|
+
tellPage({ type: "open-session", id: message.id, ...(message.cwd ? { cwd: message.cwd } : {}) });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
chrome.tabs.onActivated.addListener(() => void paintAdopt());
|
|
122
|
+
chrome.tabs.onUpdated.addListener((_id, change) => {
|
|
123
|
+
if (change.url || change.status === "complete") void paintAdopt();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
void chrome.runtime.sendMessage({ type: "servers" }).then((state) => render(state ?? {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A model-agnostic agent harness: swappable providers, tool calling, MCP, subagents, compaction, hooks and permissions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"oharness": "./dist/ui/main.js",
|
|
32
32
|
"oharness-headless": "./dist/headless/main.js",
|
|
33
33
|
"oharness-web": "./dist/bin/web.js",
|
|
34
|
-
"oharness-sync": "./dist/bin/sync.js"
|
|
34
|
+
"oharness-sync": "./dist/bin/sync.js",
|
|
35
|
+
"oharness-browser-host": "./dist/bin/browser-host.js"
|
|
35
36
|
},
|
|
36
37
|
"main": "./dist/index.js",
|
|
37
38
|
"types": "./dist/index.d.ts",
|
|
@@ -54,15 +55,16 @@
|
|
|
54
55
|
},
|
|
55
56
|
"files": [
|
|
56
57
|
"dist",
|
|
57
|
-
"README.md"
|
|
58
|
+
"README.md",
|
|
59
|
+
"extension"
|
|
58
60
|
],
|
|
59
61
|
"scripts": {
|
|
60
62
|
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
61
63
|
},
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"@oharness/engine": "0.
|
|
64
|
-
"@oharness/server": "0.
|
|
65
|
-
"@oharness/sync": "0.
|
|
65
|
+
"@oharness/engine": "0.8.0",
|
|
66
|
+
"@oharness/server": "0.8.0",
|
|
67
|
+
"@oharness/sync": "0.8.0",
|
|
66
68
|
"ink": "^7.1.1",
|
|
67
69
|
"react": "^19.2.8"
|
|
68
70
|
}
|