@oar-sh/oar 0.9.3 → 0.9.4
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/.github/extensions/web-relay/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +67 -0
- package/README.md +62 -13
- package/package.json +5 -2
- package/server/README.md +43 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +92 -25
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +8 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +103 -0
- package/server/public/app/bootstrap.js +21 -6
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-view.js +98 -7
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +4 -1
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +4 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +4 -0
- package/server/public/index.html +257 -8
- package/server/relay-tools.md +5 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/question-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +90 -16
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +80 -29
- package/server/server-runtime.mjs +238 -0
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/worker-runtime/api-client.mjs +112 -22
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripRemotePromptHeader } from './remote-relay-shared.mjs';
|
|
2
|
+
|
|
1
3
|
const RELAY_TOOL_GUIDANCE = [
|
|
2
4
|
'# Relay Tool Guidance',
|
|
3
5
|
'For any user-facing question or clarification, use the ask_user tool so the web relay can render question cards and buttons. Never ask questions in plain assistant text.',
|
|
@@ -118,12 +120,36 @@ const STEER_NOTE_PATTERN = /^\s*\[Sent while you were still working on my previo
|
|
|
118
120
|
// it is matched on its own, and only where it guards a "/".
|
|
119
121
|
const SLASH_COMMAND_GUARD_PATTERN = /^\u200B(?=\s*\/)/;
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
// The hint the relay adds to a turn whose user message mentions a remote relay
|
|
124
|
+
// (docs/plans/2026-09-27-remote-relays.md §5.5). stripAttachmentPromptArtifacts
|
|
125
|
+
// already drops every <system_reminder>; this narrower pattern is for text that
|
|
126
|
+
// did not come through that path (a stored bubble the server already sanitised).
|
|
127
|
+
const REMOTE_RELAY_MENTION_HINT_PATTERN = /<system_reminder>\s*The user mentioned the remote OAR relay[\s\S]*?<\/system_reminder>\s*/gi;
|
|
128
|
+
|
|
129
|
+
function hasRemoteOrigin(origin) {
|
|
130
|
+
return !!origin && typeof origin === 'object';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stripLeadingRelayNotes(text, { origin = null } = {}) {
|
|
134
|
+
const remote = hasRemoteOrigin(origin);
|
|
135
|
+
let stripped = String(text || '');
|
|
136
|
+
// A prompt another relay's agent sent starts with a header line naming it;
|
|
137
|
+
// the bubble's provenance badge carries the same facts, so it is hidden —
|
|
138
|
+
// but only when the message really carries `origin`, never on a lookalike a
|
|
139
|
+
// human typed. A steer note may sit in front of it.
|
|
140
|
+
if (remote) stripped = stripRemotePromptHeader(stripped.trimStart());
|
|
141
|
+
stripped = stripped.replace(STEER_NOTE_PATTERN, '');
|
|
142
|
+
if (remote) stripped = stripRemotePromptHeader(stripped.trimStart());
|
|
143
|
+
stripped = stripped.replace(SLASH_COMMAND_GUARD_PATTERN, '');
|
|
123
144
|
return stripped.trim() ? stripped.trim() : String(text || '').trim();
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
|
|
147
|
+
/**
|
|
148
|
+
* `options.origin`: the message's remote-relay provenance, when it has one.
|
|
149
|
+
* Only then is the remote prompt header line stripped.
|
|
150
|
+
*/
|
|
151
|
+
export function stripRelayPromptContext(text, relayMode = '', options = {}) {
|
|
152
|
+
const origin = options && typeof options === 'object' ? options.origin : null;
|
|
127
153
|
const value = stripAttachmentPromptArtifacts(text)
|
|
128
154
|
.replace(MEDIA_EMBED_BLOCK_PATTERN, '')
|
|
129
155
|
.trim();
|
|
@@ -131,7 +157,24 @@ export function stripRelayPromptContext(text, relayMode = '') {
|
|
|
131
157
|
const patterns = buildPromptPrefixPatterns(relayMode);
|
|
132
158
|
for (const pattern of patterns) {
|
|
133
159
|
const stripped = value.replace(pattern, '').trim();
|
|
134
|
-
if (stripped && stripped !== value) return stripLeadingRelayNotes(stripped);
|
|
160
|
+
if (stripped && stripped !== value) return stripLeadingRelayNotes(stripped, { origin });
|
|
161
|
+
}
|
|
162
|
+
return stripLeadingRelayNotes(value, { origin });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* For a user bubble's text as it is about to render (already sanitised by the
|
|
167
|
+
* relay): drops a stray remote-relay mention hint, and the remote prompt header
|
|
168
|
+
* when the message carries `origin`. Text without either is returned untouched.
|
|
169
|
+
*/
|
|
170
|
+
export function stripRemoteRelayDisplayArtifacts(text, { origin = null } = {}) {
|
|
171
|
+
const value = String(text ?? '');
|
|
172
|
+
let next = value.replace(REMOTE_RELAY_MENTION_HINT_PATTERN, '');
|
|
173
|
+
if (hasRemoteOrigin(origin)) {
|
|
174
|
+
const leading = next.trimStart();
|
|
175
|
+
const withoutHeader = stripRemotePromptHeader(leading);
|
|
176
|
+
if (withoutHeader !== leading) next = withoutHeader;
|
|
135
177
|
}
|
|
136
|
-
|
|
178
|
+
if (next === value) return value;
|
|
179
|
+
return next.trim() ? next.trim() : value.trim();
|
|
137
180
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Provenance of work another relay's agent started here: the badge on a user
|
|
2
|
+
// bubble ("↗ from win-test · “report builder” · claude-sonnet-5", linking back
|
|
3
|
+
// to the source session) and the "↗ win-test" marker on the sidebar row of a
|
|
4
|
+
// conversation such an agent created. Both read the `origin` the relay stores
|
|
5
|
+
// with the message or conversation (docs/plans/2026-09-27-remote-relays.md §4).
|
|
6
|
+
|
|
7
|
+
import { remoteConversationUrl } from './remote-relay-shared.mjs';
|
|
8
|
+
|
|
9
|
+
function escapeHtml(value) {
|
|
10
|
+
return String(value ?? '')
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function oneLine(value) {
|
|
19
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The display fields of an `origin`, or null when it names no relay. */
|
|
23
|
+
export function normalizeRemoteOrigin(raw) {
|
|
24
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
25
|
+
const relayName = oneLine(raw.relayName);
|
|
26
|
+
const relayId = oneLine(raw.relayId);
|
|
27
|
+
if (!relayName && !relayId) return null;
|
|
28
|
+
const relayUrl = oneLine(raw.relayUrl);
|
|
29
|
+
return {
|
|
30
|
+
relayName: relayName || relayId,
|
|
31
|
+
relayUrl: /^https?:\/\//i.test(relayUrl) ? relayUrl : '',
|
|
32
|
+
conversationId: oneLine(raw.conversationId),
|
|
33
|
+
conversationTitle: oneLine(raw.conversationTitle),
|
|
34
|
+
model: oneLine(raw.model),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function remoteOriginBadgeText(origin) {
|
|
39
|
+
const normalized = normalizeRemoteOrigin(origin);
|
|
40
|
+
if (!normalized) return '';
|
|
41
|
+
const parts = [`↗ from ${normalized.relayName}`];
|
|
42
|
+
if (normalized.conversationTitle) parts.push(`“${normalized.conversationTitle}”`);
|
|
43
|
+
if (normalized.model) parts.push(normalized.model);
|
|
44
|
+
return parts.join(' · ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `<relayUrl>/?conv=<conversationId>`, or '' when the origin has no URL. */
|
|
48
|
+
export function remoteOriginLink(origin) {
|
|
49
|
+
const normalized = normalizeRemoteOrigin(origin);
|
|
50
|
+
if (!normalized?.relayUrl) return '';
|
|
51
|
+
return remoteConversationUrl(normalized.relayUrl, normalized.conversationId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The badge row that sits above a user bubble's text. `linkable: false` (the
|
|
56
|
+
* shared read-only view) renders it without the link to the other relay.
|
|
57
|
+
*/
|
|
58
|
+
export function renderRemoteOriginBadgeHtml(origin, { linkable = true } = {}) {
|
|
59
|
+
const normalized = normalizeRemoteOrigin(origin);
|
|
60
|
+
if (!normalized) return '';
|
|
61
|
+
const label = escapeHtml(remoteOriginBadgeText(normalized));
|
|
62
|
+
const href = linkable ? remoteOriginLink(normalized) : '';
|
|
63
|
+
const name = escapeHtml(normalized.relayName);
|
|
64
|
+
if (href) {
|
|
65
|
+
return `<div class="msg-origin"><a class="msg-origin-badge" href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Sent by an agent on relay ${name}. Opens its session there.">${label}</a></div>`;
|
|
66
|
+
}
|
|
67
|
+
return `<div class="msg-origin"><span class="msg-origin-badge" title="Sent by an agent on relay ${name}.">${label}</span></div>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The sidebar marker for a conversation another relay's agent created. */
|
|
71
|
+
export function renderConversationOriginMarkerHtml(conversation) {
|
|
72
|
+
const normalized = normalizeRemoteOrigin(conversation?.origin);
|
|
73
|
+
if (!normalized) return '';
|
|
74
|
+
const name = escapeHtml(normalized.relayName);
|
|
75
|
+
return `<span class="conv-origin-marker" title="Started by an agent on relay ${name}">↗ ${name}</span>`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Browser mirror of the pieces of shared/remote-relay-contract.mjs and
|
|
2
|
+
// shared/remote-relay-mentions.mjs the web UI needs. The browser cannot import
|
|
3
|
+
// shared/ (only server/public is served), so these are hand-kept copies;
|
|
4
|
+
// remote-relay-shared.test.mjs asserts they behave exactly like the originals.
|
|
5
|
+
|
|
6
|
+
export const REMOTE_RELAY_PERMISSIONS = Object.freeze(['read', 'prompt', 'full']);
|
|
7
|
+
export const REMOTE_RELAY_DEFAULT_PERMISSION = 'full';
|
|
8
|
+
export const REMOTE_RELAY_SOCKET_EVENT = 'remote_relays_updated';
|
|
9
|
+
|
|
10
|
+
// The header line a relay puts at the top of a prompt it forwards. The UI
|
|
11
|
+
// hides it on bubbles that carry `origin` (the badge shows the same facts).
|
|
12
|
+
export const REMOTE_PROMPT_HEADER_PATTERN = /^\[Remote prompt from an agent on relay "[^"\r\n]*"[^\r\n]*\](?:\r?\n){1,2}/;
|
|
13
|
+
|
|
14
|
+
const NAME_CHAR = /[a-z0-9._-]/;
|
|
15
|
+
|
|
16
|
+
function toText(value) {
|
|
17
|
+
return String(value ?? '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hostOf(url) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(String(url)).hostname.toLowerCase();
|
|
23
|
+
} catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeRemoteRelayPermission(value) {
|
|
29
|
+
const permission = toText(value).toLowerCase();
|
|
30
|
+
return REMOTE_RELAY_PERMISSIONS.includes(permission) ? permission : REMOTE_RELAY_DEFAULT_PERMISSION;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function stripRemotePromptHeader(text) {
|
|
34
|
+
return String(text ?? '').replace(REMOTE_PROMPT_HEADER_PATTERN, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Link that opens one conversation in a relay's web UI. */
|
|
38
|
+
export function remoteConversationUrl(baseUrl, conversationId) {
|
|
39
|
+
const base = toText(baseUrl).replace(/\/+$/, '');
|
|
40
|
+
const id = toText(conversationId);
|
|
41
|
+
if (!base) return '';
|
|
42
|
+
return id ? `${base}/?conv=${encodeURIComponent(id)}` : `${base}/`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** localhost (and *.localhost), an IPv4 literal, or an IPv6 literal (bracketed as URL hosts are). */
|
|
46
|
+
export function isAddressHost(host) {
|
|
47
|
+
const value = toText(host).toLowerCase();
|
|
48
|
+
if (!value) return false;
|
|
49
|
+
if (value === 'localhost' || value.endsWith('.localhost')) return true;
|
|
50
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(value)) return true;
|
|
51
|
+
return value.startsWith('[') || value.includes(':');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The lowercase aliases a relay can be mentioned by: its name and its URL host, unless that is an address. */
|
|
55
|
+
export function remoteRelayAliases(relay) {
|
|
56
|
+
const aliases = new Set();
|
|
57
|
+
const name = toText(relay?.name).toLowerCase();
|
|
58
|
+
if (name) aliases.add(name);
|
|
59
|
+
const host = hostOf(relay?.url);
|
|
60
|
+
if (host && !isAddressHost(host)) aliases.add(host);
|
|
61
|
+
return [...aliases];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* For the composer: the partial `@word` token ending at `caret`, or null.
|
|
66
|
+
* `{ start, end, query }` where start is the index of the `@`.
|
|
67
|
+
*/
|
|
68
|
+
export function mentionQueryAt(text, caret) {
|
|
69
|
+
const value = String(text ?? '');
|
|
70
|
+
const end = Number.isInteger(caret) ? Math.min(Math.max(caret, 0), value.length) : value.length;
|
|
71
|
+
let start = end;
|
|
72
|
+
while (start > 0 && NAME_CHAR.test(value[start - 1].toLowerCase())) start -= 1;
|
|
73
|
+
if (start === 0 || value[start - 1] !== '@') return null;
|
|
74
|
+
const at = start - 1;
|
|
75
|
+
if (at > 0 && !/\s|[([{"'`,;]/.test(value[at - 1])) return null;
|
|
76
|
+
return { start: at, end, query: value.slice(start, end).toLowerCase() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** How far the mention token that starts at `from` runs (the rest of a word the caret sits in). */
|
|
80
|
+
export function mentionTokenEnd(text, from) {
|
|
81
|
+
const value = String(text ?? '');
|
|
82
|
+
let end = Math.min(Math.max(Number(from) || 0, 0), value.length);
|
|
83
|
+
while (end < value.length && NAME_CHAR.test(value[end].toLowerCase())) end += 1;
|
|
84
|
+
return end;
|
|
85
|
+
}
|