@oar-sh/oar 0.9.8 → 0.9.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/CHANGELOG.md +231 -0
- package/README.md +53 -32
- package/bin/oar.js +814 -228
- package/package.json +4 -5
- package/server/README.md +20 -1
- package/server/db-schema.mjs +8 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/public/app/api-client.js +30 -0
- package/server/public/app/attachments-view.js +508 -125
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/bootstrap.js +46 -2
- package/server/public/app/conversation-view.js +110 -25
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/journal-view.js +16 -2
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/pwa-install.js +1 -1
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/router.js +8 -2
- package/server/public/app/socket-handlers.js +6 -0
- package/server/public/app/store.js +6 -0
- package/server/public/app/viewer-chat-gallery.mjs +59 -0
- package/server/public/app/viewer-controls-state.mjs +59 -0
- package/server/public/app/viewer-gallery.mjs +46 -0
- package/server/public/app/viewer-gestures.mjs +105 -0
- package/server/public/app/viewer-history.mjs +43 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/index.html +304 -54
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +5 -0
- package/server/routes/messages-routes.mjs +33 -61
- package/server/routes/sessions-routes.mjs +92 -24
- package/server/server-runtime.mjs +29 -0
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +139 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-history-refresh-service.mjs +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
54
|
-
"@cursor/sdk": "^1.0.
|
|
55
|
-
"@github/copilot-sdk": "^1.0.
|
|
53
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.292",
|
|
54
|
+
"@cursor/sdk": "^1.0.36",
|
|
55
|
+
"@github/copilot-sdk": "^1.0.17",
|
|
56
56
|
"better-sqlite3": "^12.10.0",
|
|
57
57
|
"express": "^4.18.2",
|
|
58
58
|
"qrcode-terminal": "^0.12.0",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"zod": "^4.4.3"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"cloudflared": "^0.7.1",
|
|
67
66
|
"tty-console": "^1.0.4"
|
|
68
67
|
},
|
|
69
68
|
"devDependencies": {
|
package/server/README.md
CHANGED
|
@@ -658,6 +658,24 @@ images, calling the OpenAI Images API directly rather than going through a CLI w
|
|
|
658
658
|
is never silently lost.
|
|
659
659
|
- `POST /api/shared/:token/presence` tracks shared-viewer presence, surfaced as a watcher count.
|
|
660
660
|
|
|
661
|
+
## Pinned messages
|
|
662
|
+
|
|
663
|
+
- `PATCH /api/conversation/:id/message/:messageId/pin` with `{ pinned: boolean }` sets or clears
|
|
664
|
+
`messages.pinned_at`. A conversation holds at most 100 pins; the next one is refused with `409`
|
|
665
|
+
and `code: "pin-limit"`. The answer is
|
|
666
|
+
`{ ok, conversationId, messageId, pinned, pins, pinsRevision }`.
|
|
667
|
+
- `GET /api/conversation/:id` carries `pins`: every pinned message of the conversation in
|
|
668
|
+
conversation order, also those outside the requested history page, each as
|
|
669
|
+
`{ messageId, role, preview, timestamp, pinnedAt, attachmentCount, hiddenFromShares }`.
|
|
670
|
+
`preview` is plain text of at most 160 characters. Each message also carries `pinned`.
|
|
671
|
+
- Every change is broadcast as the socket event `conversation_pins_updated` with
|
|
672
|
+
`{ conversationId, pins, revision }`. `revision` (`pinsRevision` in the HTTP answers) grows with
|
|
673
|
+
every change. A conversation load that left the relay before a change can reach the browser
|
|
674
|
+
after the change's own event; the browser keeps the list with the higher revision.
|
|
675
|
+
- Shared views carry neither `pins` nor `pinned`: pins are the owner's bookmarks.
|
|
676
|
+
- Pins belong to message rows. `/compact` starts a new conversation without them (they stay with
|
|
677
|
+
the archived one); re-importing an imported session keeps them, matched like hidden marks.
|
|
678
|
+
|
|
661
679
|
## Relay Mode Selection
|
|
662
680
|
|
|
663
681
|
The composer also includes a per-message mode picker:
|
|
@@ -973,6 +991,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
973
991
|
| POST | `/api/subagent-run` | (Worker) Register/update a subagent run for the active turn |
|
|
974
992
|
| POST | `/api/conversation/:conversationId/subagent/:subagentRunId/cancel` | Request cancellation of one subagent run (unsupported by the Claude runtime) |
|
|
975
993
|
| PATCH | `/api/conversation/:id/message/:messageId/share-visibility` | Hide or unhide a single message from shared views |
|
|
994
|
+
| PATCH | `/api/conversation/:id/message/:messageId/pin` | `{ pinned }` → pin or unpin a single message; answers with the conversation's pin list and broadcasts `conversation_pins_updated` |
|
|
976
995
|
| POST | `/api/conversation/:id/share` | Create or update the conversation's read-only share token |
|
|
977
996
|
| GET | `/api/shared/:token` | Read-only shared conversation view (hidden messages filtered out) |
|
|
978
997
|
| POST | `/api/shared/:token/presence` | Report shared-viewer presence for the watcher count |
|
|
@@ -1381,7 +1400,7 @@ service).
|
|
|
1381
1400
|
| `cloudflaredTunnel.enabled` | `false` | Legacy alias for mode (`true` => `managed`) |
|
|
1382
1401
|
| `cloudflaredTunnel.required` | `false` | Pause dequeue when the Cloudflare tunnel is disconnected in managed mode |
|
|
1383
1402
|
| `cloudflaredTunnel.token` | — | Tunnel token from the router panel (never logged) |
|
|
1384
|
-
| `cloudflaredTunnel.binary` |
|
|
1403
|
+
| `cloudflaredTunnel.binary` | `cloudflared` | Path or command name of the `cloudflared` you installed (OAR ships none); defaults to `cloudflared` on `PATH`. Without one the tunnel stays off and the status says how to install it |
|
|
1385
1404
|
| `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
|
|
1386
1405
|
| `tunnelMarkerHeaders` | `[]` | Extra edge-injected header names that mark tunnel traffic for the session-worker path guard |
|
|
1387
1406
|
|
package/server/db-schema.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { migrateSteerSettleMarkers } from './migrations/0004-steer-settle-marker
|
|
|
13
13
|
import { migrateRemoteRelays } from './migrations/0005-remote-relays.mjs';
|
|
14
14
|
import { migrateWorkspaceRootSettings } from './migrations/0006-workspace-root-settings.mjs';
|
|
15
15
|
import { migrateClaudeCloud } from './migrations/0007-claude-cloud.mjs';
|
|
16
|
+
import { migrateMessagePins } from './migrations/0008-message-pins.mjs';
|
|
16
17
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
17
18
|
|
|
18
19
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -734,6 +735,13 @@ try {
|
|
|
734
735
|
} catch (error) {
|
|
735
736
|
console.warn(`[claude-cloud] migration 0007 failed; retrying on next boot: ${error?.message || error}`);
|
|
736
737
|
}
|
|
738
|
+
// Pinned messages: one additive column and its partial index. Readers tolerate
|
|
739
|
+
// their absence (no pins), so a failure is logged and retried next boot like 0007.
|
|
740
|
+
try {
|
|
741
|
+
migrateMessagePins(db);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.warn(`[message-pins] migration 0008 failed; retrying on next boot: ${error?.message || error}`);
|
|
744
|
+
}
|
|
737
745
|
|
|
738
746
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
739
747
|
if (runtimeSessionColumns.length) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: pinned messages.
|
|
5
|
+
*
|
|
6
|
+
* 1. `messages.pinned_at` — when the user pinned the message (ISO time); NULL
|
|
7
|
+
* means not pinned. A pin is a property of the message row, like
|
|
8
|
+
* `hidden_from_shares`, so it goes away with the row.
|
|
9
|
+
* 2. `idx_messages_pinned` — a partial index over the pinned rows only: the
|
|
10
|
+
* pin list travels with every conversation load.
|
|
11
|
+
*
|
|
12
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node server/migrations/0008-message-pins.mjs [path/to/copilot.db]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import process from 'process';
|
|
20
|
+
import Database from 'better-sqlite3';
|
|
21
|
+
|
|
22
|
+
function tableExists(db, name) {
|
|
23
|
+
return !!db
|
|
24
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
25
|
+
.get(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function indexExists(db, name) {
|
|
29
|
+
return !!db
|
|
30
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`)
|
|
31
|
+
.get(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function columnNames(db, table) {
|
|
35
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => String(column.name)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function migrateMessagePins(db) {
|
|
39
|
+
if (!tableExists(db, 'messages')) return { applied: false, columnsAdded: 0, indexesAdded: 0 };
|
|
40
|
+
let columnsAdded = 0;
|
|
41
|
+
let indexesAdded = 0;
|
|
42
|
+
if (!columnNames(db, 'messages').has('pinned_at')) {
|
|
43
|
+
db.exec(`ALTER TABLE messages ADD COLUMN pinned_at TEXT`);
|
|
44
|
+
columnsAdded += 1;
|
|
45
|
+
}
|
|
46
|
+
if (!indexExists(db, 'idx_messages_pinned')) {
|
|
47
|
+
db.exec(`
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_messages_pinned
|
|
49
|
+
ON messages(conversation_id, timestamp)
|
|
50
|
+
WHERE pinned_at IS NOT NULL
|
|
51
|
+
`);
|
|
52
|
+
indexesAdded += 1;
|
|
53
|
+
}
|
|
54
|
+
return { applied: columnsAdded + indexesAdded > 0, columnsAdded, indexesAdded };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function defaultDbPath() {
|
|
58
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function migrate(dbPath, options = {}) {
|
|
63
|
+
const db = new Database(dbPath, options);
|
|
64
|
+
try {
|
|
65
|
+
return migrateMessagePins(db);
|
|
66
|
+
} finally {
|
|
67
|
+
db.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
72
|
+
if (isMain) {
|
|
73
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
74
|
+
try {
|
|
75
|
+
const result = migrate(dbPath);
|
|
76
|
+
console.log(`Migration 0008 on ${dbPath}`);
|
|
77
|
+
console.log(result.applied
|
|
78
|
+
? ` message pins: columns added ${result.columnsAdded}, indexes added ${result.indexesAdded}`
|
|
79
|
+
: ' skipped: nothing to change');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`Migration 0008 failed: ${err?.message || err}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -900,6 +900,36 @@ export async function updateMessageShareVisibility(conversationId, messageId, hi
|
|
|
900
900
|
});
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
+
// Pin or unpin one message. Unlike apiFetch this keeps the relay's `error`
|
|
904
|
+
// text on a refusal (the pin limit), so the notice can say why.
|
|
905
|
+
export async function updateMessagePin(conversationId, messageId, pinned) {
|
|
906
|
+
const convId = String(conversationId || '').trim();
|
|
907
|
+
const msgId = String(messageId || '').trim();
|
|
908
|
+
if (!convId || !msgId || !networkRequestsEnabled) return null;
|
|
909
|
+
const url = `/api/conversation/${encodeURIComponent(convId)}/message/${encodeURIComponent(msgId)}/pin`;
|
|
910
|
+
try {
|
|
911
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
912
|
+
signal: requestTimeoutSignal(),
|
|
913
|
+
method: 'PATCH',
|
|
914
|
+
headers: {
|
|
915
|
+
'Content-Type': 'application/json',
|
|
916
|
+
...authHeaders(),
|
|
917
|
+
},
|
|
918
|
+
body: JSON.stringify({ pinned: pinned === true }),
|
|
919
|
+
});
|
|
920
|
+
const result = await response.json().catch(() => null);
|
|
921
|
+
if (!response.ok) {
|
|
922
|
+
console.error('API error', response.status, url);
|
|
923
|
+
return { ok: false, status: response.status, error: String(result?.error || '').trim() };
|
|
924
|
+
}
|
|
925
|
+
noteFetchSuccess();
|
|
926
|
+
return result;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
noteFetchFailure(url, error);
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
903
933
|
export async function updateConversationTitle(id, title) {
|
|
904
934
|
const convId = String(id || '').trim();
|
|
905
935
|
if (!convId) return null;
|