@olares/lares-core 0.15.4 → 0.15.6
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/files/draft-images.js +25 -0
- package/files/filename.js +6 -0
- package/files/preview-tabs.js +51 -0
- package/files/preview-workspace.js +18 -31
- package/i18n/mobile.js +222 -0
- package/i18n/preview.js +2 -0
- package/larepass/chat.js +23 -7
- package/larepass/commands.js +49 -0
- package/larepass/context-occupancy.js +30 -0
- package/larepass/host.js +17 -0
- package/larepass/mux.js +37 -0
- package/larepass/permissions.js +56 -0
- package/larepass/queue.js +33 -0
- package/larepass/references.js +101 -0
- package/larepass/rpc.js +6 -3
- package/larepass/runtime.js +403 -17
- package/larepass/settings.js +49 -0
- package/larepass/submission-settings.js +26 -0
- package/larepass/transcript.js +12 -1
- package/larepass/turn-metrics.js +142 -0
- package/larepass/unread.js +64 -0
- package/larepass/workspace.js +161 -0
- package/olares/trusted-host.js +9 -0
- package/package.json +14 -2
- package/router/session-model.js +1 -1
- package/workspace/constants.js +1 -0
- package/workspace/seed.js +2 -1
package/larepass/host.js
CHANGED
|
@@ -45,6 +45,23 @@ export function hostConfigFromEnv(env = {}) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Every key the host may hand down. The list is canonical: a host spreads these
|
|
50
|
+
* onto the components as props and `createHostClient` reads them back off the
|
|
51
|
+
* object, so a key that exists in one place and not the other is dropped in
|
|
52
|
+
* silence — which is how `socketProtocol` shipped inert once already.
|
|
53
|
+
*/
|
|
54
|
+
export const HOST_PORT_KEYS = ["baseUrl", "proxyPrefix", "env", "request", "socketProtocol"];
|
|
55
|
+
|
|
56
|
+
/** Collect the port keys off any carrier (a component instance included). */
|
|
57
|
+
export function pickHostPorts(source = {}) {
|
|
58
|
+
const ports = {};
|
|
59
|
+
for (const key of HOST_PORT_KEYS) {
|
|
60
|
+
ports[key] = source[key];
|
|
61
|
+
}
|
|
62
|
+
return ports;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
/** Resolve the live Host origin. Explicit baseUrl/proxyPrefix win over env. */
|
|
49
66
|
export function hostTarget(ports = {}) {
|
|
50
67
|
const fromEnv = hostConfigFromEnv(ports.env);
|
package/larepass/mux.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeQueueItems } from "./queue.js";
|
|
2
|
+
|
|
1
3
|
export const MUX_PATH = "/api/events.mux";
|
|
2
4
|
|
|
3
5
|
export const MUX_FETCH_HEADERS = {
|
|
@@ -63,6 +65,22 @@ export function classifyMuxEnvelope(envelope) {
|
|
|
63
65
|
event: frame.view ? { ...frame.event, view: frame.view } : frame.event,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
68
|
+
if (frame.type === "session/queue") {
|
|
69
|
+
return {
|
|
70
|
+
kind: "queue",
|
|
71
|
+
sessionId,
|
|
72
|
+
items: normalizeQueueItems(frame.items),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (frame.type === "session/projection" && typeof frame.key === "string") {
|
|
76
|
+
return {
|
|
77
|
+
kind: "projection",
|
|
78
|
+
sessionId,
|
|
79
|
+
key: frame.key,
|
|
80
|
+
value: frame.value,
|
|
81
|
+
seq: frame.seq,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
66
84
|
if (frame.type === "question/requested") {
|
|
67
85
|
return {
|
|
68
86
|
kind: "question",
|
|
@@ -71,6 +89,25 @@ export function classifyMuxEnvelope(envelope) {
|
|
|
71
89
|
questions: Array.isArray(frame.questions) ? frame.questions : [],
|
|
72
90
|
};
|
|
73
91
|
}
|
|
92
|
+
if (frame.type === "approval/requested") {
|
|
93
|
+
return {
|
|
94
|
+
kind: "approval",
|
|
95
|
+
sessionId,
|
|
96
|
+
rpcId,
|
|
97
|
+
approvalId: frame.approvalId,
|
|
98
|
+
toolName: frame.toolName,
|
|
99
|
+
callId: frame.callId,
|
|
100
|
+
reason: frame.reason,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (frame.type === "approval/resolved") {
|
|
104
|
+
return {
|
|
105
|
+
kind: "approval-resolved",
|
|
106
|
+
sessionId,
|
|
107
|
+
approvalId: frame.approvalId,
|
|
108
|
+
outcome: frame.outcome,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
74
111
|
if (frame.type === "question/resolved") {
|
|
75
112
|
return {
|
|
76
113
|
kind: "question-resolved",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's permission preset: which sandbox and approval policy its tools
|
|
3
|
+
* run under.
|
|
4
|
+
*
|
|
5
|
+
* The host publishes the effective preset and the offered options as the
|
|
6
|
+
* `permissions` session projection, and switching is the `/permission <name>`
|
|
7
|
+
* command — there is no dedicated RPC. Preset names arrive as the kebab keys
|
|
8
|
+
* the deployment declares (`workspace-write`), never as display copy, because
|
|
9
|
+
* the wire carries no locale; the dictionary below the `permission.` prefix
|
|
10
|
+
* owns the words and this module falls back to title case for a preset the
|
|
11
|
+
* client has never heard of.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown} row - a session summary or a raw `projections` block owner.
|
|
16
|
+
* @returns {{ options: Array<{ value: string, name?: string, description?: string }>, currentValue: string } | null}
|
|
17
|
+
*/
|
|
18
|
+
export function permissionSelect(row) {
|
|
19
|
+
const select = row?.permissions ?? row?.projections?.values?.permissions;
|
|
20
|
+
const options = (Array.isArray(select?.options) ? select.options : []).filter((option) => option?.value);
|
|
21
|
+
if (options.length === 0) return null;
|
|
22
|
+
return { options, currentValue: String(select?.currentValue || "") };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function permissionOption(select, value = select?.currentValue) {
|
|
26
|
+
return select?.options.find((option) => option.value === value) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function titleCase(name) {
|
|
30
|
+
return String(name)
|
|
31
|
+
.split(/[-_\s]+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
34
|
+
.join(" ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function presetLabel(t, option) {
|
|
38
|
+
const value = option?.value || "";
|
|
39
|
+
if (!value) return "";
|
|
40
|
+
const key = `permission.${value}`;
|
|
41
|
+
const text = t(key);
|
|
42
|
+
return text === key ? titleCase(option.name || value) : text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function presetDescription(t, option) {
|
|
46
|
+
const value = option?.value || "";
|
|
47
|
+
if (!value) return "";
|
|
48
|
+
const key = `permission.${value}.hint`;
|
|
49
|
+
const text = t(key);
|
|
50
|
+
return text === key ? String(option.description || "") : text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The line the host parses as a preset switch. */
|
|
54
|
+
export function permissionLine(value) {
|
|
55
|
+
return `/permission ${value}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { textFromBlocks } from "./transcript.js";
|
|
2
|
+
|
|
3
|
+
function previewOf(text) {
|
|
4
|
+
return String(text ?? "").replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function textOf(row) {
|
|
8
|
+
if (row.text === null) return null;
|
|
9
|
+
if (typeof row.text === "string") return row.text;
|
|
10
|
+
const content = Array.isArray(row.content) ? row.content : [];
|
|
11
|
+
const text = textFromBlocks(content);
|
|
12
|
+
if (text) return text;
|
|
13
|
+
return content.some((block) => block?.type && block.type !== "text") ? null : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Host queue rows carry `content` plus optional `preview`/`text`. The dock
|
|
18
|
+
* edits only a single text block; image-only rows stay non-editable.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeQueueItems(items) {
|
|
21
|
+
return (Array.isArray(items) ? items : []).flatMap((row) => {
|
|
22
|
+
if (!row || typeof row.id !== "string" || !row.id) return [];
|
|
23
|
+
const content = Array.isArray(row.content) ? row.content : [];
|
|
24
|
+
const text = textOf(row);
|
|
25
|
+
return [{
|
|
26
|
+
id: row.id,
|
|
27
|
+
placement: row.placement === "queued" ? "queued" : String(row.placement || "queued"),
|
|
28
|
+
content,
|
|
29
|
+
text,
|
|
30
|
+
preview: typeof row.preview === "string" && row.preview ? row.preview : previewOf(text),
|
|
31
|
+
}];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const REFERENCES_PATH = "/api/lares/references";
|
|
2
|
+
|
|
3
|
+
export function referencesUrl(sessionId, query = "") {
|
|
4
|
+
const params = new URLSearchParams();
|
|
5
|
+
if (sessionId) params.set("sessionId", sessionId);
|
|
6
|
+
if (query) params.set("query", query);
|
|
7
|
+
const suffix = params.toString();
|
|
8
|
+
return suffix ? `${REFERENCES_PATH}?${suffix}` : REFERENCES_PATH;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function activeReferenceToken(draft, caret = String(draft ?? "").length) {
|
|
12
|
+
const text = String(draft ?? "");
|
|
13
|
+
const end = Math.max(0, Math.min(Number(caret) || 0, text.length));
|
|
14
|
+
const prefix = text.slice(0, end);
|
|
15
|
+
const match = prefix.match(/(?:^|\s)(@(?:"[^"]*|[^\s]*))$/u);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
const token = match[1];
|
|
18
|
+
const start = end - token.length;
|
|
19
|
+
if (token.startsWith('@"')) {
|
|
20
|
+
const query = token.slice(2);
|
|
21
|
+
if (query.includes('"')) return null;
|
|
22
|
+
return { start, end, query, quoted: true };
|
|
23
|
+
}
|
|
24
|
+
const query = token.slice(1);
|
|
25
|
+
if (/\s/.test(query)) return null;
|
|
26
|
+
return { start, end, query, quoted: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatFileMention(candidate, preserveQuote = false) {
|
|
30
|
+
if (!candidate || typeof candidate.path !== "string") return null;
|
|
31
|
+
const directory = candidate.kind === "directory";
|
|
32
|
+
const path = directory ? `${candidate.path.replace(/\/$/, "")}/` : candidate.path;
|
|
33
|
+
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return null;
|
|
34
|
+
if (!(preserveQuote || /\s/u.test(path))) return `@${path}`;
|
|
35
|
+
return directory ? `@"${path}` : `@"${path}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeReferenceCandidates(body, preserveQuote = false) {
|
|
39
|
+
const files = (Array.isArray(body?.files) ? body.files : []).flatMap((candidate) => {
|
|
40
|
+
const mention = formatFileMention(candidate, preserveQuote);
|
|
41
|
+
if (!mention) return [];
|
|
42
|
+
const directory = candidate.kind === "directory";
|
|
43
|
+
const label = candidate.path.slice(candidate.path.lastIndexOf("/") + 1) || candidate.path;
|
|
44
|
+
return [{
|
|
45
|
+
id: `file:${candidate.path}:${directory ? "d" : "f"}`,
|
|
46
|
+
kind: directory ? "directory" : "file",
|
|
47
|
+
label: `${label}${directory ? "/" : ""}`,
|
|
48
|
+
description: candidate.path,
|
|
49
|
+
mention,
|
|
50
|
+
continuation: directory,
|
|
51
|
+
}];
|
|
52
|
+
});
|
|
53
|
+
const sessions = (Array.isArray(body?.sessions) ? body.sessions : []).flatMap((candidate) => {
|
|
54
|
+
if (typeof candidate?.mention !== "string" || !candidate.mention) return [];
|
|
55
|
+
return [{
|
|
56
|
+
id: `session:${candidate.sessionId || candidate.mention}`,
|
|
57
|
+
kind: "session",
|
|
58
|
+
label: candidate.label || candidate.sessionId || candidate.mention,
|
|
59
|
+
description: candidate.cwd || candidate.sessionId || "",
|
|
60
|
+
mention: candidate.mention,
|
|
61
|
+
continuation: false,
|
|
62
|
+
}];
|
|
63
|
+
});
|
|
64
|
+
return [...files, ...sessions];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function base64Url(value) {
|
|
68
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
69
|
+
let binary = "";
|
|
70
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
71
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function sessionReferenceCandidates(sessions, currentSessionId, query = "") {
|
|
75
|
+
const needle = String(query).toLocaleLowerCase();
|
|
76
|
+
return (Array.isArray(sessions) ? sessions : []).flatMap((session) => {
|
|
77
|
+
if (!session?.sessionId || session.sessionId === currentSessionId) return [];
|
|
78
|
+
const label = String(session.title || session.sessionId);
|
|
79
|
+
const haystack = `${label}\n${session.sessionId}\n${session.cwd || ""}`.toLocaleLowerCase();
|
|
80
|
+
if (needle && !haystack.includes(needle)) return [];
|
|
81
|
+
const escaped = label.replace(/[\\\]]/gu, (match) => `\\${match}`);
|
|
82
|
+
return [{
|
|
83
|
+
id: `session:${session.sessionId}`,
|
|
84
|
+
kind: "session",
|
|
85
|
+
label,
|
|
86
|
+
description: session.cwd || session.sessionId,
|
|
87
|
+
mention: `@[${escaped}](dsh-session:${base64Url(session.sessionId)})`,
|
|
88
|
+
continuation: false,
|
|
89
|
+
}];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function insertReference(draft, token, candidate) {
|
|
94
|
+
const text = String(draft ?? "");
|
|
95
|
+
const suffix = candidate.continuation || /\s/.test(text[token.end] ?? "") ? "" : " ";
|
|
96
|
+
const next = `${text.slice(0, token.start)}${candidate.mention}${suffix}${text.slice(token.end)}`;
|
|
97
|
+
return {
|
|
98
|
+
draft: next,
|
|
99
|
+
caret: token.start + candidate.mention.length + suffix.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/larepass/rpc.js
CHANGED
|
@@ -50,11 +50,14 @@ export function unwrapServerResponse(body) {
|
|
|
50
50
|
return { ok: false, rpcId: body.rpcId, error: { code: "invalid-envelope", message: "invalid rpc envelope" } };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function promptPayload(sessionId,
|
|
53
|
+
export function promptPayload(sessionId, textOrContent, timeZone, mode = "queue") {
|
|
54
|
+
const content = Array.isArray(textOrContent)
|
|
55
|
+
? textOrContent
|
|
56
|
+
: [{ type: "text", text: String(textOrContent ?? "") }];
|
|
54
57
|
return {
|
|
55
58
|
sessionId,
|
|
56
|
-
mode: "queue",
|
|
57
|
-
content
|
|
59
|
+
mode: mode === "steer" ? "steer" : "queue",
|
|
60
|
+
content,
|
|
58
61
|
...(timeZone ? { clientTimeZone: timeZone } : {}),
|
|
59
62
|
};
|
|
60
63
|
}
|