@c0sc0s/codex-tags 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/.codex-plugin/plugin.json +24 -0
- package/AGENTS.md +44 -0
- package/CHANGELOG.md +75 -0
- package/README.md +87 -0
- package/README.zh-CN.md +87 -0
- package/assets/README.md +19 -0
- package/assets/banner.png +0 -0
- package/assets/icon.icns +0 -0
- package/assets/logo.png +0 -0
- package/bin/codex-tags.mjs +89 -0
- package/docs/architecture.md +56 -0
- package/docs/compatibility.md +47 -0
- package/docs/development.md +84 -0
- package/docs/distribution.md +65 -0
- package/docs/protocol.md +89 -0
- package/docs/roadmap.md +40 -0
- package/hooks/hooks.json +40 -0
- package/hooks/session-naming.mjs +107 -0
- package/package.json +65 -0
- package/runtime/dist/injected.js +3161 -0
- package/runtime/src/cdp-client.mjs +100 -0
- package/runtime/src/codex-process.mjs +115 -0
- package/runtime/src/content-index.mjs +138 -0
- package/runtime/src/controller-router.mjs +84 -0
- package/runtime/src/controller-state.mjs +17 -0
- package/runtime/src/controller.mjs +290 -0
- package/runtime/src/inject-expression.mjs +49 -0
- package/runtime/src/protocol.d.mts +31 -0
- package/runtime/src/protocol.mjs +43 -0
- package/runtime/src/runtime-target-registry.mjs +92 -0
- package/runtime/src/search-index.mjs +191 -0
- package/runtime/src/session-catalog.mjs +52 -0
- package/runtime/src/settings-repository.mjs +58 -0
- package/runtime/src/tag-settings.d.mts +18 -0
- package/runtime/src/tag-settings.mjs +65 -0
- package/runtime/src/title-format.d.mts +11 -0
- package/runtime/src/title-format.mjs +33 -0
- package/scripts/cli-options.mjs +17 -0
- package/scripts/health.mjs +20 -0
- package/scripts/lifecycle-lock.mjs +21 -0
- package/scripts/manage.mjs +19 -0
- package/scripts/manager-core.mjs +463 -0
- package/skills/doctor/SKILL.md +18 -0
- package/skills/doctor/agents/openai.yaml +4 -0
- package/skills/initial/SKILL.md +22 -0
- package/skills/initial/agents/openai.yaml +4 -0
- package/skills/rename/SKILL.md +20 -0
- package/skills/rename/agents/openai.yaml +4 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export class CdpClient {
|
|
2
|
+
static async connect(target) {
|
|
3
|
+
const socket = new WebSocket(target.webSocketDebuggerUrl);
|
|
4
|
+
await new Promise((resolve, reject) => {
|
|
5
|
+
const timer = setTimeout(() => {
|
|
6
|
+
socket.close();
|
|
7
|
+
reject(new Error(`CDP websocket open timed out for target ${target.id}`));
|
|
8
|
+
}, 3000);
|
|
9
|
+
socket.addEventListener("open", () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
resolve();
|
|
12
|
+
}, { once: true });
|
|
13
|
+
socket.addEventListener("error", () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
reject(new Error(`CDP websocket open failed for target ${target.id}`));
|
|
16
|
+
}, { once: true });
|
|
17
|
+
});
|
|
18
|
+
return new CdpClient(target, socket);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor(target, socket) {
|
|
22
|
+
this.target = target;
|
|
23
|
+
this.socket = socket;
|
|
24
|
+
this.nextCommandId = 1;
|
|
25
|
+
this.pendingCommands = new Map();
|
|
26
|
+
this.bindingHandlers = new Map();
|
|
27
|
+
socket.addEventListener("message", (event) => this.#handleMessage(event.data));
|
|
28
|
+
socket.addEventListener("close", () => this.#rejectPending(new Error(`CDP target ${target.id} disconnected`)));
|
|
29
|
+
socket.addEventListener("error", () => this.#rejectPending(new Error(`CDP target ${target.id} failed`)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get connected() {
|
|
33
|
+
return this.socket.readyState === WebSocket.OPEN;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async command(method, params = {}, timeoutMs = 15_000) {
|
|
37
|
+
if (!this.connected) throw new Error(`CDP target ${this.target.id} is not connected`);
|
|
38
|
+
const id = this.nextCommandId;
|
|
39
|
+
this.nextCommandId += 1;
|
|
40
|
+
return await new Promise((resolve, reject) => {
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
this.pendingCommands.delete(id);
|
|
43
|
+
reject(new Error(`${method} timed out for target ${this.target.id}`));
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
this.pendingCommands.set(id, { resolve, reject, timer, method });
|
|
46
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async evaluate(expression, executionContextId) {
|
|
51
|
+
const params = { expression, awaitPromise: true, returnByValue: true };
|
|
52
|
+
if (Number.isSafeInteger(executionContextId)) params.contextId = executionContextId;
|
|
53
|
+
const response = await this.command("Runtime.evaluate", params);
|
|
54
|
+
if (response?.exceptionDetails) {
|
|
55
|
+
const description = response.exceptionDetails.exception?.description ?? response.exceptionDetails.text;
|
|
56
|
+
throw new Error(description ?? `Runtime.evaluate failed for target ${this.target.id}`);
|
|
57
|
+
}
|
|
58
|
+
return response?.result?.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async addBinding(name, handler) {
|
|
62
|
+
this.bindingHandlers.set(name, handler);
|
|
63
|
+
await this.command("Runtime.enable");
|
|
64
|
+
await this.command("Runtime.addBinding", { name });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
close() {
|
|
68
|
+
this.socket.close();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#handleMessage(rawMessage) {
|
|
72
|
+
let message;
|
|
73
|
+
try {
|
|
74
|
+
message = JSON.parse(rawMessage);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (message.id !== undefined) {
|
|
79
|
+
const pending = this.pendingCommands.get(message.id);
|
|
80
|
+
if (!pending) return;
|
|
81
|
+
this.pendingCommands.delete(message.id);
|
|
82
|
+
clearTimeout(pending.timer);
|
|
83
|
+
if (message.error) pending.reject(new Error(`${pending.method} failed: ${message.error.message ?? "unknown CDP error"}`));
|
|
84
|
+
else pending.resolve(message.result);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (message.method !== "Runtime.bindingCalled") return;
|
|
88
|
+
const handler = this.bindingHandlers.get(message.params?.name);
|
|
89
|
+
if (!handler) return;
|
|
90
|
+
Promise.resolve(handler(message.params)).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#rejectPending(error) {
|
|
94
|
+
for (const { reject, timer } of this.pendingCommands.values()) {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
reject(error);
|
|
97
|
+
}
|
|
98
|
+
this.pendingCommands.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const defaultRun = promisify(execFile);
|
|
7
|
+
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
8
|
+
|
|
9
|
+
export function findCodexApp() {
|
|
10
|
+
for (const appPath of ["/Applications/Codex.app", join(homedir(), "Applications", "Codex.app"), "/Applications/ChatGPT.app"]) {
|
|
11
|
+
try {
|
|
12
|
+
const plist = join(appPath, "Contents", "Info.plist");
|
|
13
|
+
const read = (key) => execFileSync("/usr/bin/plutil", ["-extract", key, "raw", plist], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 2000 }).trim();
|
|
14
|
+
if (read("CFBundleIdentifier") !== "com.openai.codex") continue;
|
|
15
|
+
const executable = read("CFBundleExecutable");
|
|
16
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(executable)) continue;
|
|
17
|
+
return { appPath, executable: join(appPath, "Contents", "MacOS", executable) };
|
|
18
|
+
} catch { /* Uninstalled or unrelated applications are not candidates. */ }
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CodexProcess {
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.port = options.port ?? 9341;
|
|
26
|
+
const detected = options.appPath ? null : findCodexApp();
|
|
27
|
+
this.appPath = options.appPath ?? detected?.appPath ?? "/Applications/Codex.app";
|
|
28
|
+
this.executable = options.executable ?? detected?.executable ?? join(this.appPath, "Contents", "MacOS", "ChatGPT");
|
|
29
|
+
this.run = options.run ?? defaultRun;
|
|
30
|
+
this.spawn = options.spawn ?? spawn;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async isRunning() {
|
|
34
|
+
const { stdout } = await this.run("/bin/ps", ["-axo", "command="]);
|
|
35
|
+
return stdout.split("\n").some((command) => {
|
|
36
|
+
const value = command.trim();
|
|
37
|
+
return value === this.executable || value.startsWith(`${this.executable} `);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async hasCdpLaunchArguments() {
|
|
42
|
+
const { stdout } = await this.run("/bin/ps", ["-axo", "command="]);
|
|
43
|
+
return stdout.split("\n").some((command) => {
|
|
44
|
+
const value = command.trim();
|
|
45
|
+
const isCodex = value === this.executable || value.startsWith(`${this.executable} `);
|
|
46
|
+
return isCodex && value.includes(`--remote-debugging-port=${this.port}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ownsCdpEndpoint() {
|
|
51
|
+
let stdout;
|
|
52
|
+
try {
|
|
53
|
+
({ stdout } = await this.run("/usr/sbin/lsof", [
|
|
54
|
+
"-nP", `-iTCP:${this.port}`, "-sTCP:LISTEN", "-t",
|
|
55
|
+
]));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === 1) return false;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const value of stdout.trim().split(/\s+/u)) {
|
|
62
|
+
let pid = Number(value);
|
|
63
|
+
for (let depth = 0; Number.isSafeInteger(pid) && pid > 0 && depth < 8; depth += 1) {
|
|
64
|
+
try {
|
|
65
|
+
const [{ stdout: command }, { stdout: parent }] = await Promise.all([
|
|
66
|
+
this.run("/bin/ps", ["-p", String(pid), "-o", "command="]),
|
|
67
|
+
this.run("/bin/ps", ["-p", String(pid), "-o", "ppid="]),
|
|
68
|
+
]);
|
|
69
|
+
if (command.trim() === this.executable || command.trim().startsWith(`${this.executable} `)) return true;
|
|
70
|
+
pid = Number(parent.trim());
|
|
71
|
+
} catch {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async portIsListening() {
|
|
80
|
+
try {
|
|
81
|
+
const { stdout } = await this.run("/usr/sbin/lsof", ["-nP", `-iTCP:${this.port}`, "-sTCP:LISTEN", "-t"]);
|
|
82
|
+
return stdout.trim().length > 0;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.code === 1) return false;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async cdpIsReady(discoverTargets) {
|
|
90
|
+
try {
|
|
91
|
+
return await this.ownsCdpEndpoint() && (await discoverTargets()).length > 0;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async waitForCdp(discoverTargets, timeoutMs = 30_000) {
|
|
98
|
+
const deadline = Date.now() + timeoutMs;
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
if (await this.cdpIsReady(discoverTargets)) return;
|
|
101
|
+
await wait(250);
|
|
102
|
+
}
|
|
103
|
+
throw new Error("等待 Codex CDP 端口超时");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async launchWithCdp(discoverTargets) {
|
|
107
|
+
if (await this.isRunning()) throw new Error("Codex is already running without Tags. Quit Codex completely, then open Codex Tags.app. The running app was not restarted.");
|
|
108
|
+
const child = this.spawn(this.executable, [
|
|
109
|
+
"--remote-debugging-address=127.0.0.1",
|
|
110
|
+
`--remote-debugging-port=${this.port}`,
|
|
111
|
+
], { detached: true, stdio: "ignore" });
|
|
112
|
+
child.unref();
|
|
113
|
+
await this.waitForCdp(discoverTargets);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const SESSION_ROOTS = [
|
|
7
|
+
join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "sessions"),
|
|
8
|
+
join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "archived_sessions"),
|
|
9
|
+
];
|
|
10
|
+
const TEXT_FIELD = '"text":"';
|
|
11
|
+
const MAX_FIELD_LENGTH = 24_000;
|
|
12
|
+
const MAX_THREAD_LENGTH = 500_000;
|
|
13
|
+
const SESSION_FILE_DISCOVERY_TTL_MS = 30_000;
|
|
14
|
+
let sessionFiles = null;
|
|
15
|
+
let sessionFilesScannedAt = 0;
|
|
16
|
+
|
|
17
|
+
export async function discoverSessionFiles(force = false) {
|
|
18
|
+
if (!force && sessionFiles && Date.now() - sessionFilesScannedAt < SESSION_FILE_DISCOVERY_TTL_MS) return sessionFiles;
|
|
19
|
+
const files = new Map();
|
|
20
|
+
for (const root of SESSION_ROOTS) {
|
|
21
|
+
let paths = [];
|
|
22
|
+
try { paths = await readdir(root, { recursive: true }); } catch { continue; }
|
|
23
|
+
for (const relativePath of paths) {
|
|
24
|
+
if (!relativePath.endsWith(".jsonl")) continue;
|
|
25
|
+
const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(basename(relativePath));
|
|
26
|
+
if (match) files.set(match[1], join(root, relativePath));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
sessionFiles = files;
|
|
30
|
+
sessionFilesScannedAt = Date.now();
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeJsonString(value) {
|
|
35
|
+
try { return JSON.parse(`"${value}"`); } catch {
|
|
36
|
+
return value.replace(/\\n/g, " ").replace(/\\r/g, " ").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function extractConversationText(path) {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let totalLength = 0;
|
|
43
|
+
let prefix = "";
|
|
44
|
+
let eligible = false;
|
|
45
|
+
let role = "";
|
|
46
|
+
let inText = false;
|
|
47
|
+
let escaped = false;
|
|
48
|
+
let rawText = "";
|
|
49
|
+
let fieldTruncated = false;
|
|
50
|
+
let patternCarry = "";
|
|
51
|
+
|
|
52
|
+
const resetLine = () => {
|
|
53
|
+
prefix = "";
|
|
54
|
+
eligible = false;
|
|
55
|
+
role = "";
|
|
56
|
+
inText = false;
|
|
57
|
+
escaped = false;
|
|
58
|
+
rawText = "";
|
|
59
|
+
fieldTruncated = false;
|
|
60
|
+
patternCarry = "";
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const finishText = () => {
|
|
64
|
+
let text = decodeJsonString(rawText).replace(/\s+/gu, " ").trim();
|
|
65
|
+
const requestMarker = text.lastIndexOf("## My request:");
|
|
66
|
+
if (requestMarker !== -1) text = text.slice(requestMarker + "## My request:".length).trim();
|
|
67
|
+
if (text) {
|
|
68
|
+
const clipped = text.slice(0, MAX_THREAD_LENGTH - totalLength);
|
|
69
|
+
chunks.push({ role, text: clipped + (fieldTruncated ? "…" : "") });
|
|
70
|
+
totalLength += clipped.length;
|
|
71
|
+
}
|
|
72
|
+
inText = false;
|
|
73
|
+
escaped = false;
|
|
74
|
+
rawText = "";
|
|
75
|
+
fieldTruncated = false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const scanEligibleSegment = (segment) => {
|
|
79
|
+
let offset = 0;
|
|
80
|
+
while (offset < segment.length && totalLength < MAX_THREAD_LENGTH) {
|
|
81
|
+
if (!inText) {
|
|
82
|
+
const searchable = patternCarry + segment.slice(offset);
|
|
83
|
+
const fieldIndex = searchable.indexOf(TEXT_FIELD);
|
|
84
|
+
if (fieldIndex === -1) {
|
|
85
|
+
patternCarry = searchable.slice(-(TEXT_FIELD.length - 1));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
offset += Math.max(0, fieldIndex + TEXT_FIELD.length - patternCarry.length);
|
|
89
|
+
patternCarry = "";
|
|
90
|
+
inText = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let cursor = offset;
|
|
94
|
+
for (; cursor < segment.length; cursor += 1) {
|
|
95
|
+
const character = segment[cursor];
|
|
96
|
+
if (escaped) {
|
|
97
|
+
if (!fieldTruncated) rawText += character;
|
|
98
|
+
escaped = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (character === "\\") {
|
|
102
|
+
if (!fieldTruncated) rawText += character;
|
|
103
|
+
escaped = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (character === '"') {
|
|
107
|
+
finishText();
|
|
108
|
+
offset = cursor + 1;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (!fieldTruncated) {
|
|
112
|
+
rawText += character;
|
|
113
|
+
if (rawText.length >= MAX_FIELD_LENGTH) fieldTruncated = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (cursor >= segment.length) return;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for await (const block of createReadStream(path, { encoding: "utf8", highWaterMark: 64 * 1024 })) {
|
|
121
|
+
let blockOffset = 0;
|
|
122
|
+
while (blockOffset < block.length) {
|
|
123
|
+
const newline = block.indexOf("\n", blockOffset);
|
|
124
|
+
const lineEnds = newline !== -1;
|
|
125
|
+
const segment = block.slice(blockOffset, lineEnds ? newline : block.length);
|
|
126
|
+
if (!eligible) {
|
|
127
|
+
if (prefix.length < 4096) prefix += segment.slice(0, 4096 - prefix.length);
|
|
128
|
+
if (prefix.includes('"type":"UserMessage"')) { eligible = true; role = "你"; }
|
|
129
|
+
else if (prefix.includes('"type":"response_item"') && prefix.includes('"role":"user"')) { eligible = true; role = "你"; }
|
|
130
|
+
else if (prefix.includes('"type":"response_item"') && prefix.includes('"role":"assistant"')) { eligible = true; role = "Codex"; }
|
|
131
|
+
}
|
|
132
|
+
if (eligible) scanEligibleSegment(segment);
|
|
133
|
+
if (lineEnds) resetLine();
|
|
134
|
+
blockOffset = lineEnds ? newline + 1 : block.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return chunks;
|
|
138
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createRuntimeMessage, parseRuntimeMessage, RuntimeMessageType } from "./protocol.mjs";
|
|
2
|
+
|
|
3
|
+
export class ControllerRouter {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.searchIndex = options.searchIndex;
|
|
6
|
+
this.settingsRepository = options.settingsRepository;
|
|
7
|
+
this.getSettings = options.getSettings;
|
|
8
|
+
this.onSettingsChanged = options.onSettingsChanged;
|
|
9
|
+
this.waitForIndex = options.waitForIndex;
|
|
10
|
+
this.getIndexStatus = options.getIndexStatus;
|
|
11
|
+
this.send = options.send;
|
|
12
|
+
this.openSession = options.openSession;
|
|
13
|
+
this.latestSearchRequestIds = new Map();
|
|
14
|
+
this.handlers = new Map([
|
|
15
|
+
[RuntimeMessageType.settingsGet, this.handleSettingsGet.bind(this)],
|
|
16
|
+
[RuntimeMessageType.settingsUpdate, this.handleSettingsUpdate.bind(this)],
|
|
17
|
+
[RuntimeMessageType.searchRequest, this.handleSearchRequest.bind(this)],
|
|
18
|
+
[RuntimeMessageType.navigationOpen, async (_client, request) => {
|
|
19
|
+
const threadId = typeof request.payload.threadId === "string" ? request.payload.threadId.replace(/^local:/u, "") : "";
|
|
20
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(threadId)) await this.openSession?.(threadId);
|
|
21
|
+
}],
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async handle(client, params) {
|
|
26
|
+
const parsed = parseRuntimeMessage(params.payload);
|
|
27
|
+
if (!parsed.ok) return false;
|
|
28
|
+
const handler = this.handlers.get(parsed.message.type);
|
|
29
|
+
if (!handler) return false;
|
|
30
|
+
await handler(client, parsed.message, params.executionContextId);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
forgetTarget(targetId) {
|
|
35
|
+
this.latestSearchRequestIds.delete(targetId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async sendSettingsSnapshot(client, executionContextId) {
|
|
39
|
+
await this.send(
|
|
40
|
+
client,
|
|
41
|
+
createRuntimeMessage(RuntimeMessageType.settingsSnapshot, { settings: this.getSettings() }),
|
|
42
|
+
executionContextId,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async handleSettingsGet(client, _request, executionContextId) {
|
|
47
|
+
await this.sendSettingsSnapshot(client, executionContextId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handleSettingsUpdate(client, request, executionContextId) {
|
|
51
|
+
try {
|
|
52
|
+
if (!Array.isArray(request.payload.tags) || request.payload.tags.length > 32) throw new Error("Invalid tag definitions");
|
|
53
|
+
const result = await this.settingsRepository.write(request.payload.tags);
|
|
54
|
+
await this.onSettingsChanged(result.settings);
|
|
55
|
+
} catch {
|
|
56
|
+
await this.sendSettingsSnapshot(client, executionContextId);
|
|
57
|
+
await this.send(client, createRuntimeMessage(RuntimeMessageType.settingsError, {}), executionContextId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async handleSearchRequest(client, request, executionContextId) {
|
|
62
|
+
if (!Number.isSafeInteger(request.requestId)) return;
|
|
63
|
+
const targetId = client.target.id;
|
|
64
|
+
this.latestSearchRequestIds.set(targetId, request.requestId);
|
|
65
|
+
await this.waitForIndex();
|
|
66
|
+
if (this.latestSearchRequestIds.get(targetId) !== request.requestId) return;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const items = this.searchIndex.search(request.payload);
|
|
70
|
+
await this.send(client, createRuntimeMessage(RuntimeMessageType.searchResult, {
|
|
71
|
+
query: request.payload.query,
|
|
72
|
+
items,
|
|
73
|
+
indexStatus: this.getIndexStatus(),
|
|
74
|
+
}, request.requestId), executionContextId);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await this.send(client, createRuntimeMessage(RuntimeMessageType.searchResult, {
|
|
77
|
+
query: request.payload.query,
|
|
78
|
+
items: [],
|
|
79
|
+
indexStatus: this.getIndexStatus(),
|
|
80
|
+
error: error instanceof Error ? error.message : "Search failed",
|
|
81
|
+
}, request.requestId), executionContextId).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function parseControllerPid(raw) {
|
|
2
|
+
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
3
|
+
if (!trimmed) return null;
|
|
4
|
+
let value;
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(trimmed);
|
|
7
|
+
value = typeof parsed === "number" ? parsed : parsed?.pid;
|
|
8
|
+
} catch {
|
|
9
|
+
value = Number(trimmed);
|
|
10
|
+
}
|
|
11
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isOwnedControllerCommand(command, scriptPath) {
|
|
15
|
+
if (typeof command !== "string" || typeof scriptPath !== "string" || !scriptPath) return false;
|
|
16
|
+
return command.trim().endsWith(`${scriptPath} watch`);
|
|
17
|
+
}
|