claude4arc 0.5.0

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/lib/editors.js ADDED
@@ -0,0 +1,373 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function editorLibrary() {
4
+ const KINDS = [
5
+ { kind: "monaco", selector: ".monaco-editor" },
6
+ { kind: "codemirror6", selector: ".cm-editor, .cm-content" },
7
+ { kind: "codemirror5", selector: ".CodeMirror" },
8
+ { kind: "ace", selector: ".ace_editor" },
9
+ { kind: "ckeditor5", selector: ".ck-editor__editable, .ck-editor" },
10
+ { kind: "lexical", selector: "[data-lexical-editor]" },
11
+ { kind: "quill", selector: ".ql-container, .ql-editor" },
12
+ { kind: "prosemirror", selector: ".ProseMirror" },
13
+ { kind: "tinymce", selector: ".tox-tinymce, .mce-tinymce, .mce-content-body" },
14
+ { kind: "draft", selector: ".DraftEditor-root, [data-contents=\"true\"]" },
15
+ { kind: "slate", selector: "[data-slate-editor]" },
16
+ ];
17
+
18
+ const CODE_KINDS = new Set(["monaco", "codemirror6", "codemirror5", "ace"]);
19
+
20
+ const EDITOR_INPUTS = "textarea.inputarea, textarea.ime-text-area, textarea.ace_text-input, .CodeMirror > div > textarea";
21
+
22
+ const matchKind = (node) => KINDS.find((entry) => node.matches(entry.selector)) ?? null;
23
+
24
+ const isForeignInput = (element) =>
25
+ ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(element.tagName) && !element.matches(EDITOR_INPUTS);
26
+
27
+ const locate = (element) => {
28
+ if (!element || isForeignInput(element)) return null;
29
+ for (let node = element; node; node = node.parentElement) {
30
+ const entry = matchKind(node);
31
+ if (entry) return { kind: entry.kind, root: node };
32
+ }
33
+ if (element.tagName === "IFRAME") {
34
+ try {
35
+ const body = element.contentDocument?.body;
36
+ if (body?.matches(".mce-content-body")) return { kind: "tinymce", root: body };
37
+ } catch {}
38
+ }
39
+ const found = KINDS.map((entry) => ({ kind: entry.kind, root: element.querySelector?.(entry.selector) }))
40
+ .filter((entry) => entry.root)
41
+ .sort((a, b) => (a.root.compareDocumentPosition(b.root) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
42
+ return found[0] ?? null;
43
+ };
44
+
45
+ const windowOf = (node) => node.ownerDocument.defaultView;
46
+
47
+ const related = (a, b) => Boolean(a && b && (a === b || a.contains(b) || b.contains(a)));
48
+
49
+ const normalize = (text) => String(text ?? "").replace(/\r\n?/g, "\n").replace(/\u00a0/g, " ");
50
+
51
+ const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
52
+
53
+ const textToHtml = (text, emptyBlock) =>
54
+ normalize(text)
55
+ .split("\n")
56
+ .map((line) => {
57
+ if (!line) return `<p>${emptyBlock}</p>`;
58
+ const kept = line.replace(/^ +| +$| {2,}/g, (run) => "\u00a0".repeat(run.length));
59
+ return `<p>${escapeHtml(kept).replace(/\u00a0/g, "&nbsp;")}</p>`;
60
+ })
61
+ .join("");
62
+
63
+ const inlineText = (node) => {
64
+ if (node.nodeType === Node.TEXT_NODE) return node.data;
65
+ if (node.nodeType !== Node.ELEMENT_NODE) return "";
66
+ if (node.getAttribute("data-mce-bogus")) return "";
67
+ if (node.tagName === "BR") return node.nextSibling ? "\n" : "";
68
+ return [...node.childNodes].map(inlineText).join("");
69
+ };
70
+
71
+ const blockText = (container) => {
72
+ const blocks = [...container.children].filter((child) => !child.getAttribute("data-mce-bogus"));
73
+ if (!blocks.length) return normalize(container.textContent);
74
+ return normalize(blocks.map(inlineText).join("\n"));
75
+ };
76
+
77
+ const monacoApi = (win) => {
78
+ if (win.monaco?.editor?.getEditors) return win.monaco;
79
+ try {
80
+ const loaded = win.require?.("vs/editor/editor.main");
81
+ if (loaded?.editor?.getEditors) return loaded;
82
+ } catch {}
83
+ return null;
84
+ };
85
+
86
+ const monacoEditor = (root) => {
87
+ const api = monacoApi(windowOf(root));
88
+ if (!api) return null;
89
+ const editors = api.editor.getEditors();
90
+ return editors.find((editor) => editor.getDomNode()?.contains(root)) ?? editors.find((editor) => related(editor.getDomNode(), root)) ?? null;
91
+ };
92
+
93
+ const codeMirror6View = (root) => {
94
+ const content = root.matches(".cm-content") ? root : root.querySelector(".cm-content");
95
+ if (!content) return null;
96
+ const holder = content.cmView ?? content.cmTile;
97
+ const view = holder?.view ?? holder?.rootView?.view ?? holder?.root?.view ?? null;
98
+ return view?.state && view.dispatch ? view : null;
99
+ };
100
+
101
+ const codeMirror5 = (root) => root.CodeMirror ?? null;
102
+
103
+ const aceEditor = (root) => {
104
+ if (root.env?.editor) return root.env.editor;
105
+ try {
106
+ return windowOf(root).ace?.edit(root) ?? null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ };
111
+
112
+ const quillEditor = (root) => {
113
+ const container = root.closest(".ql-container") ?? root.querySelector(".ql-container") ?? root;
114
+ if (container.__quill) return container.__quill;
115
+ try {
116
+ return windowOf(root).Quill?.find(container) || null;
117
+ } catch {
118
+ return null;
119
+ }
120
+ };
121
+
122
+ const proseMirrorView = (root) => {
123
+ const dom = root.matches(".ProseMirror") ? root : root.querySelector(".ProseMirror");
124
+ if (!dom) return null;
125
+ const candidates = [dom.editor?.view, dom.pmViewDesc?.view, windowOf(dom).view, windowOf(dom).editor?.view];
126
+ return candidates.find((view) => view?.state && view.dispatch && view.dom === dom) ?? null;
127
+ };
128
+
129
+ const lexicalEditor = (root) => {
130
+ const dom = root.matches("[data-lexical-editor]") ? root : root.querySelector("[data-lexical-editor]");
131
+ return dom?.__lexicalEditor ?? null;
132
+ };
133
+
134
+ const ckEditor = (root) => {
135
+ for (let node = root; node; node = node.parentElement?.closest(".ck-editor__editable")) {
136
+ if (node.ckeditorInstance) return node.ckeditorInstance;
137
+ }
138
+ return [...root.querySelectorAll(".ck-editor__editable")].find((node) => node.ckeditorInstance)?.ckeditorInstance ?? null;
139
+ };
140
+
141
+ const tinyEditor = (root) => {
142
+ const windows = [windowOf(root)];
143
+ try {
144
+ if (windowOf(root).parent !== windowOf(root)) windows.push(windowOf(root).parent);
145
+ } catch {}
146
+ for (const win of windows) {
147
+ let editors = [];
148
+ try {
149
+ const all = win.tinymce?.get?.();
150
+ editors = Array.isArray(all) ? all : Object.values(win.tinymce?.editors ?? {});
151
+ } catch {}
152
+ const match = editors.find(
153
+ (editor) =>
154
+ editor?.getBody &&
155
+ (editor.getBody() === root || related(editor.getContainer?.(), root) || related(editor.getElement?.(), root)),
156
+ );
157
+ if (match) return match;
158
+ }
159
+ return null;
160
+ };
161
+
162
+ const lexicalText = (node) => {
163
+ if (node.type === "text") return node.text ?? "";
164
+ if (node.type === "linebreak") return "\n";
165
+ if (node.type === "tab") return "\t";
166
+ return (node.children ?? []).map(lexicalText).join("");
167
+ };
168
+
169
+ const lexicalState = (text) => ({
170
+ root: {
171
+ type: "root",
172
+ version: 1,
173
+ direction: null,
174
+ format: "",
175
+ indent: 0,
176
+ children: normalize(text)
177
+ .split("\n")
178
+ .map((line) => ({
179
+ type: "paragraph",
180
+ version: 1,
181
+ direction: null,
182
+ format: "",
183
+ indent: 0,
184
+ textFormat: 0,
185
+ textStyle: "",
186
+ children: line ? [{ type: "text", version: 1, text: line, detail: 0, format: 0, mode: "normal", style: "" }] : [],
187
+ })),
188
+ },
189
+ });
190
+
191
+ const ckText = (node) => {
192
+ if (typeof node.data === "string") return node.data;
193
+ if (node.name === "softBreak") return "\n";
194
+ return node.getChildren ? [...node.getChildren()].map(ckText).join("") : "";
195
+ };
196
+
197
+ const proseMirrorText = (view) => {
198
+ const doc = view.state.doc;
199
+ return doc.textBetween(0, doc.content.size, "\n", "\n");
200
+ };
201
+
202
+ const HANDLERS = {
203
+ monaco: {
204
+ find: monacoEditor,
205
+ focus: (editor) => editor.focus(),
206
+ get: (editor) => editor.getValue(),
207
+ set: (editor, text) => {
208
+ const model = editor.getModel();
209
+ editor.pushUndoStop();
210
+ const applied = editor.executeEdits("claude4arc", [{ range: model.getFullModelRange(), text, forceMoveMarkers: true }]);
211
+ if (!applied) model.setValue(text);
212
+ editor.pushUndoStop();
213
+ },
214
+ },
215
+ codemirror6: {
216
+ find: codeMirror6View,
217
+ focus: (view) => view.focus(),
218
+ get: (view) => view.state.doc.toString(),
219
+ set: (view, text) => {
220
+ view.dispatch({
221
+ changes: { from: 0, to: view.state.doc.length, insert: text },
222
+ selection: { anchor: text.length },
223
+ userEvent: "input.paste",
224
+ });
225
+ },
226
+ },
227
+ codemirror5: {
228
+ find: codeMirror5,
229
+ focus: (editor) => editor.focus(),
230
+ get: (editor) => editor.getValue(),
231
+ set: (editor, text) => {
232
+ editor.setValue(text);
233
+ editor.setCursor(editor.lineCount(), 0);
234
+ },
235
+ },
236
+ ace: {
237
+ find: aceEditor,
238
+ focus: (editor) => editor.focus(),
239
+ get: (editor) => editor.getValue(),
240
+ set: (editor, text) => editor.setValue(text, 1),
241
+ },
242
+ quill: {
243
+ find: quillEditor,
244
+ focus: (editor) => editor.focus(),
245
+ get: (editor) => editor.getText().replace(/\n$/, ""),
246
+ set: (editor, text) => editor.setText(normalize(text), "user"),
247
+ },
248
+ prosemirror: {
249
+ find: proseMirrorView,
250
+ focus: (view) => view.focus(),
251
+ get: proseMirrorText,
252
+ set: (view, text) => {
253
+ const { state } = view;
254
+ const schema = state.schema;
255
+ const type = schema.nodes.paragraph ?? schema.topNodeType.contentMatch.defaultType;
256
+ const blocks = normalize(text)
257
+ .split("\n")
258
+ .map((line) => type.create(null, line ? schema.text(line) : null));
259
+ view.dispatch(state.tr.replaceWith(0, state.doc.content.size, blocks).scrollIntoView());
260
+ },
261
+ },
262
+ lexical: {
263
+ find: lexicalEditor,
264
+ focus: (editor) => editor.getRootElement()?.focus(),
265
+ get: (editor) => normalize((editor.getEditorState().toJSON().root.children ?? []).map(lexicalText).join("\n")),
266
+ set: (editor, text) => {
267
+ editor.setEditorState(editor.parseEditorState(JSON.stringify(lexicalState(text))));
268
+ },
269
+ },
270
+ ckeditor5: {
271
+ find: ckEditor,
272
+ focus: (editor) => editor.editing.view.focus(),
273
+ get: (editor) => normalize([...editor.model.document.getRoot().getChildren()].map(ckText).join("\n")),
274
+ set: (editor, text) => editor.setData(textToHtml(text, "")),
275
+ },
276
+ tinymce: {
277
+ find: tinyEditor,
278
+ focus: (editor) => editor.focus(),
279
+ get: (editor) => blockText(editor.getBody()),
280
+ set: (editor, text) => {
281
+ const html = textToHtml(text, "<br>");
282
+ if (editor.undoManager?.transact) editor.undoManager.transact(() => editor.setContent(html));
283
+ else editor.setContent(html);
284
+ editor.save?.();
285
+ },
286
+ },
287
+ };
288
+
289
+ const instanceFor = (element) => {
290
+ const located = locate(element);
291
+ if (!located) return { kind: null, instance: null };
292
+ const handler = HANDLERS[located.kind];
293
+ let instance = null;
294
+ try {
295
+ instance = handler?.find(located.root) ?? null;
296
+ } catch {}
297
+ return { kind: located.kind, root: located.root, handler, instance };
298
+ };
299
+
300
+ const detect = (element) => locate(element)?.kind ?? null;
301
+
302
+ const editableOf = (root) => {
303
+ if (root.isContentEditable) return root;
304
+ return root.querySelector(`${EDITOR_INPUTS}, [contenteditable=true], [contenteditable=""]`);
305
+ };
306
+
307
+ const prepare = (element) => {
308
+ const { kind, root, handler, instance } = instanceFor(element);
309
+ if (!kind) return null;
310
+ const view = windowOf(root);
311
+ let box = root.getBoundingClientRect();
312
+ if (box.bottom < 0 || box.top > view.innerHeight || box.right < 0 || box.left > view.innerWidth) {
313
+ root.scrollIntoView({ block: "center", behavior: "instant" });
314
+ box = root.getBoundingClientRect();
315
+ }
316
+ try {
317
+ if (instance) handler.focus(instance);
318
+ else editableOf(root)?.focus();
319
+ } catch {}
320
+ const top = Math.max(box.top, 0);
321
+ const bottom = Math.min(box.bottom, view.innerHeight);
322
+ return {
323
+ kind,
324
+ api: Boolean(instance),
325
+ root,
326
+ x: Math.round(box.left + Math.min(box.width / 2, 40)),
327
+ y: Math.round(top + Math.min((bottom - top) / 2, 20)),
328
+ };
329
+ };
330
+
331
+ const codeEditors = () => {
332
+ const results = [];
333
+ for (const node of document.querySelectorAll(".monaco-editor, .cm-editor, .CodeMirror, .ace_editor")) {
334
+ if (!node.getClientRects().length) continue;
335
+ const { kind, handler, instance } = instanceFor(node);
336
+ if (!CODE_KINDS.has(kind) || !instance) continue;
337
+ try {
338
+ results.push({ kind, value: normalize(handler.get(instance)) });
339
+ } catch {}
340
+ }
341
+ return results;
342
+ };
343
+
344
+ const getValue = (element) => {
345
+ const { kind, root, handler, instance } = instanceFor(element);
346
+ if (!kind) return null;
347
+ if (instance) return normalize(handler.get(instance));
348
+ if (kind === "monaco") return normalize(root.querySelector(".view-lines")?.innerText ?? "").replace(/\n$/, "");
349
+ const editable = root.isContentEditable ? root : root.querySelector("[contenteditable=true]") ?? root;
350
+ return blockText(editable);
351
+ };
352
+
353
+ const setValue = (element, text) => {
354
+ const value = String(text ?? "");
355
+ const { kind, handler, instance } = instanceFor(element);
356
+ if (!kind || !instance) return { kind, ok: false };
357
+ try {
358
+ handler.set(instance, value);
359
+ } catch (error) {
360
+ return { kind, ok: false, error: String(error?.message ?? error) };
361
+ }
362
+ const result = normalize(handler.get(instance));
363
+ return { kind, ok: result === normalize(value), value: result };
364
+ };
365
+
366
+ return { detect, prepare, setValue, getValue, codeEditors };
367
+ }
368
+
369
+ const SOURCE = editorLibrary.toString();
370
+
371
+ export const EDITOR_KEY = `__arcForClaudeEditors_${createHash("sha1").update(SOURCE).digest("hex").slice(0, 10)}`;
372
+
373
+ export const EDITOR_PRELUDE = `(globalThis[${JSON.stringify(EDITOR_KEY)}] ||= (${SOURCE})())`;
package/lib/frames.js ADDED
@@ -0,0 +1,39 @@
1
+ export const ROOT_INPUT = { sessionId: null, x: 0, y: 0 };
2
+
3
+ export const ROOT_CONTEXT = { sessionId: null, contextId: null, input: ROOT_INPUT, offset: { x: 0, y: 0 }, prefix: "", depth: 0 };
4
+
5
+ export const scopeOf = (context) => ({ sessionId: context.sessionId, contextId: context.contextId });
6
+
7
+ export const CLOSED_AWARE = new Set(["snapshot", "find", "resolve", "count", "seekProbe"]);
8
+
9
+ function registerClosedRoot(key) {
10
+ let view = window;
11
+ while (view) {
12
+ try {
13
+ view[key]?.registerClosedRoot(this);
14
+ } catch {}
15
+ let parent = view.parent === view ? null : view.parent;
16
+ try {
17
+ void parent?.[key];
18
+ } catch {
19
+ parent = null;
20
+ }
21
+ view = parent;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ export const REGISTER_CLOSED_ROOT = registerClosedRoot.toString();
27
+
28
+ export function splitFrameSelector(selector) {
29
+ const text = String(selector).trim();
30
+ const match = text.match(/^@(\d+(?:\.\d+)*)(?:\s*>>\s*(?!nth=)([\s\S]+))?$/);
31
+ if (!match) return { path: [], local: text };
32
+ const parts = match[1].split(".").map(Number);
33
+ if (match[2] !== undefined) return { path: parts, local: match[2].trim() };
34
+ return { path: parts.slice(0, -1), local: `@${parts.at(-1)}` };
35
+ }
36
+
37
+ export const pathOf = (context) => context.prefix.split(".").filter(Boolean).map(Number);
38
+
39
+ export const prefixRefs = (text, prefix) => (prefix ? text.replace(/@(\d+)/g, `@${prefix}$1`) : text);
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { STATE_DIR, LOG_PATH } from "./paths.js";
4
+
5
+ const HOUR = 3_600_000;
6
+
7
+ export const SCREENSHOT_DIR = path.join(STATE_DIR, "screenshots");
8
+ export const SCREENSHOT_LIMITS = { maxAge: 24 * HOUR, keep: 100 };
9
+ export const LOG_LIMIT = 512 * 1024;
10
+
11
+ async function filesIn(directory) {
12
+ const names = await fs.readdir(directory).catch(() => []);
13
+ const entries = await Promise.all(
14
+ names.map(async (name) => {
15
+ const file = path.join(directory, name);
16
+ const stat = await fs.stat(file).catch(() => null);
17
+ return stat?.isFile() ? { file, name, mtime: stat.mtimeMs } : null;
18
+ }),
19
+ );
20
+ return entries.filter(Boolean).sort((a, b) => b.mtime - a.mtime);
21
+ }
22
+
23
+ export function expired(entries, { maxAge, keep, now = Date.now() }) {
24
+ return entries.filter((entry, index) => index >= keep || now - entry.mtime > maxAge);
25
+ }
26
+
27
+ export async function pruneScreenshots({ all = false, now = Date.now() } = {}) {
28
+ const entries = await filesIn(SCREENSHOT_DIR);
29
+ const doomed = all ? entries : expired(entries, { ...SCREENSHOT_LIMITS, now });
30
+ await Promise.all(doomed.map((entry) => fs.rm(entry.file, { force: true })));
31
+ return doomed.length;
32
+ }
33
+
34
+ export async function pruneTempFiles({ now = Date.now() } = {}) {
35
+ const entries = await filesIn(STATE_DIR);
36
+ const doomed = entries.filter((entry) => entry.name.endsWith(".tmp") && now - entry.mtime > HOUR);
37
+ await Promise.all(doomed.map((entry) => fs.rm(entry.file, { force: true })));
38
+ return doomed.length;
39
+ }
40
+
41
+ export async function rotateLog(limit = LOG_LIMIT) {
42
+ const stat = await fs.stat(LOG_PATH).catch(() => null);
43
+ if (!stat || stat.size <= limit) return false;
44
+ await fs.rename(LOG_PATH, `${LOG_PATH}.1`);
45
+ return true;
46
+ }