@olares/lares-core 0.15.5 → 0.15.7
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 +226 -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/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/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
|
}
|