@oar-sh/oar 0.9.10 → 0.9.12
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 +440 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +112 -25
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +217 -11
- package/server/claude-worker/claude-session-worker.mjs +26 -1
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +68 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +46 -6
- package/server/grok-worker/grok-background-tasks.mjs +241 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +264 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +253 -37
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +567 -94
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +1033 -82
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +150 -10
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +138 -20
- package/server/public/app/store.js +121 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +37 -7
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
- package/server/public/app/transcript-jump-bottom.mjs +170 -22
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +159 -0
- package/server/public/index.html +625 -21
- package/server/relay-tools.md +3 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +9 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +433 -68
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +901 -253
- package/server/server-runtime.mjs +392 -99
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +365 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +187 -7
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +231 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +35 -3
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/task-output-text.mjs +737 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: persisted previews.
|
|
5
|
+
*
|
|
6
|
+
* `previews` holds every published preview (a public link to a dev-server port
|
|
7
|
+
* or to a folder the relay serves), so a restarted runtime can restore them and
|
|
8
|
+
* a link handed out before the restart keeps working. One row per token:
|
|
9
|
+
*
|
|
10
|
+
* - `mode` is 'port' or 'static'. A port row has `target_host` and
|
|
11
|
+
* `target_port`, a static row `root_dir` (the folder's realpath).
|
|
12
|
+
* - `conversation_id` is NULL for a preview registered without one.
|
|
13
|
+
* - `created_at` and `last_hit_at` are epoch milliseconds.
|
|
14
|
+
* - `host_boot_id` names the host boot the preview was registered in. A port
|
|
15
|
+
* preview is restored only in the same boot (a dev server does not survive a
|
|
16
|
+
* reboot, and the port may later belong to something else).
|
|
17
|
+
*
|
|
18
|
+
* No foreign key on `conversation_id`: deleting a conversation closes its
|
|
19
|
+
* previews through the registry, and a restore drops rows whose conversation
|
|
20
|
+
* is gone.
|
|
21
|
+
*
|
|
22
|
+
* Additive and idempotent. Safe to run on every boot, and from the command line
|
|
23
|
+
* against a database a running relay has open (one CREATE TABLE in WAL mode).
|
|
24
|
+
* An older build never reads the table.
|
|
25
|
+
*
|
|
26
|
+
* A `previews` table this migration did not create (other columns) is left
|
|
27
|
+
* alone and reported as not compatible; the relay then keeps previews in
|
|
28
|
+
* memory only, it never refuses to start over it.
|
|
29
|
+
*
|
|
30
|
+
* Usage: node server/migrations/0013-previews.mjs [path/to/copilot.db]
|
|
31
|
+
* The file must exist: a wrong path is refused rather than creating an empty
|
|
32
|
+
* database.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import fs from 'fs';
|
|
36
|
+
import { fileURLToPath } from 'url';
|
|
37
|
+
import path from 'path';
|
|
38
|
+
import process from 'process';
|
|
39
|
+
import Database from 'better-sqlite3';
|
|
40
|
+
|
|
41
|
+
function tableExists(db, name) {
|
|
42
|
+
return !!db
|
|
43
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
44
|
+
.get(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const PREVIEWS_COLUMNS = Object.freeze([
|
|
48
|
+
'token', 'mode', 'conversation_id', 'label', 'target_host', 'target_port', 'root_dir',
|
|
49
|
+
'created_at', 'last_hit_at', 'host_boot_id',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Why the `previews` table cannot be used, or '' when it can: every column
|
|
54
|
+
* this build reads and writes is there, and any other column takes a NULL or
|
|
55
|
+
* has a default (an insert would fail otherwise). '' too when the table is
|
|
56
|
+
* missing; the caller checks that separately.
|
|
57
|
+
*/
|
|
58
|
+
export function previewsTableProblem(db) {
|
|
59
|
+
const info = db.prepare(`PRAGMA table_info(previews)`).all();
|
|
60
|
+
if (info.length === 0) return '';
|
|
61
|
+
const names = new Set(info.map((column) => column.name));
|
|
62
|
+
const missing = PREVIEWS_COLUMNS.filter((name) => !names.has(name));
|
|
63
|
+
if (missing.length) return `the previews table lacks column(s) ${missing.join(', ')}`;
|
|
64
|
+
const blocking = info.filter((column) => !PREVIEWS_COLUMNS.includes(column.name)
|
|
65
|
+
&& column.notnull && column.dflt_value === null && !column.pk);
|
|
66
|
+
if (blocking.length) {
|
|
67
|
+
return `the previews table has required column(s) this build does not write: ${blocking.map((column) => column.name).join(', ')}`;
|
|
68
|
+
}
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function migratePreviews(db) {
|
|
73
|
+
let tablesAdded = 0;
|
|
74
|
+
if (!tableExists(db, 'previews')) {
|
|
75
|
+
db.exec(`
|
|
76
|
+
CREATE TABLE previews (
|
|
77
|
+
token TEXT PRIMARY KEY,
|
|
78
|
+
mode TEXT NOT NULL,
|
|
79
|
+
conversation_id TEXT,
|
|
80
|
+
label TEXT NOT NULL,
|
|
81
|
+
target_host TEXT,
|
|
82
|
+
target_port INTEGER,
|
|
83
|
+
root_dir TEXT,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
last_hit_at INTEGER,
|
|
86
|
+
host_boot_id TEXT
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
tablesAdded += 1;
|
|
90
|
+
}
|
|
91
|
+
const problem = previewsTableProblem(db);
|
|
92
|
+
if (problem) return { applied: false, tablesAdded, compatible: false, problem };
|
|
93
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_previews_conversation ON previews(conversation_id)`);
|
|
94
|
+
return { applied: tablesAdded > 0, tablesAdded, compatible: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function defaultDbPath() {
|
|
98
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
99
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function migrate(dbPath, options = {}) {
|
|
103
|
+
if (!fs.existsSync(dbPath)) throw new Error(`no database at ${dbPath}`);
|
|
104
|
+
const db = new Database(dbPath, { fileMustExist: true, ...options });
|
|
105
|
+
try {
|
|
106
|
+
return migratePreviews(db);
|
|
107
|
+
} finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
113
|
+
if (isMain) {
|
|
114
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
115
|
+
try {
|
|
116
|
+
const result = migrate(dbPath);
|
|
117
|
+
console.log(`Migration 0013 on ${dbPath}`);
|
|
118
|
+
if (!result.compatible) {
|
|
119
|
+
console.error(` not compatible: ${result.problem}; the relay will keep previews in memory only`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
console.log(result.applied
|
|
123
|
+
? ` previews: tables added ${result.tablesAdded}`
|
|
124
|
+
: ' skipped: nothing to change');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`Migration 0013 failed: ${err?.message || err}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: session groups in the sidebar.
|
|
5
|
+
*
|
|
6
|
+
* A group is the set of conversations that share a `group_id`. The table
|
|
7
|
+
* `conversation_groups` holds what the set itself needs: a name given by hand
|
|
8
|
+
* (NULL means "shown under the head's title") and the head, the session that
|
|
9
|
+
* created the others with the remote_relay tool (NULL for a group made by
|
|
10
|
+
* hand, or once the head is deleted). A conversation an agent creates on this
|
|
11
|
+
* relay joins its creator's group at bootstrap; everything else is done by
|
|
12
|
+
* hand in the sidebar. Nothing is backfilled.
|
|
13
|
+
*
|
|
14
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
15
|
+
*
|
|
16
|
+
* Usage: node server/migrations/0014-conversation-groups.mjs [path/to/copilot.db]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import process from 'process';
|
|
22
|
+
import Database from 'better-sqlite3';
|
|
23
|
+
|
|
24
|
+
function tableExists(db, name) {
|
|
25
|
+
return !!db
|
|
26
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
27
|
+
.get(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function migrateConversationGroups(db) {
|
|
31
|
+
if (!tableExists(db, 'conversations')) {
|
|
32
|
+
throw new Error('conversations is missing (the base schema has not run)');
|
|
33
|
+
}
|
|
34
|
+
let tablesAdded = 0;
|
|
35
|
+
if (!tableExists(db, 'conversation_groups')) {
|
|
36
|
+
db.exec(`
|
|
37
|
+
CREATE TABLE conversation_groups (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT,
|
|
40
|
+
parent_conversation_id TEXT,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
updated_at TEXT NOT NULL
|
|
43
|
+
)
|
|
44
|
+
`);
|
|
45
|
+
tablesAdded += 1;
|
|
46
|
+
}
|
|
47
|
+
const present = new Set(db.prepare(`PRAGMA table_info(conversations)`).all().map((column) => column.name));
|
|
48
|
+
let columnsAdded = 0;
|
|
49
|
+
if (!present.has('group_id')) {
|
|
50
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN group_id TEXT`);
|
|
51
|
+
columnsAdded += 1;
|
|
52
|
+
}
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_group
|
|
55
|
+
ON conversations(group_id)
|
|
56
|
+
WHERE group_id IS NOT NULL
|
|
57
|
+
`);
|
|
58
|
+
return { applied: columnsAdded > 0 || tablesAdded > 0, columnsAdded, tablesAdded };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultDbPath() {
|
|
62
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function migrate(dbPath, options = {}) {
|
|
67
|
+
const db = new Database(dbPath, options);
|
|
68
|
+
try {
|
|
69
|
+
return migrateConversationGroups(db);
|
|
70
|
+
} finally {
|
|
71
|
+
db.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
76
|
+
if (isMain) {
|
|
77
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
78
|
+
try {
|
|
79
|
+
const result = migrate(dbPath);
|
|
80
|
+
console.log(`Migration 0014 on ${dbPath}`);
|
|
81
|
+
console.log(result.applied
|
|
82
|
+
? ` conversation groups: columns added ${result.columnsAdded}, tables added ${result.tablesAdded}`
|
|
83
|
+
: ' skipped: nothing to change');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`Migration 0014 failed: ${err?.message || err}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: a hand order inside a session group.
|
|
5
|
+
*
|
|
6
|
+
* `group_position` is a member's place in its group once the owner has
|
|
7
|
+
* dragged a row there: the members with one come first, by position, the
|
|
8
|
+
* others after them, newest first. The head keeps NULL (it always leads),
|
|
9
|
+
* and so does every member of a group nobody reordered. A row that leaves
|
|
10
|
+
* its group loses its position.
|
|
11
|
+
*
|
|
12
|
+
* Additive and idempotent. Safe to run on every boot. Rolling back needs no
|
|
13
|
+
* data change: older code ignores the column; a row it moves out of a group
|
|
14
|
+
* keeps a stale place, which the next run here clears.
|
|
15
|
+
*
|
|
16
|
+
* Usage: node server/migrations/0015-conversation-group-position.mjs [path/to/copilot.db]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import process from 'process';
|
|
22
|
+
import Database from 'better-sqlite3';
|
|
23
|
+
|
|
24
|
+
function tableExists(db, name) {
|
|
25
|
+
return !!db
|
|
26
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
27
|
+
.get(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function migrateConversationGroupPosition(db) {
|
|
31
|
+
if (!tableExists(db, 'conversations')) {
|
|
32
|
+
throw new Error('conversations is missing (the base schema has not run)');
|
|
33
|
+
}
|
|
34
|
+
const present = new Set(db.prepare(`PRAGMA table_info(conversations)`).all().map((column) => column.name));
|
|
35
|
+
let columnsAdded = 0;
|
|
36
|
+
if (!present.has('group_position')) {
|
|
37
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN group_position INTEGER`);
|
|
38
|
+
columnsAdded += 1;
|
|
39
|
+
}
|
|
40
|
+
// A place without a group: an older release (which ignores the column)
|
|
41
|
+
// moved the row out of its group after a roll back. Cleared on every run.
|
|
42
|
+
const placesCleared = present.has('group_id')
|
|
43
|
+
? db.prepare(`UPDATE conversations SET group_position = NULL WHERE group_id IS NULL AND group_position IS NOT NULL`).run().changes
|
|
44
|
+
: 0;
|
|
45
|
+
return { applied: columnsAdded > 0 || placesCleared > 0, columnsAdded, placesCleared };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultDbPath() {
|
|
49
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function migrate(dbPath, options = {}) {
|
|
54
|
+
const db = new Database(dbPath, options);
|
|
55
|
+
try {
|
|
56
|
+
return migrateConversationGroupPosition(db);
|
|
57
|
+
} finally {
|
|
58
|
+
db.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
63
|
+
if (isMain) {
|
|
64
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
65
|
+
try {
|
|
66
|
+
const result = migrate(dbPath);
|
|
67
|
+
console.log(`Migration 0015 on ${dbPath}`);
|
|
68
|
+
console.log(result.applied
|
|
69
|
+
? ` group position: columns added ${result.columnsAdded}, stale places cleared ${result.placesCleared}`
|
|
70
|
+
: ' skipped: nothing to change');
|
|
71
|
+
process.exit(0);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`Migration 0015 failed: ${err?.message || err}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: share lineages and the share access log.
|
|
5
|
+
*
|
|
6
|
+
* A share link can be renewed: the conversation gets a new token and the old
|
|
7
|
+
* one stops at once. What the link has been through (its opens) belongs to the
|
|
8
|
+
* share, not to one token, so every row of `conversation_shares` gets a
|
|
9
|
+
* `lineage_id`: the token the share started with, carried over to every
|
|
10
|
+
* renewed token. Existing rows start their own lineage (`lineage_id = token`).
|
|
11
|
+
*
|
|
12
|
+
* `conversation_share_opens` holds one row per open of a shared page:
|
|
13
|
+
*
|
|
14
|
+
* - `lineage_id` names the share; the log survives a renew.
|
|
15
|
+
* - `opened_at` is an ISO timestamp.
|
|
16
|
+
* - `ip` is NULL when the relay does not store viewer addresses (a setting).
|
|
17
|
+
* - `country` is the edge's two-letter country, NULL when unknown.
|
|
18
|
+
* - `viewer_key` is a keyed hash of address and browser, so distinct viewers
|
|
19
|
+
* can be counted when the address itself is not stored.
|
|
20
|
+
*
|
|
21
|
+
* Stopping a share deletes its log; deleting the conversation does too (the
|
|
22
|
+
* foreign key, and the delete route by hand). Nothing else prunes it.
|
|
23
|
+
*
|
|
24
|
+
* Additive and idempotent. Safe to run on every boot. An older build never
|
|
25
|
+
* reads the new table, and a share it creates has no lineage, which this build
|
|
26
|
+
* reads as the share's own token.
|
|
27
|
+
*
|
|
28
|
+
* Usage: node server/migrations/0016-share-access-log.mjs [path/to/copilot.db]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { fileURLToPath } from 'url';
|
|
32
|
+
import path from 'path';
|
|
33
|
+
import process from 'process';
|
|
34
|
+
import Database from 'better-sqlite3';
|
|
35
|
+
|
|
36
|
+
function tableExists(db, name) {
|
|
37
|
+
return !!db
|
|
38
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
39
|
+
.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function migrateShareAccessLog(db) {
|
|
43
|
+
if (!tableExists(db, 'conversation_shares')) {
|
|
44
|
+
throw new Error('conversation_shares is missing (the base schema has not run)');
|
|
45
|
+
}
|
|
46
|
+
let columnsAdded = 0;
|
|
47
|
+
let tablesAdded = 0;
|
|
48
|
+
const present = new Set(db.prepare(`PRAGMA table_info(conversation_shares)`).all().map((column) => column.name));
|
|
49
|
+
if (!present.has('lineage_id')) {
|
|
50
|
+
db.exec(`ALTER TABLE conversation_shares ADD COLUMN lineage_id TEXT`);
|
|
51
|
+
columnsAdded += 1;
|
|
52
|
+
}
|
|
53
|
+
// Every run: a share an older build created after this migration has none.
|
|
54
|
+
const lineagesSet = db.prepare(`UPDATE conversation_shares SET lineage_id = token WHERE lineage_id IS NULL OR lineage_id = ''`).run().changes;
|
|
55
|
+
if (!tableExists(db, 'conversation_share_opens')) {
|
|
56
|
+
db.exec(`
|
|
57
|
+
CREATE TABLE conversation_share_opens (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
lineage_id TEXT NOT NULL,
|
|
60
|
+
conversation_id TEXT NOT NULL,
|
|
61
|
+
opened_at TEXT NOT NULL,
|
|
62
|
+
ip TEXT,
|
|
63
|
+
user_agent TEXT,
|
|
64
|
+
country TEXT,
|
|
65
|
+
viewer_key TEXT NOT NULL,
|
|
66
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
tablesAdded += 1;
|
|
70
|
+
}
|
|
71
|
+
db.exec(`
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_share_opens_lineage
|
|
73
|
+
ON conversation_share_opens(lineage_id, opened_at DESC)
|
|
74
|
+
`);
|
|
75
|
+
db.exec(`
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_share_opens_conversation
|
|
77
|
+
ON conversation_share_opens(conversation_id)
|
|
78
|
+
`);
|
|
79
|
+
return { applied: columnsAdded > 0 || tablesAdded > 0 || lineagesSet > 0, columnsAdded, tablesAdded, lineagesSet };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function defaultDbPath() {
|
|
83
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function migrate(dbPath, options = {}) {
|
|
88
|
+
const db = new Database(dbPath, options);
|
|
89
|
+
try {
|
|
90
|
+
return migrateShareAccessLog(db);
|
|
91
|
+
} finally {
|
|
92
|
+
db.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
97
|
+
if (isMain) {
|
|
98
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
99
|
+
try {
|
|
100
|
+
const result = migrate(dbPath);
|
|
101
|
+
console.log(`Migration 0016 on ${dbPath}`);
|
|
102
|
+
console.log(result.applied
|
|
103
|
+
? ` share access log: columns added ${result.columnsAdded}, tables added ${result.tablesAdded}, lineages set ${result.lineagesSet}`
|
|
104
|
+
: ' skipped: nothing to change');
|
|
105
|
+
process.exit(0);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`Migration 0016 failed: ${err?.message || err}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: two indexes for loading the history of long conversations.
|
|
5
|
+
*
|
|
6
|
+
* - `idx_queue_response_message` on `queue(response_message_id)`: the
|
|
7
|
+
* sub-agent records of a reply are found through the queue row that
|
|
8
|
+
* produced it, one lookup per assistant message on every history page.
|
|
9
|
+
* Without the index each lookup scanned the whole queue.
|
|
10
|
+
* - `idx_messages_conv_order` on `messages(conversation_id, timestamp, id)`:
|
|
11
|
+
* a history page is selected in SQL in conversation order (timestamp, then
|
|
12
|
+
* id); the index serves that order and the check that a conversation's
|
|
13
|
+
* rows can be paged that way, without reading the message rows.
|
|
14
|
+
*
|
|
15
|
+
* Additive and idempotent. Safe to run on every boot. An older build ignores
|
|
16
|
+
* both indexes.
|
|
17
|
+
*
|
|
18
|
+
* Usage: node server/migrations/0017-conversation-history-indexes.mjs [path/to/copilot.db]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import process from 'process';
|
|
24
|
+
import Database from 'better-sqlite3';
|
|
25
|
+
|
|
26
|
+
function tableExists(db, name) {
|
|
27
|
+
return !!db
|
|
28
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
29
|
+
.get(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function indexExists(db, name) {
|
|
33
|
+
return !!db
|
|
34
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`)
|
|
35
|
+
.get(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function columnNames(db, table) {
|
|
39
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function migrateConversationHistoryIndexes(db) {
|
|
43
|
+
if (!tableExists(db, 'messages') || !tableExists(db, 'queue')) {
|
|
44
|
+
throw new Error('messages or queue is missing (the base schema has not run)');
|
|
45
|
+
}
|
|
46
|
+
if (!columnNames(db, 'queue').has('response_message_id')) {
|
|
47
|
+
throw new Error('queue.response_message_id is missing (the base schema has not run)');
|
|
48
|
+
}
|
|
49
|
+
let indexesAdded = 0;
|
|
50
|
+
if (!indexExists(db, 'idx_queue_response_message')) {
|
|
51
|
+
db.exec(`CREATE INDEX idx_queue_response_message ON queue(response_message_id)`);
|
|
52
|
+
indexesAdded += 1;
|
|
53
|
+
}
|
|
54
|
+
if (!indexExists(db, 'idx_messages_conv_order')) {
|
|
55
|
+
db.exec(`CREATE INDEX idx_messages_conv_order ON messages(conversation_id, timestamp, id)`);
|
|
56
|
+
indexesAdded += 1;
|
|
57
|
+
}
|
|
58
|
+
return { applied: indexesAdded > 0, indexesAdded };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultDbPath() {
|
|
62
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function migrate(dbPath, options = {}) {
|
|
67
|
+
const db = new Database(dbPath, options);
|
|
68
|
+
try {
|
|
69
|
+
return migrateConversationHistoryIndexes(db);
|
|
70
|
+
} finally {
|
|
71
|
+
db.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
76
|
+
if (isMain) {
|
|
77
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
78
|
+
try {
|
|
79
|
+
const result = migrate(dbPath);
|
|
80
|
+
console.log(`Migration 0017 on ${dbPath}`);
|
|
81
|
+
console.log(result.applied
|
|
82
|
+
? ` conversation history indexes: added ${result.indexesAdded}`
|
|
83
|
+
: ' skipped: nothing to change');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`Migration 0017 failed: ${err?.message || err}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ⋯ → About OAR: the menu entry's label and the modal (about-view.mjs builds
|
|
2
|
+
// its HTML from GET /api/about).
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
closeSummaryModal,
|
|
6
|
+
escHtml,
|
|
7
|
+
getUpdateState,
|
|
8
|
+
openSummaryModal,
|
|
9
|
+
renderSummaryModalContent,
|
|
10
|
+
setSummaryModalLoading,
|
|
11
|
+
} from './store.js';
|
|
12
|
+
import { apiFetchOrThrow } from './api-client.js';
|
|
13
|
+
import { openSettingsModal } from './settings-modal.js';
|
|
14
|
+
import { aboutMenuLabel, buildAboutModalHtml } from './about-view.mjs';
|
|
15
|
+
|
|
16
|
+
const ABOUT_KIND = 'about';
|
|
17
|
+
const ABOUT_TITLE = 'About OAR';
|
|
18
|
+
|
|
19
|
+
/** The entry carries the running version; /api/status brings it on load. */
|
|
20
|
+
export function syncAboutMenuLabel(version = getUpdateState()?.runningVersion) {
|
|
21
|
+
const button = document.getElementById('chat-menu-about');
|
|
22
|
+
if (button) button.textContent = aboutMenuLabel(version);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function bindAboutModalActions() {
|
|
26
|
+
const body = document.getElementById('summary-modal-body');
|
|
27
|
+
const button = body?.querySelector('[data-about-open-settings]');
|
|
28
|
+
if (!button) return;
|
|
29
|
+
button.addEventListener('click', (event) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
closeSummaryModal();
|
|
32
|
+
openSettingsModal('general');
|
|
33
|
+
document.getElementById('update-auto-check-toggle')?.scrollIntoView?.({ block: 'center' });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadAboutAndRender() {
|
|
38
|
+
const about = await apiFetchOrThrow('/api/about');
|
|
39
|
+
if (!about) throw new Error('The relay is not reachable right now.');
|
|
40
|
+
renderSummaryModalContent({
|
|
41
|
+
title: ABOUT_TITLE,
|
|
42
|
+
subtitle: about.version ? `Version ${about.version}` : '',
|
|
43
|
+
bodyHtml: buildAboutModalHtml(about),
|
|
44
|
+
refresh: loadAboutAndRender,
|
|
45
|
+
kind: ABOUT_KIND,
|
|
46
|
+
});
|
|
47
|
+
bindAboutModalActions();
|
|
48
|
+
syncAboutMenuLabel(about.version);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function showAbout() {
|
|
52
|
+
openSummaryModal({
|
|
53
|
+
title: ABOUT_TITLE,
|
|
54
|
+
subtitle: 'Loading…',
|
|
55
|
+
bodyHtml: '<div class="summary-loading">Reading the relay…</div>',
|
|
56
|
+
refresh: loadAboutAndRender,
|
|
57
|
+
kind: ABOUT_KIND,
|
|
58
|
+
});
|
|
59
|
+
setSummaryModalLoading(true);
|
|
60
|
+
try {
|
|
61
|
+
await loadAboutAndRender();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
renderSummaryModalContent({
|
|
64
|
+
title: ABOUT_TITLE,
|
|
65
|
+
subtitle: 'Unable to load',
|
|
66
|
+
bodyHtml: `<div class="summary-error">${escHtml(error?.message || 'Unknown error')}</div>`,
|
|
67
|
+
refresh: loadAboutAndRender,
|
|
68
|
+
kind: ABOUT_KIND,
|
|
69
|
+
});
|
|
70
|
+
} finally {
|
|
71
|
+
setSummaryModalLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}
|