@mirrowel/opencode-openshare 0.2.1 → 0.3.0-dev.10
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 +74 -28
- package/ci/README.md +30 -14
- package/ci/openshare.js +475 -61
- package/dist/binding-queue.js +2 -2
- package/dist/commands.js +8 -8
- package/dist/config.d.ts +37 -14
- package/dist/config.js +119 -33
- package/dist/contract.d.ts +5 -5
- package/dist/contract.js +5 -5
- package/dist/core.d.ts +12 -1
- package/dist/core.js +409 -138
- package/dist/dialogs.d.ts +9 -10
- package/dist/dialogs.js +25 -28
- package/dist/redact.d.ts +46 -0
- package/dist/redact.js +130 -0
- package/dist/settings.d.ts +10 -7
- package/dist/settings.js +138 -56
- package/dist/share-flow.d.ts +30 -6
- package/dist/share-flow.js +73 -18
- package/dist/tui.js +260 -60
- package/dist/ui-prefs.js +6 -6
- package/dist/v2-server.js +5 -6
- package/package.json +2 -4
- package/src/binding-queue.ts +2 -2
- package/src/commands.ts +8 -8
- package/src/config.ts +124 -31
- package/src/contract.ts +5 -5
- package/src/core.ts +384 -137
- package/src/dialogs.tsx +29 -32
- package/src/redact.ts +134 -0
- package/src/settings.ts +134 -54
- package/src/share-flow.ts +76 -20
- package/src/sidebar.tsx +7 -7
- package/src/tui.tsx +22 -24
- package/src/ui-prefs.ts +6 -6
- package/src/v2-server.ts +5 -6
package/dist/tui.js
CHANGED
|
@@ -22,11 +22,13 @@ var DEFAULTS = {
|
|
|
22
22
|
debounceMs: 1e4,
|
|
23
23
|
completedDelayMs: 5000,
|
|
24
24
|
modelLabel: "$Provider/$Model",
|
|
25
|
-
|
|
25
|
+
ttlHours: 90 * 24,
|
|
26
26
|
authLogin: "",
|
|
27
27
|
authPassword: "",
|
|
28
28
|
spoolDir: "",
|
|
29
29
|
urlFile: "",
|
|
30
|
+
urlFileFormat: "url",
|
|
31
|
+
configSet: "",
|
|
30
32
|
announceStderr: true
|
|
31
33
|
};
|
|
32
34
|
var CONFIG_NAME = "openshare.config.json";
|
|
@@ -34,6 +36,14 @@ var LOCAL_NAME = "openshare.local.json";
|
|
|
34
36
|
function moduleDir() {
|
|
35
37
|
return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
|
|
36
38
|
}
|
|
39
|
+
function pluginVersion() {
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(moduleDir()), "package.json"), "utf8"));
|
|
42
|
+
return pkg.version ?? "?";
|
|
43
|
+
} catch {
|
|
44
|
+
return "?";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
37
47
|
function packageRoot(start) {
|
|
38
48
|
let current = start;
|
|
39
49
|
for (let guard = 0;guard < 16; guard++) {
|
|
@@ -97,10 +107,13 @@ function configSourceMap(pluginDir, env = process.env) {
|
|
|
97
107
|
debounceMs: env.OPENSHARE_DEBOUNCE_MS,
|
|
98
108
|
completedDelayMs: env.OPENSHARE_COMPLETED_DELAY_MS,
|
|
99
109
|
modelLabel: env.OPENSHARE_MODEL_LABEL,
|
|
110
|
+
ttlHours: env.OPENSHARE_TTL_HOURS,
|
|
100
111
|
ttlDays: env.OPENSHARE_TTL_DAYS,
|
|
101
112
|
authLogin: env.OPENSHARE_AUTH_LOGIN,
|
|
102
113
|
authPassword: env.OPENSHARE_AUTH_PASSWORD,
|
|
103
114
|
urlFile: env.OPENSHARE_URL_FILE,
|
|
115
|
+
urlFileFormat: env.OPENSHARE_URL_FILE_FORMAT,
|
|
116
|
+
configSet: env.OPENSHARE_CONFIG_SET,
|
|
104
117
|
spoolDir: env.OPENSHARE_SPOOL_DIR,
|
|
105
118
|
announceStderr: env.OPENSHARE_ANNOUNCE_STDERR,
|
|
106
119
|
shareMode: env.OPENSHARE_SHARE_MODE
|
|
@@ -109,6 +122,24 @@ function configSourceMap(pluginDir, env = process.env) {
|
|
|
109
122
|
if (value !== undefined && value !== "")
|
|
110
123
|
sources[key] = "environment";
|
|
111
124
|
}
|
|
125
|
+
if (sources.ttlDays && !sources.ttlHours)
|
|
126
|
+
sources.ttlHours = sources.ttlDays;
|
|
127
|
+
delete sources.ttlDays;
|
|
128
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
129
|
+
try {
|
|
130
|
+
const raw = JSON.parse(env.OPENSHARE_CONFIG_JSON);
|
|
131
|
+
if (raw && typeof raw === "object") {
|
|
132
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
133
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
134
|
+
continue;
|
|
135
|
+
if (sources[key] === "environment")
|
|
136
|
+
continue;
|
|
137
|
+
if (key === "servers" || known.has(key))
|
|
138
|
+
sources[key] = "OPENSHARE_CONFIG_JSON";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
112
143
|
for (const path of configPaths(pluginDir, env)) {
|
|
113
144
|
let raw;
|
|
114
145
|
try {
|
|
@@ -162,6 +193,8 @@ function normalizeServers(raw) {
|
|
|
162
193
|
entry.adminToken = e.adminToken;
|
|
163
194
|
if (e.active === true)
|
|
164
195
|
entry.active = true;
|
|
196
|
+
if (e.anonymous === true)
|
|
197
|
+
entry.anonymous = true;
|
|
165
198
|
out[url] = entry;
|
|
166
199
|
}
|
|
167
200
|
return Object.keys(out).length ? out : undefined;
|
|
@@ -231,26 +264,53 @@ function loadConfig(pluginDir, options) {
|
|
|
231
264
|
const cfg = { ...DEFAULTS };
|
|
232
265
|
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
233
266
|
const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
const applyObject = (raw) => {
|
|
268
|
+
if (!raw || typeof raw !== "object")
|
|
269
|
+
return;
|
|
270
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
271
|
+
if (key === "servers") {
|
|
272
|
+
const servers = normalizeServers(value);
|
|
273
|
+
if (servers)
|
|
274
|
+
cfg.servers = mergeServerGroups(cfg.servers, servers);
|
|
238
275
|
continue;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
continue;
|
|
276
|
+
}
|
|
277
|
+
if (key === "redact") {
|
|
278
|
+
if (value && typeof value === "object" && Array.isArray(value.patterns)) {
|
|
279
|
+
const pats = value.patterns.filter((x) => typeof x === "string" && x.length > 0 && x.length < 500).slice(0, 50);
|
|
280
|
+
cfg.redact = { patterns: pats };
|
|
245
281
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (key === "meta") {
|
|
285
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
286
|
+
const clean = {};
|
|
287
|
+
for (const [mk, mv] of Object.entries(value)) {
|
|
288
|
+
if (typeof mv === "string" && mv.length <= 500 && mk.length <= 64)
|
|
289
|
+
clean[mk] = mv;
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(clean).length)
|
|
292
|
+
cfg.meta = { ...cfg.meta, ...clean };
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (key === "ttlDays") {
|
|
297
|
+
const d = Math.floor(Number(value));
|
|
298
|
+
if (Number.isFinite(d) && d > 0)
|
|
299
|
+
cfg.ttlHours = d * 24;
|
|
300
|
+
continue;
|
|
253
301
|
}
|
|
302
|
+
if (!known.has(key))
|
|
303
|
+
continue;
|
|
304
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
305
|
+
continue;
|
|
306
|
+
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
307
|
+
continue;
|
|
308
|
+
cfg[key] = value;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
for (const path of [...configPaths(pluginDir)].reverse()) {
|
|
312
|
+
try {
|
|
313
|
+
applyObject(JSON.parse(readFileSync(path, "utf8")));
|
|
254
314
|
} catch (e) {
|
|
255
315
|
if (existsSync(path))
|
|
256
316
|
console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
|
|
@@ -273,6 +333,13 @@ function loadConfig(pluginDir, options) {
|
|
|
273
333
|
cfg.shareMode = mode;
|
|
274
334
|
}
|
|
275
335
|
const env = process.env;
|
|
336
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
337
|
+
try {
|
|
338
|
+
applyObject(JSON.parse(env.OPENSHARE_CONFIG_JSON));
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.error("[openshare] OPENSHARE_CONFIG_JSON could not be parsed - ignored:", e?.message ?? e);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
276
343
|
if (env.OPENSHARE_WORKER_URL)
|
|
277
344
|
cfg.workerUrl = env.OPENSHARE_WORKER_URL;
|
|
278
345
|
if (env.OPENSHARE_CREATE_TOKEN)
|
|
@@ -283,14 +350,20 @@ function loadConfig(pluginDir, options) {
|
|
|
283
350
|
cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
|
|
284
351
|
if (env.OPENSHARE_MODEL_LABEL !== undefined)
|
|
285
352
|
cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
|
|
353
|
+
if (env.OPENSHARE_TTL_HOURS)
|
|
354
|
+
cfg.ttlHours = num(env.OPENSHARE_TTL_HOURS, cfg.ttlHours);
|
|
286
355
|
if (env.OPENSHARE_TTL_DAYS)
|
|
287
|
-
cfg.
|
|
356
|
+
cfg.ttlHours = Math.max(1, Math.floor(num(env.OPENSHARE_TTL_DAYS, 90) * 24));
|
|
288
357
|
if (env.OPENSHARE_AUTH_LOGIN !== undefined)
|
|
289
358
|
cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
|
|
290
359
|
if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
|
|
291
360
|
cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
|
|
292
361
|
if (env.OPENSHARE_URL_FILE !== undefined)
|
|
293
362
|
cfg.urlFile = env.OPENSHARE_URL_FILE;
|
|
363
|
+
if (env.OPENSHARE_URL_FILE_FORMAT === "json" || env.OPENSHARE_URL_FILE_FORMAT === "url")
|
|
364
|
+
cfg.urlFileFormat = env.OPENSHARE_URL_FILE_FORMAT;
|
|
365
|
+
if (env.OPENSHARE_CONFIG_SET)
|
|
366
|
+
cfg.configSet = env.OPENSHARE_CONFIG_SET;
|
|
294
367
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
295
368
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
296
369
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
@@ -444,8 +517,18 @@ function queueBindingWrite(sid, fn) {
|
|
|
444
517
|
return run;
|
|
445
518
|
}
|
|
446
519
|
|
|
520
|
+
// src/redact.ts
|
|
521
|
+
var PATH_KEYS = new Set(["file", "path", "filePath", "cwd", "directory", "root"]);
|
|
522
|
+
|
|
447
523
|
// src/core.ts
|
|
448
524
|
var liveTargets = [];
|
|
525
|
+
var statsProvider = null;
|
|
526
|
+
var redactionLedger = new Map;
|
|
527
|
+
function sessionStatsFor(sid) {
|
|
528
|
+
const led = redactionLedger.get(sid) ?? { count: 0, samples: [] };
|
|
529
|
+
const counts = statsProvider ? statsProvider(sid) : { messages: 0, toolCalls: 0 };
|
|
530
|
+
return { ...counts, redaction: led };
|
|
531
|
+
}
|
|
449
532
|
function engineTargetUrls() {
|
|
450
533
|
return liveTargets;
|
|
451
534
|
}
|
|
@@ -474,7 +557,7 @@ function saveConfig(cfg, target, baseDir = moduleDir()) {
|
|
|
474
557
|
"debounceMs",
|
|
475
558
|
"completedDelayMs",
|
|
476
559
|
"modelLabel",
|
|
477
|
-
"
|
|
560
|
+
"ttlHours",
|
|
478
561
|
"authLogin",
|
|
479
562
|
"authPassword",
|
|
480
563
|
"spoolDir",
|
|
@@ -562,6 +645,23 @@ function bindingsOf(info) {
|
|
|
562
645
|
async function readBindings(client, sid) {
|
|
563
646
|
return bindingsOf(await getSession(client, sid));
|
|
564
647
|
}
|
|
648
|
+
function fmtHours(hours) {
|
|
649
|
+
if (!Number.isFinite(hours) || hours <= 0)
|
|
650
|
+
return "?";
|
|
651
|
+
if (hours % 24 === 0)
|
|
652
|
+
return `${Math.floor(hours / 24)}d`;
|
|
653
|
+
return `${Math.floor(hours)}h`;
|
|
654
|
+
}
|
|
655
|
+
function parseTtlHours(raw) {
|
|
656
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(h|d|hr|hours?|days?)?\s*$/i.exec(raw ?? "");
|
|
657
|
+
if (!m)
|
|
658
|
+
return;
|
|
659
|
+
const v = Number(m[1]);
|
|
660
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
661
|
+
return;
|
|
662
|
+
const unit = (m[2] ?? "h").toLowerCase();
|
|
663
|
+
return Math.max(1, Math.floor(unit.startsWith("d") ? v * 24 : v));
|
|
664
|
+
}
|
|
565
665
|
function hostOf(url) {
|
|
566
666
|
try {
|
|
567
667
|
return new URL(url).host;
|
|
@@ -683,6 +783,27 @@ async function workerDeleteShare(workerUrl, id, secret) {
|
|
|
683
783
|
return false;
|
|
684
784
|
}
|
|
685
785
|
}
|
|
786
|
+
async function patchShareOpts(workerUrl, id, opts, secret) {
|
|
787
|
+
const body = { secret };
|
|
788
|
+
if (opts.authLogin !== undefined && opts.authPassword !== undefined) {
|
|
789
|
+
body.authLogin = opts.authLogin;
|
|
790
|
+
body.authPassword = opts.authPassword;
|
|
791
|
+
}
|
|
792
|
+
if (opts.ttlHours !== undefined)
|
|
793
|
+
body.ttlHours = opts.ttlHours;
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch(`${workerUrl.replace(/\/$/, "")}/api/share/${id}`, {
|
|
796
|
+
method: "PATCH",
|
|
797
|
+
headers: { "content-type": "application/json" },
|
|
798
|
+
body: JSON.stringify(body),
|
|
799
|
+
signal: AbortSignal.timeout(8000)
|
|
800
|
+
});
|
|
801
|
+
const json = await res.json().catch(() => ({}));
|
|
802
|
+
return res.ok && json.ok ? { ok: true } : { ok: false, error: json.error ?? `HTTP ${res.status}` };
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return { ok: false, error: e?.message || String(e) };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
686
807
|
async function probeWorker(workerUrl) {
|
|
687
808
|
if (!workerUrl)
|
|
688
809
|
return { ok: false, detail: "workerUrl is empty" };
|
|
@@ -866,8 +987,10 @@ async function requestShareConsent(host, context) {
|
|
|
866
987
|
const bits = [];
|
|
867
988
|
if (context.server)
|
|
868
989
|
bits.push(`server: ${context.server}`);
|
|
869
|
-
if (context.
|
|
870
|
-
bits.push(`TTL: ${context.
|
|
990
|
+
if (context.ttlHours !== undefined)
|
|
991
|
+
bits.push(`TTL: ${fmtHours(context.ttlHours)}`);
|
|
992
|
+
if (context.redaction && context.redaction.hits > 0)
|
|
993
|
+
bits.push(`redaction: ${context.redaction.hits} hit(s)`);
|
|
871
994
|
bits.push(`read auth: ${context.auth ? "on" : "off"}`);
|
|
872
995
|
lines.push(bits.join(" - "));
|
|
873
996
|
}
|
|
@@ -895,12 +1018,18 @@ async function shareWithConsent(host, sessionID) {
|
|
|
895
1018
|
}
|
|
896
1019
|
const opts = all[0]?.opts;
|
|
897
1020
|
const server = serverEntries(cfg).find((s) => s.active) ?? serverEntries(cfg)[0] ?? (engineTargetUrls()[0] ? { url: engineTargetUrls()[0] } : undefined);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1021
|
+
{
|
|
1022
|
+
const stats = sessionStatsFor(root);
|
|
1023
|
+
const gated = !!(opts?.authLogin ?? cfg.authLogin) && !!(opts?.authPassword ?? cfg.authPassword);
|
|
1024
|
+
if (!await requestShareConsent(host, {
|
|
1025
|
+
server: server ? hostOf(server.url) : undefined,
|
|
1026
|
+
ttlHours: opts?.ttlHours ?? cfg.ttlHours,
|
|
1027
|
+
auth: gated,
|
|
1028
|
+
counts: { messages: stats.messages, toolCalls: stats.toolCalls },
|
|
1029
|
+
redaction: gated ? undefined : { hits: stats.redaction.count, samples: stats.redaction.samples.map((h) => `${h.sample} [${h.kind}]`) }
|
|
1030
|
+
}))
|
|
1031
|
+
return { ok: false, message: "Share cancelled." };
|
|
1032
|
+
}
|
|
904
1033
|
return confirmShare(host, root);
|
|
905
1034
|
}
|
|
906
1035
|
async function confirmShare(host, rootSessionID) {
|
|
@@ -1070,15 +1199,15 @@ A graceful opencode exit flushes the pending flip, so CI runs land their final s
|
|
|
1070
1199
|
info: 'How the model name appears on the share page. "$Provider/$Model" renders e.g. "closedrouter/zai/glm-5.3". Any literal text is used as-is; empty string hides the model line entirely.',
|
|
1071
1200
|
def: '"$Provider/$Model"'
|
|
1072
1201
|
},
|
|
1073
|
-
|
|
1074
|
-
label: "TTL (
|
|
1075
|
-
help: "Share lifetime
|
|
1202
|
+
ttlHours: {
|
|
1203
|
+
label: "TTL (hours)",
|
|
1204
|
+
help: "Share lifetime in hours (2d writes 48); capped server-side.",
|
|
1076
1205
|
kind: "number",
|
|
1077
1206
|
section: "Sharing",
|
|
1078
|
-
info: `How long pages live before the server's sweeper deletes them. Capped server-side: the deployment's MAX_TTL_DAYS and the token's
|
|
1207
|
+
info: `How long pages live before the server's sweeper deletes them, in HOURS. Capped server-side: the deployment's MAX_TTL_HOURS (legacy MAX_TTL_DAYS converted) and the token's limit both apply; the floor is one hour.
|
|
1079
1208
|
|
|
1080
|
-
A per-session override lives in the This-session section; it is sent at create/attach time.`,
|
|
1081
|
-
def: "worker cap (
|
|
1209
|
+
A per-session override lives in the This-session section; it is sent at create/attach time. The session prompt takes 6h / 2d style input.`,
|
|
1210
|
+
def: "worker cap (2160h = 90d on the reference deployment)"
|
|
1082
1211
|
},
|
|
1083
1212
|
authLogin: {
|
|
1084
1213
|
label: "Read auth: login",
|
|
@@ -1203,26 +1332,74 @@ async function connectServerFlow(host) {
|
|
|
1203
1332
|
const url = normServerUrl(raw);
|
|
1204
1333
|
if (!/^https?:\/\//.test(url))
|
|
1205
1334
|
return { ok: false, message: "URL must start with http:// or https://" };
|
|
1206
|
-
host.ui.toast({ variant: "info", title: "OpenShare", message: `Probing ${url}
|
|
1335
|
+
host.ui.toast({ variant: "info", title: "OpenShare", message: `Probing ${url}:` });
|
|
1207
1336
|
const probe = await probeAuthMode(url);
|
|
1208
1337
|
if (!probe.ok)
|
|
1209
1338
|
return { ok: false, message: `${url}: ${probe.detail}` };
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1339
|
+
const existing = serverEntries().find((sv) => sv.url === url)?.entry ?? {};
|
|
1340
|
+
const deactivateOthers = () => {
|
|
1341
|
+
for (const other of serverEntries()) {
|
|
1342
|
+
if (other.url !== url)
|
|
1343
|
+
saveServerEntry(other.url, { active: undefined });
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
if (probe.mode === "public") {
|
|
1347
|
+
const picked = await showSelect(host.ui, {
|
|
1348
|
+
title: `${url} - account?`,
|
|
1349
|
+
options: [
|
|
1350
|
+
{
|
|
1351
|
+
value: "onboard",
|
|
1352
|
+
title: "Onboard (get an account)",
|
|
1353
|
+
description: "your own user, tokens and share limits; survives the server's anonymous caps",
|
|
1354
|
+
help: "An account gives you a revocable identity with your own share registry and limits. Depending on the server you may need a one-time access code from the hoster - you'll be asked only if it's required."
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
value: "anon",
|
|
1358
|
+
title: "Stay anonymous",
|
|
1359
|
+
description: "share without an account; counts against the server-wide anonymous limits",
|
|
1360
|
+
help: "No identity, no token to manage. Shares are ownerless: whoever holds the share's secret controls them, and the server may throttle anonymous creates globally. You can onboard later from Servers -> the server -> Enroll now."
|
|
1361
|
+
}
|
|
1362
|
+
]
|
|
1363
|
+
});
|
|
1364
|
+
if (picked === "anon") {
|
|
1365
|
+
saveServerEntry(url, { ...existing.token ? {} : { anonymous: true }, label: existing.label, active: true });
|
|
1366
|
+
return { ok: true, message: `Connected to ${url} anonymously - sharing works, no account.`, url };
|
|
1367
|
+
}
|
|
1368
|
+
if (picked === undefined || picked === null)
|
|
1369
|
+
return { ok: false, message: "Cancelled." };
|
|
1370
|
+
let code = existing.code ?? "";
|
|
1371
|
+
if (!code) {
|
|
1372
|
+
const free = await enrollAtServer(url, "");
|
|
1373
|
+
if (free.ok) {
|
|
1374
|
+
deactivateOthers();
|
|
1375
|
+
saveServerEntry(url, { token: free.token, label: free.label, anonymous: undefined, active: true });
|
|
1376
|
+
return { ok: true, message: `Connected to ${url} as "${free.label}" - sharing goes there now.`, url };
|
|
1377
|
+
}
|
|
1378
|
+
const typed = await showPrompt(host.ui, { title: `${url} needs an access code`, placeholder: "osc_: (from the hoster)" });
|
|
1379
|
+
if (typed === undefined)
|
|
1380
|
+
return { ok: false, message: "Cancelled - this server needs a code to onboard." };
|
|
1381
|
+
code = typed.trim();
|
|
1382
|
+
}
|
|
1383
|
+
const res = await enrollAtServer(url, code);
|
|
1384
|
+
if (!res.ok)
|
|
1385
|
+
return { ok: false, message: `Enrollment refused: ${res.error}` };
|
|
1386
|
+
deactivateOthers();
|
|
1387
|
+
saveServerEntry(url, { token: res.token, label: res.label, code: code || undefined, anonymous: undefined, active: true });
|
|
1388
|
+
return { ok: true, message: `Connected to ${url} as "${res.label}" - sharing goes there now.`, url };
|
|
1389
|
+
}
|
|
1390
|
+
let code = existing.code ?? "";
|
|
1391
|
+
if (!code) {
|
|
1392
|
+
const typed = await showPrompt(host.ui, { title: `${url} needs an access code`, placeholder: "osc_: (from the hoster)" });
|
|
1213
1393
|
if (typed === undefined)
|
|
1214
|
-
return { ok: false, message: "Cancelled
|
|
1394
|
+
return { ok: false, message: "Cancelled - private servers need a code." };
|
|
1215
1395
|
code = typed.trim();
|
|
1216
1396
|
}
|
|
1217
1397
|
const res = await enrollAtServer(url, code);
|
|
1218
1398
|
if (!res.ok)
|
|
1219
1399
|
return { ok: false, message: `Enrollment refused: ${res.error}` };
|
|
1220
|
-
|
|
1221
|
-
if (other.url !== url)
|
|
1222
|
-
saveServerEntry(other.url, { active: undefined });
|
|
1223
|
-
}
|
|
1400
|
+
deactivateOthers();
|
|
1224
1401
|
saveServerEntry(url, { token: res.token, label: res.label, code: code || undefined, active: true });
|
|
1225
|
-
return { ok: true, message: `Connected to ${url} as "${res.label}"
|
|
1402
|
+
return { ok: true, message: `Connected to ${url} as "${res.label}" - sharing goes there now.`, url };
|
|
1226
1403
|
}
|
|
1227
1404
|
async function serverMenu(host, url) {
|
|
1228
1405
|
while (true) {
|
|
@@ -1231,7 +1408,7 @@ async function serverMenu(host, url) {
|
|
|
1231
1408
|
if (!mine)
|
|
1232
1409
|
return;
|
|
1233
1410
|
const holdsRoot = !!mine.entry.token && mine.entry.token === (mine.entry.adminToken ?? adminTokenFor(url));
|
|
1234
|
-
const cred = holdsRoot ? `ROOT token ${mask(mine.entry.token)} \u2014 admin, no user identity` : mine.entry.token ? `token ${mask(mine.entry.token)}` : mine.entry.code ? "no token (code saved, enrolls on demand)" : "no token (public server,
|
|
1411
|
+
const cred = holdsRoot ? `ROOT token ${mask(mine.entry.token)} \u2014 admin, no user identity` : mine.entry.token ? `token ${mask(mine.entry.token)}` : mine.entry.code ? "no token (code saved, enrolls on demand)" : mine.entry.anonymous ? "anonymous (declined onboarding)" : "no token (public server, shares anonymously)";
|
|
1235
1412
|
const options = [
|
|
1236
1413
|
{ title: url, value: "info", description: `${mine.entry.label ?? "(no label)"} \xB7 ${cred}${mine.active ? " \xB7 ACTIVE" : ""}`, disabled: true }
|
|
1237
1414
|
];
|
|
@@ -1311,8 +1488,18 @@ Needs the server's root CREATE_TOKEN (saved admin token used first; prompted oth
|
|
|
1311
1488
|
continue;
|
|
1312
1489
|
}
|
|
1313
1490
|
let code = mine.entry.code ?? "";
|
|
1314
|
-
if (
|
|
1315
|
-
const
|
|
1491
|
+
if (!code) {
|
|
1492
|
+
const free = await enrollAtServer(url, "");
|
|
1493
|
+
if (free.ok) {
|
|
1494
|
+
saveServerEntry(url, { token: free.token, label: free.label, anonymous: undefined });
|
|
1495
|
+
host.ui.toast({ variant: "success", title: "OpenShare", message: `Enrolled as "${free.label}".` });
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (!/code required/i.test(free.error ?? "")) {
|
|
1499
|
+
await showInfo(host.ui, { title: "Enrollment refused", message: free.error ?? "unknown error" });
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const typed = await showPrompt(host.ui, { title: `${url} needs an access code`, placeholder: "osc_: (from the hoster)" });
|
|
1316
1503
|
if (typed === undefined)
|
|
1317
1504
|
continue;
|
|
1318
1505
|
code = typed.trim();
|
|
@@ -1321,7 +1508,7 @@ Needs the server's root CREATE_TOKEN (saved admin token used first; prompted oth
|
|
|
1321
1508
|
}
|
|
1322
1509
|
const res = await enrollAtServer(url, code);
|
|
1323
1510
|
if (res.ok) {
|
|
1324
|
-
saveServerEntry(url, { token: res.token, label: res.label });
|
|
1511
|
+
saveServerEntry(url, { token: res.token, label: res.label, anonymous: undefined });
|
|
1325
1512
|
host.ui.toast({ variant: "success", title: "OpenShare", message: `Enrolled as "${res.label}".` });
|
|
1326
1513
|
} else {
|
|
1327
1514
|
await showInfo(host.ui, { title: "Enrollment refused", message: res.error ?? "unknown error" });
|
|
@@ -1984,10 +2171,10 @@ The clamp note appears when the server or your token's policy reduced a requeste
|
|
|
1984
2171
|
rows.push({
|
|
1985
2172
|
title: "Session TTL override (days)",
|
|
1986
2173
|
value: "session:ttl",
|
|
1987
|
-
description: binding?.opts?.
|
|
1988
|
-
help: `Per-session share lifetime. Overrides the config
|
|
2174
|
+
description: binding?.opts?.ttlHours !== undefined ? fmtHours(binding.opts.ttlHours) : "(worker default)",
|
|
2175
|
+
help: `Per-session share lifetime in HOURS (prompt accepts 6h / 2d / 48). Overrides the config TTL for THIS session's pages; still capped server-side. On a live share it applies immediately (shortening may end an older page at once); otherwise it is used when you share.
|
|
1989
2176
|
|
|
1990
|
-
Current: ${binding?.opts?.
|
|
2177
|
+
Current: ${binding?.opts?.ttlHours !== undefined ? fmtHours(binding.opts.ttlHours) : "(none \u2014 config default)"}`
|
|
1991
2178
|
});
|
|
1992
2179
|
rows.push({
|
|
1993
2180
|
title: "Session read auth",
|
|
@@ -2218,26 +2405,32 @@ Every link stops working for everyone. This cannot be undone.`,
|
|
|
2218
2405
|
return;
|
|
2219
2406
|
}
|
|
2220
2407
|
if (choice === "session:ttl") {
|
|
2221
|
-
const raw = await showPrompt(host.ui, { title: "Session TTL
|
|
2408
|
+
const raw = await showPrompt(host.ui, { title: "Session TTL", placeholder: "hours: 6h, 2d or 48", value: current?.opts?.ttlHours !== undefined ? fmtHours(current.opts.ttlHours) : "" });
|
|
2222
2409
|
if (raw === undefined)
|
|
2223
2410
|
return;
|
|
2224
2411
|
const opts = { ...current?.opts ?? {} };
|
|
2225
2412
|
if (raw === "")
|
|
2226
|
-
delete opts.
|
|
2413
|
+
delete opts.ttlHours;
|
|
2227
2414
|
else {
|
|
2228
|
-
const
|
|
2229
|
-
if (
|
|
2230
|
-
host.ui.toast({ variant: "error", title: "OpenShare", message: "TTL must be a positive number of
|
|
2415
|
+
const hours = parseTtlHours(raw);
|
|
2416
|
+
if (hours === undefined) {
|
|
2417
|
+
host.ui.toast({ variant: "error", title: "OpenShare", message: "TTL must be 6h / 2d style or a positive number of hours." });
|
|
2231
2418
|
return;
|
|
2232
2419
|
}
|
|
2233
|
-
opts.
|
|
2420
|
+
opts.ttlHours = hours;
|
|
2234
2421
|
}
|
|
2235
2422
|
await setShareOpts(host.client, root, opts);
|
|
2236
2423
|
const liveNow = live.length > 0;
|
|
2424
|
+
if (liveNow) {
|
|
2425
|
+
const results = await Promise.all(live.map((b) => b.id && b.secret && b.server ? patchShareOpts(b.server, b.id, opts.ttlHours !== undefined ? { ttlHours: opts.ttlHours } : {}, b.secret) : Promise.resolve({ ok: false })));
|
|
2426
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
2427
|
+
if (failed)
|
|
2428
|
+
host.ui.toast({ variant: "error", title: "OpenShare", message: `TTL applied on ${results.length - failed}/${results.length} server(s) - the rest sync on next connect.` });
|
|
2429
|
+
}
|
|
2237
2430
|
host.ui.toast({
|
|
2238
2431
|
variant: "success",
|
|
2239
2432
|
title: "OpenShare",
|
|
2240
|
-
message: liveNow ? `Session TTL: ${opts.
|
|
2433
|
+
message: liveNow ? `Session TTL: ${opts.ttlHours !== undefined ? `${fmtHours(opts.ttlHours)} - applied now (shortening may end an older page immediately)` : "(default) - applied now"}.` : `Session TTL: ${opts.ttlHours !== undefined ? fmtHours(opts.ttlHours) : "(default)"} - saved, applies when you share.`
|
|
2241
2434
|
});
|
|
2242
2435
|
return;
|
|
2243
2436
|
}
|
|
@@ -2262,6 +2455,13 @@ Every link stops working for everyone. This cannot be undone.`,
|
|
|
2262
2455
|
}
|
|
2263
2456
|
await setShareOpts(host.client, root, opts);
|
|
2264
2457
|
const liveNow2 = live.length > 0;
|
|
2458
|
+
if (liveNow2) {
|
|
2459
|
+
const pair = login === "" ? { authLogin: "", authPassword: "" } : { authLogin: login, authPassword: password };
|
|
2460
|
+
const results2 = await Promise.all(live.map((b) => b.id && b.secret && b.server ? patchShareOpts(b.server, b.id, pair, b.secret) : Promise.resolve({ ok: false })));
|
|
2461
|
+
const failed2 = results2.filter((r) => !r.ok).length;
|
|
2462
|
+
if (failed2)
|
|
2463
|
+
host.ui.toast({ variant: "error", title: "OpenShare", message: `Read auth applied on ${results2.length - failed2}/${results2.length} server(s) - the rest sync on next connect.` });
|
|
2464
|
+
}
|
|
2265
2465
|
host.ui.toast({ variant: "success", title: "OpenShare", message: liveNow2 ? `Read auth ${login === "" ? "removed" : "saved"} - applied now.` : `Read auth ${login === "" ? "cleared" : "saved"} - applies when you share.` });
|
|
2266
2466
|
return;
|
|
2267
2467
|
}
|
|
@@ -2312,7 +2512,7 @@ Every ACTIVE server receives new sessions (fan-out).`
|
|
|
2312
2512
|
});
|
|
2313
2513
|
groups.push({
|
|
2314
2514
|
id: "sharing",
|
|
2315
|
-
summary: `sync ${seconds(cfg.debounceMs, 1e4)} \xB7 completed ${seconds(cfg.completedDelayMs, 5000)} \xB7 TTL ${cfg.
|
|
2515
|
+
summary: `sync ${seconds(cfg.debounceMs, 1e4)} \xB7 completed ${seconds(cfg.completedDelayMs, 5000)} \xB7 TTL ${fmtHours(cfg.ttlHours ?? 2160)}`,
|
|
2316
2516
|
help: `What and how sessions stream: share mode (all/manual), sync cadence, the completed-delay, the model label on pages, share TTL, and the viewer's task-tab behavior.
|
|
2317
2517
|
|
|
2318
2518
|
Cadence values are clamped by the server's policy window.`
|
|
@@ -2348,7 +2548,7 @@ Stored in the host's preference store \u2014 per machine, not the config file.`
|
|
|
2348
2548
|
help: group.help
|
|
2349
2549
|
}));
|
|
2350
2550
|
options.push({ title: dirty ? "Save & exit" : "Exit", value: "action:exit", description: dirty ? `Write ${writeTarget.path}` : "No changes" });
|
|
2351
|
-
const choice = await showSelect(host.ui, { title:
|
|
2551
|
+
const choice = await showSelect(host.ui, { title: `OpenShare v${pluginVersion()} \xB7 settings`, options });
|
|
2352
2552
|
if (choice === undefined || choice === "action:exit") {
|
|
2353
2553
|
if (dirty) {
|
|
2354
2554
|
const saved = saveConfig(cfg, writeTarget.path);
|
package/dist/ui-prefs.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Host-agnostic dialog size preferences
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Host-agnostic dialog size preferences, stored in host kv rather than the
|
|
2
|
+
// config file because UI settings are per-host and synced by opencode
|
|
3
|
+
// itself. Dialog width maps to the host's fixed dialog widths; height is a
|
|
4
|
+
// percent of terminal rows. v1 dialogs apply these directly (dialogs.tsx);
|
|
5
|
+
// the settings dialog edits them through this module so the flows stay
|
|
6
|
+
// host-agnostic.
|
|
7
7
|
export const UI_WIDTH_KV = "openshare.ui-width";
|
|
8
8
|
export const UI_HEIGHT_PERCENT_KV = "openshare.ui-height-percent";
|
|
9
9
|
export const HEIGHT_PERCENT_MIN = 25;
|
package/dist/v2-server.js
CHANGED
|
@@ -177,12 +177,11 @@ export function createV2ServerSetup() {
|
|
|
177
177
|
// the client adapter and feed them into the engine as the v1 events it
|
|
178
178
|
// understands (session.updated / message.updated / message.part.updated).
|
|
179
179
|
// The engine's keyed queue makes repeated re-syncs idempotent.
|
|
180
|
-
// NOTE: no content-hash dedupe here
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// belongs engine-side, where dormancy is known, if it's ever needed.
|
|
180
|
+
// NOTE: no content-hash dedupe here. A refresh running while the session
|
|
181
|
+
// is still dormant (pre-flag) would poison the cache without emitting
|
|
182
|
+
// anything, and the flag-time refresh would then SKIP the unchanged
|
|
183
|
+
// messages, so the share's first assistant content would only arrive on
|
|
184
|
+
// the next change. Dedupe belongs engine-side, where dormancy is known.
|
|
186
185
|
const pendingRefresh = new Map();
|
|
187
186
|
// queued inbox inputs per session: v2 persists the prompt in the inbox
|
|
188
187
|
// (delivery "queue") while the current turn still runs — the message
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirrowel/opencode-openshare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-dev.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open(code)Share — re:share: self-hosted session sharing for opencode. Server plugin (single file or package), optional TUI for interactive opt-in sharing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,10 +64,8 @@
|
|
|
64
64
|
"build:tui": "bun scripts/build-tui.mjs",
|
|
65
65
|
"build:standalone": "bun scripts/build-standalone.mjs",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
|
-
"ci": "npm run
|
|
67
|
+
"ci": "npm run ci:package",
|
|
68
68
|
"ci:package": "npm run typecheck && npm run build && npm run pack:dry-run",
|
|
69
|
-
"release:intent": "node scripts/set-release-intent.mjs",
|
|
70
|
-
"release:intent:check": "node scripts/check-release-intent.mjs",
|
|
71
69
|
"pack:dry-run": "node scripts/pack-dry-run.mjs",
|
|
72
70
|
"test:regression": "node scripts/regression-tests.mjs",
|
|
73
71
|
"prepublishOnly": "npm run typecheck && npm run build",
|
package/src/binding-queue.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the servers map) and the TUI (`patchSessionBinding` in share-flow.ts —
|
|
6
6
|
* opt-outs, collision resolution). Their PATCHes interleave at the host:
|
|
7
7
|
* writer A reads at T0, writer B reads at T1, A PATCHes at T2, B PATCHes
|
|
8
|
-
* at T3
|
|
9
|
-
*
|
|
8
|
+
* at T3, and A's change is silently lost (a TUI opt-out erased by the
|
|
9
|
+
* engine's binding refresh).
|
|
10
10
|
*
|
|
11
11
|
* Both route their whole read-modify-write through this per-session FIFO.
|
|
12
12
|
* The queue never breaks on errors; entries drop when the process exits.
|
package/src/commands.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
// auth — one surface, no duplicated manager); on an unshared session it
|
|
6
6
|
// asks consent and creates the share. /openshare opens settings.
|
|
7
7
|
//
|
|
8
|
-
// LAYER SHAPE
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
8
|
+
// LAYER SHAPE: the v1 host keymap gates layers that declare `mode` on the
|
|
9
|
+
// mode stack (resting mode is "base"), so a plugin layer MUST NOT pass mode
|
|
10
|
+
// ("global" made the whole layer unreachable: no palette entries, no slash
|
|
11
|
+
// commands, no shadow) and must not carry undefined-valued command fields.
|
|
12
|
+
// The registration shape is exactly what the proven family plugins use:
|
|
13
|
+
// {commands, bindings}, commands with only defined fields, category
|
|
14
|
+
// "OpenShare" (own group, deliberately NOT joined into the shared family
|
|
15
|
+
// palette category).
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
clearGoneBindingEntries,
|