ai-worklens-agent 0.1.2 → 0.1.4
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 +3 -3
- package/package.json +7 -2
- package/src/config.mjs +100 -9
- package/src/hook-adapter.mjs +150 -12
- package/src/install.mjs +337 -17
- package/src/protocol/client-update-policy.mjs +2 -2
- package/src/protocol/event-types.mjs +35 -3
- package/src/publish-npm.mjs +64 -8
- package/src/uploader.mjs +102 -0
package/README.md
CHANGED
|
@@ -36,8 +36,8 @@ npm run mcp
|
|
|
36
36
|
如果管理员已经把员工端发布到 npm 或企业私有 npm 源,可以使用 `npx` 首次安装:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.
|
|
40
|
-
--server-url http://
|
|
39
|
+
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.4 worklens-agent-install \
|
|
40
|
+
--server-url http://192.168.1.241:8797 \
|
|
41
41
|
--tool codex \
|
|
42
42
|
--employee-pinyin zhangsan
|
|
43
43
|
```
|
|
@@ -60,7 +60,7 @@ NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
|
|
|
60
60
|
如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
curl -fL http://
|
|
63
|
+
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.4.sh \
|
|
64
64
|
-o ai-worklens-install.sh
|
|
65
65
|
chmod +x ai-worklens-install.sh
|
|
66
66
|
./ai-worklens-install.sh zhangsan
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-worklens-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Employee-side collector agent for AI WorkLens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,5 +18,10 @@
|
|
|
18
18
|
"claude-code",
|
|
19
19
|
"opencode"
|
|
20
20
|
],
|
|
21
|
-
"license": "UNLICENSED"
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"private": false,
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
}
|
|
22
27
|
}
|
package/src/config.mjs
CHANGED
|
@@ -22,6 +22,92 @@ function readJson(filePath) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function readText(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(filePath)) return "";
|
|
28
|
+
return fs.readFileSync(filePath, "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function firstValue(...values) {
|
|
35
|
+
return values.map((value) => String(value || "").trim()).find(Boolean) || "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function knownModel(model) {
|
|
39
|
+
return Boolean(model?.label && model.label !== "unknown");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rootTomlString(content, key) {
|
|
43
|
+
const keyPattern = new RegExp(`^${key}\\s*=\\s*["']([^"']+)["']`);
|
|
44
|
+
for (const line of String(content || "").split(/\r?\n/)) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
47
|
+
if (trimmed.startsWith("[")) return "";
|
|
48
|
+
const match = trimmed.match(keyPattern);
|
|
49
|
+
if (match) return match[1].trim();
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function jsonishString(content, key) {
|
|
55
|
+
const match = String(content || "").match(new RegExp(`["']?${key}["']?\\s*[:=]\\s*["']([^"']+)["']`));
|
|
56
|
+
return match?.[1]?.trim() || "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function inferCodexModel(homeDir, env) {
|
|
60
|
+
const codexConfig = readText(path.join(homeDir, ".codex", "config.toml"));
|
|
61
|
+
return normalizeModelInfo({
|
|
62
|
+
provider: firstValue(env.OPENAI_PROVIDER, "openai"),
|
|
63
|
+
name: firstValue(
|
|
64
|
+
env.CODEX_MODEL,
|
|
65
|
+
env.OPENAI_MODEL,
|
|
66
|
+
rootTomlString(codexConfig, "model"),
|
|
67
|
+
jsonishString(codexConfig, "model")
|
|
68
|
+
)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inferClaudeModel(homeDir, env) {
|
|
73
|
+
const claudeSettings = readJson(path.join(homeDir, ".claude", "settings.json"));
|
|
74
|
+
const claudeJson = readJson(path.join(homeDir, ".claude.json"));
|
|
75
|
+
return normalizeModelInfo({
|
|
76
|
+
provider: firstValue(env.ANTHROPIC_PROVIDER, "anthropic"),
|
|
77
|
+
name: firstValue(
|
|
78
|
+
env.CLAUDE_CODE_MODEL,
|
|
79
|
+
env.CLAUDE_MODEL,
|
|
80
|
+
env.ANTHROPIC_MODEL,
|
|
81
|
+
claudeSettings.model,
|
|
82
|
+
claudeSettings.modelName,
|
|
83
|
+
claudeSettings.env?.ANTHROPIC_MODEL,
|
|
84
|
+
claudeJson.model,
|
|
85
|
+
claudeJson.modelName
|
|
86
|
+
)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inferOpenCodeModel(homeDir, env) {
|
|
91
|
+
const candidates = [
|
|
92
|
+
path.join(homeDir, ".config", "opencode", "opencode.json"),
|
|
93
|
+
path.join(homeDir, ".config", "opencode", "opencode.jsonc"),
|
|
94
|
+
path.join(homeDir, ".opencode", "config.json"),
|
|
95
|
+
path.join(homeDir, ".opencode", "config.jsonc")
|
|
96
|
+
];
|
|
97
|
+
const configText = candidates.map(readText).find(Boolean) || "";
|
|
98
|
+
return normalizeModelInfo({
|
|
99
|
+
name: firstValue(env.OPENCODE_MODEL, env.OPENAI_MODEL, jsonishString(configText, "model")),
|
|
100
|
+
provider: firstValue(env.OPENCODE_PROVIDER, jsonishString(configText, "provider"))
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inferToolModel({ tool, homeDir, env }) {
|
|
105
|
+
const normalizedTool = normalizeToolId(tool);
|
|
106
|
+
if (normalizedTool === "claude-code") return inferClaudeModel(homeDir, env);
|
|
107
|
+
if (normalizedTool === "opencode") return inferOpenCodeModel(homeDir, env);
|
|
108
|
+
return inferCodexModel(homeDir, env);
|
|
109
|
+
}
|
|
110
|
+
|
|
25
111
|
function defaultClientId() {
|
|
26
112
|
const seed = `${os.hostname()}:${os.userInfo().username}`;
|
|
27
113
|
return `device_${crypto.createHash("sha256").update(seed).digest("hex").slice(0, 12)}`;
|
|
@@ -38,9 +124,19 @@ export function defaultQueueFile(configFile = defaultConfigFile()) {
|
|
|
38
124
|
export function loadClientConfig(options = {}) {
|
|
39
125
|
const configFile = options.configFile || defaultConfigFile();
|
|
40
126
|
const fileConfig = readJson(configFile);
|
|
41
|
-
const env = process.env;
|
|
127
|
+
const env = options.env || process.env;
|
|
128
|
+
const homeDir = options.homeDir || envValue(env, "WORKLENS_HOME_DIR") || fileConfig.homeDir || os.homedir();
|
|
129
|
+
const tool = normalizeToolId(options.tool || envValue(env, "WORKLENS_TOOL") || fileConfig.tool || "codex");
|
|
42
130
|
const serverUrl = options.serverUrl || envValue(env, "WORKLENS_SERVER_URL") || fileConfig.serverUrl || "http://127.0.0.1:8797";
|
|
43
131
|
const collection = normalizeCollectionSettings(options.collection || fileConfig.collection || {});
|
|
132
|
+
const configuredModel = normalizeModelInfo({
|
|
133
|
+
...fileConfig.model,
|
|
134
|
+
provider: options.modelProvider || envValue(env, "WORKLENS_MODEL_PROVIDER") || fileConfig.model?.provider,
|
|
135
|
+
name: options.modelName || envValue(env, "WORKLENS_MODEL_NAME") || fileConfig.model?.name,
|
|
136
|
+
version: options.modelVersion || envValue(env, "WORKLENS_MODEL_VERSION") || fileConfig.model?.version,
|
|
137
|
+
family: options.modelFamily || envValue(env, "WORKLENS_MODEL_FAMILY") || fileConfig.model?.family
|
|
138
|
+
});
|
|
139
|
+
const inferredModel = inferToolModel({ tool, homeDir, env });
|
|
44
140
|
const employee = {
|
|
45
141
|
id: options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || os.userInfo().username,
|
|
46
142
|
name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || os.userInfo().username,
|
|
@@ -50,19 +146,14 @@ export function loadClientConfig(options = {}) {
|
|
|
50
146
|
};
|
|
51
147
|
return {
|
|
52
148
|
configFile,
|
|
149
|
+
homeDir,
|
|
53
150
|
queueFile: options.queueFile || defaultQueueFile(configFile),
|
|
54
151
|
serverUrl,
|
|
55
152
|
collectorToken: options.collectorToken || envValue(env, "WORKLENS_COLLECTOR_TOKEN") || fileConfig.collectorToken || "",
|
|
56
153
|
clientId: options.clientId || envValue(env, "WORKLENS_CLIENT_ID") || fileConfig.clientId || defaultClientId(),
|
|
57
154
|
clientVersion: options.clientVersion || fileConfig.clientVersion || "0.1.0",
|
|
58
|
-
tool
|
|
59
|
-
model:
|
|
60
|
-
...fileConfig.model,
|
|
61
|
-
provider: options.modelProvider || envValue(env, "WORKLENS_MODEL_PROVIDER") || fileConfig.model?.provider,
|
|
62
|
-
name: options.modelName || envValue(env, "WORKLENS_MODEL_NAME") || fileConfig.model?.name,
|
|
63
|
-
version: options.modelVersion || envValue(env, "WORKLENS_MODEL_VERSION") || fileConfig.model?.version,
|
|
64
|
-
family: options.modelFamily || envValue(env, "WORKLENS_MODEL_FAMILY") || fileConfig.model?.family
|
|
65
|
-
}),
|
|
155
|
+
tool,
|
|
156
|
+
model: knownModel(configuredModel) ? configuredModel : inferredModel,
|
|
66
157
|
workspaceRoot: options.workspaceRoot || envValue(env, "WORKLENS_WORKSPACE_ROOT") || process.cwd(),
|
|
67
158
|
repoName: options.repoName || envValue(env, "WORKLENS_REPO_NAME") || path.basename(options.workspaceRoot || envValue(env, "WORKLENS_WORKSPACE_ROOT") || process.cwd()),
|
|
68
159
|
branch: options.branch || envValue(env, "WORKLENS_BRANCH") || fileConfig.branch || "",
|
package/src/hook-adapter.mjs
CHANGED
|
@@ -70,11 +70,17 @@ function eventTypeFromHook(name, payload) {
|
|
|
70
70
|
if (["pretooluse", "toolcall", "tooluse", "toolusebefore", "toolexecutebefore"].includes(compact)) {
|
|
71
71
|
const toolName = toolNameFromPayload(payload);
|
|
72
72
|
const command = commandFromPayload(payload);
|
|
73
|
+
if (skillNameFromPayload(payload)) return "skill_use";
|
|
74
|
+
if (mcpServerFromPayload(payload)) return "mcp_tool_call";
|
|
75
|
+
if (pluginNameFromPayload(payload)) return "plugin_use";
|
|
73
76
|
return /bash|shell|terminal|command/i.test(toolName) ? commandEventType(command) : "tool_call";
|
|
74
77
|
}
|
|
75
78
|
if (["posttooluse", "posttoolbatch", "tooluseafter", "toolexecuteafter"].includes(compact)) {
|
|
76
79
|
const toolName = toolNameFromPayload(payload);
|
|
77
80
|
const command = commandFromPayload(payload);
|
|
81
|
+
if (skillNameFromPayload(payload)) return "skill_use";
|
|
82
|
+
if (mcpServerFromPayload(payload)) return "mcp_tool_call";
|
|
83
|
+
if (pluginNameFromPayload(payload)) return "plugin_use";
|
|
78
84
|
return /bash|shell|terminal|command/i.test(toolName) ? commandEventType(command) : "tool_result";
|
|
79
85
|
}
|
|
80
86
|
if (["commandexecuted", "tuicommandexecute"].includes(compact)) return commandEventType(commandFromPayload(payload));
|
|
@@ -96,13 +102,32 @@ function commandEventType(command) {
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
function toolNameFromPayload(payload) {
|
|
99
|
-
return
|
|
100
|
-
payload.
|
|
101
|
-
payload.
|
|
102
|
-
payload.
|
|
103
|
-
payload.
|
|
104
|
-
payload.
|
|
105
|
-
|
|
105
|
+
return firstText(
|
|
106
|
+
payload.toolName,
|
|
107
|
+
payload.tool_name,
|
|
108
|
+
payload.tool,
|
|
109
|
+
payload.name,
|
|
110
|
+
payload.input?.toolName,
|
|
111
|
+
payload.input?.tool_name,
|
|
112
|
+
payload.input?.tool,
|
|
113
|
+
payload.input?.name,
|
|
114
|
+
payload.output?.toolName,
|
|
115
|
+
payload.output?.tool_name,
|
|
116
|
+
payload.output?.tool,
|
|
117
|
+
payload.output?.name,
|
|
118
|
+
payload.request?.toolName,
|
|
119
|
+
payload.request?.tool_name,
|
|
120
|
+
payload.request?.tool,
|
|
121
|
+
payload.tool_input?.toolName,
|
|
122
|
+
payload.tool_input?.tool_name,
|
|
123
|
+
payload.tool_input?.tool,
|
|
124
|
+
payload.toolInput?.toolName,
|
|
125
|
+
payload.toolInput?.tool_name,
|
|
126
|
+
payload.toolInput?.tool,
|
|
127
|
+
payload.tool_args?.toolName,
|
|
128
|
+
payload.tool_args?.tool_name,
|
|
129
|
+
payload.tool_args?.tool
|
|
130
|
+
);
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
function firstText(...values) {
|
|
@@ -127,6 +152,111 @@ function modeFromPayload(payload) {
|
|
|
127
152
|
);
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
function skillNameFromPayload(payload) {
|
|
156
|
+
return firstText(
|
|
157
|
+
payload.skillName,
|
|
158
|
+
payload.skill_name,
|
|
159
|
+
payload.skill,
|
|
160
|
+
payload.metadata?.skillName,
|
|
161
|
+
payload.metadata?.skill_name,
|
|
162
|
+
payload.metadata?.skill,
|
|
163
|
+
payload.context?.skillName,
|
|
164
|
+
payload.context?.skill_name,
|
|
165
|
+
payload.context?.skill,
|
|
166
|
+
payload.input?.skillName,
|
|
167
|
+
payload.input?.skill_name,
|
|
168
|
+
payload.input?.skill,
|
|
169
|
+
payload.output?.skillName,
|
|
170
|
+
payload.output?.skill_name,
|
|
171
|
+
payload.output?.skill
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pluginNameFromPayload(payload) {
|
|
176
|
+
const explicit = firstText(
|
|
177
|
+
payload.pluginName,
|
|
178
|
+
payload.plugin_name,
|
|
179
|
+
payload.plugin,
|
|
180
|
+
payload.metadata?.pluginName,
|
|
181
|
+
payload.metadata?.plugin_name,
|
|
182
|
+
payload.metadata?.plugin,
|
|
183
|
+
payload.context?.pluginName,
|
|
184
|
+
payload.context?.plugin_name,
|
|
185
|
+
payload.context?.plugin,
|
|
186
|
+
payload.input?.pluginName,
|
|
187
|
+
payload.input?.plugin_name,
|
|
188
|
+
payload.input?.plugin,
|
|
189
|
+
payload.output?.pluginName,
|
|
190
|
+
payload.output?.plugin_name,
|
|
191
|
+
payload.output?.plugin
|
|
192
|
+
);
|
|
193
|
+
if (explicit) return explicit;
|
|
194
|
+
return pluginNameFromMcpNamespace(mcpNamespaceFromToolName(toolNameFromPayload(payload)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function mcpServerFromPayload(payload) {
|
|
198
|
+
const explicit = firstText(
|
|
199
|
+
payload.mcpServer,
|
|
200
|
+
payload.mcp_server,
|
|
201
|
+
payload.server,
|
|
202
|
+
payload.metadata?.mcpServer,
|
|
203
|
+
payload.metadata?.mcp_server,
|
|
204
|
+
payload.metadata?.server,
|
|
205
|
+
payload.context?.mcpServer,
|
|
206
|
+
payload.context?.mcp_server,
|
|
207
|
+
payload.context?.server,
|
|
208
|
+
payload.input?.mcpServer,
|
|
209
|
+
payload.input?.mcp_server,
|
|
210
|
+
payload.input?.server,
|
|
211
|
+
payload.output?.mcpServer,
|
|
212
|
+
payload.output?.mcp_server,
|
|
213
|
+
payload.output?.server
|
|
214
|
+
);
|
|
215
|
+
if (explicit) return normalizeMcpServerName(explicit);
|
|
216
|
+
return normalizeMcpServerName(mcpNamespaceFromToolName(toolNameFromPayload(payload)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mcpNamespaceFromToolName(value) {
|
|
220
|
+
const toolName = String(value || "");
|
|
221
|
+
const match = toolName.match(/^mcp__(.+?)(?:[.:].*)?$/);
|
|
222
|
+
return match ? match[1] : "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeMcpServerName(value) {
|
|
226
|
+
const raw = String(value || "").trim();
|
|
227
|
+
if (!raw) return "";
|
|
228
|
+
if (raw.startsWith("codex_apps__")) return raw.slice("codex_apps__".length).replaceAll("_", "-");
|
|
229
|
+
return raw.replaceAll("_", "-");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pluginNameFromMcpNamespace(value) {
|
|
233
|
+
const raw = String(value || "").trim();
|
|
234
|
+
if (!raw) return "";
|
|
235
|
+
const normalized = normalizeMcpServerName(raw);
|
|
236
|
+
const key = normalized.toLowerCase();
|
|
237
|
+
const rawKey = raw.toLowerCase();
|
|
238
|
+
const mapping = {
|
|
239
|
+
playwright: "Browser",
|
|
240
|
+
browser: "Browser",
|
|
241
|
+
chrome: "Chrome",
|
|
242
|
+
"computer-use": "Computer Use",
|
|
243
|
+
figma: "Figma",
|
|
244
|
+
"outlook-email": "Outlook Email",
|
|
245
|
+
presentations: "Presentations",
|
|
246
|
+
spreadsheets: "Spreadsheets"
|
|
247
|
+
};
|
|
248
|
+
if (rawKey.startsWith("codex_apps__")) return mapping[key] || upperPluginName(normalized);
|
|
249
|
+
return mapping[key] || "";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function upperPluginName(value) {
|
|
253
|
+
return String(value || "")
|
|
254
|
+
.split(/[-_\s]+/)
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map((part) => `${part[0]?.toUpperCase() || ""}${part.slice(1)}`)
|
|
257
|
+
.join(" ");
|
|
258
|
+
}
|
|
259
|
+
|
|
130
260
|
function previousModeFromPayload(payload) {
|
|
131
261
|
return firstText(
|
|
132
262
|
payload.previousMode,
|
|
@@ -268,6 +398,10 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
268
398
|
const eventType = eventTypeFromHook(name, payload);
|
|
269
399
|
const summary = pickSummary(payload, name, eventType);
|
|
270
400
|
const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
|
|
401
|
+
const skillName = skillNameFromPayload(payload);
|
|
402
|
+
const pluginName = pluginNameFromPayload(payload);
|
|
403
|
+
const mcpServer = mcpServerFromPayload(payload);
|
|
404
|
+
const toolName = toolNameFromPayload(payload);
|
|
271
405
|
return buildEvent({
|
|
272
406
|
...payload,
|
|
273
407
|
eventType,
|
|
@@ -277,9 +411,9 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
277
411
|
hookName: name,
|
|
278
412
|
localSessionId: payload.sessionId || payload.session_id || payload.localSessionId,
|
|
279
413
|
turnIndex: payload.turnIndex || payload.turn_index,
|
|
280
|
-
skillName
|
|
281
|
-
pluginName
|
|
282
|
-
mcpServer
|
|
414
|
+
skillName,
|
|
415
|
+
pluginName,
|
|
416
|
+
mcpServer,
|
|
283
417
|
files: fileRefsFromPayload(payload),
|
|
284
418
|
commands: commandFromPayload(payload),
|
|
285
419
|
durationSeconds: payload.durationSeconds || payload.duration_seconds || payload.duration_ms && Number(payload.duration_ms) / 1000,
|
|
@@ -289,7 +423,11 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
289
423
|
sourceTool: config.tool,
|
|
290
424
|
rawHookEvent: name,
|
|
291
425
|
hookEventName: payload.hook_event_name || payload.hookEventName || payload.event || payload.type || name,
|
|
292
|
-
toolName
|
|
426
|
+
toolName,
|
|
427
|
+
skillName,
|
|
428
|
+
pluginName,
|
|
429
|
+
mcpServer,
|
|
430
|
+
mcpNamespace: mcpNamespaceFromToolName(toolName),
|
|
293
431
|
codexMode: nextModeFromPayload(payload),
|
|
294
432
|
interactionType: eventType,
|
|
295
433
|
cwd: payload.cwd || payload.directory || payload.project?.directory,
|
|
@@ -305,7 +443,7 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
305
443
|
previousMode: previousModeFromPayload(payload),
|
|
306
444
|
nextMode: nextModeFromPayload(payload),
|
|
307
445
|
phase: payload.phase || payload.input?.phase || payload.output?.phase,
|
|
308
|
-
toolName
|
|
446
|
+
toolName,
|
|
309
447
|
toolStatus: statusFromPayload(payload),
|
|
310
448
|
exitCode: payload.exitCode ?? payload.exit_code ?? payload.output?.exitCode ?? payload.output?.exit_code,
|
|
311
449
|
permissionDecision: permissionDecisionFromPayload(payload),
|
package/src/install.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { writeClientConfig } from "./config.mjs";
|
|
7
8
|
import { getToolProfile, listToolProfiles, normalizeToolId } from "./protocol/tool-profiles.mjs";
|
|
8
9
|
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const agentSrcDir = __dirname;
|
|
12
|
+
const clientPackageVersion = readPackageVersion();
|
|
11
13
|
|
|
12
14
|
function parseArgs(argv) {
|
|
13
15
|
const result = {};
|
|
@@ -25,10 +27,20 @@ function parseArgs(argv) {
|
|
|
25
27
|
return result;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
function readPackageVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8"));
|
|
33
|
+
return packageJson.version || "0.1.0";
|
|
34
|
+
} catch {
|
|
35
|
+
return "0.1.0";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function buildInstallManifest(targetDir, options = {}) {
|
|
29
40
|
const node = process.execPath;
|
|
30
41
|
const selectedTool = normalizeToolId(options.tool || "codex");
|
|
31
42
|
const profile = getToolProfile(selectedTool);
|
|
43
|
+
const serverUrl = options.serverUrl || "http://127.0.0.1:8797";
|
|
32
44
|
const displayTargetDir = displayPath(targetDir);
|
|
33
45
|
const configFile = path.join(displayTargetDir, "client.json");
|
|
34
46
|
const mcpCommand = node;
|
|
@@ -40,6 +52,7 @@ export function buildInstallManifest(targetDir, options = {}) {
|
|
|
40
52
|
displayTargetDir,
|
|
41
53
|
selectedTool,
|
|
42
54
|
configFile,
|
|
55
|
+
serverUrl,
|
|
43
56
|
mcpCommand,
|
|
44
57
|
mcpArgs,
|
|
45
58
|
hookCommand,
|
|
@@ -55,7 +68,7 @@ export function buildInstallManifest(targetDir, options = {}) {
|
|
|
55
68
|
name: profile.mcpServerName,
|
|
56
69
|
command: mcpCommand,
|
|
57
70
|
args: mcpArgs,
|
|
58
|
-
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: selectedTool }
|
|
71
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: selectedTool, WORKLENS_SERVER_URL: serverUrl }
|
|
59
72
|
},
|
|
60
73
|
hook: {
|
|
61
74
|
name: profile.hookName,
|
|
@@ -94,12 +107,13 @@ export function buildInstallManifest(targetDir, options = {}) {
|
|
|
94
107
|
fullReplyUploaded: false,
|
|
95
108
|
localRedaction: true
|
|
96
109
|
},
|
|
97
|
-
serverUrl
|
|
110
|
+
serverUrl
|
|
98
111
|
};
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
export function installClient(options = {}) {
|
|
102
115
|
const targetDir = options.targetDir || path.join(os.homedir(), ".ai-worklens");
|
|
116
|
+
const homeDir = options.homeDir || os.homedir();
|
|
103
117
|
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
104
118
|
const configFile = path.join(targetDir, "client.json");
|
|
105
119
|
const manifestFile = path.join(targetDir, "install-manifest.json");
|
|
@@ -113,11 +127,23 @@ export function installClient(options = {}) {
|
|
|
113
127
|
const installOrUpdateScriptFile = path.join(targetDir, "worklens-install-or-update.sh");
|
|
114
128
|
const readmeFile = path.join(targetDir, "README.md");
|
|
115
129
|
const selectedTool = normalizeToolId(options.tool || "codex");
|
|
130
|
+
const installedClientVersion = options.clientVersion || clientPackageVersion;
|
|
131
|
+
const updatePolicyInput = options.updatePolicy || {};
|
|
132
|
+
const installedArtifactRevision = String(
|
|
133
|
+
options.artifactRevision
|
|
134
|
+
|| (String(updatePolicyInput.clientVersion || "") === String(installedClientVersion) ? updatePolicyInput.artifactRevision : "")
|
|
135
|
+
|| installedClientVersion
|
|
136
|
+
);
|
|
137
|
+
const updatePolicy = {
|
|
138
|
+
...updatePolicyInput,
|
|
139
|
+
appliedRevision: options.appliedRevision || installedArtifactRevision,
|
|
140
|
+
lastUpdatedAt: updatePolicyInput.lastUpdatedAt || new Date().toISOString()
|
|
141
|
+
};
|
|
116
142
|
writeClientConfig(configFile, {
|
|
117
143
|
serverUrl: options.serverUrl || "http://127.0.0.1:8797",
|
|
118
144
|
collectorToken: options.collectorToken || "",
|
|
119
145
|
clientId: options.clientId || "",
|
|
120
|
-
clientVersion:
|
|
146
|
+
clientVersion: installedClientVersion,
|
|
121
147
|
tool: selectedTool,
|
|
122
148
|
model: {
|
|
123
149
|
provider: options.modelProvider || "",
|
|
@@ -134,7 +160,7 @@ export function installClient(options = {}) {
|
|
|
134
160
|
},
|
|
135
161
|
upload: options.upload || { timeoutMs: 5000, batchSize: 50 },
|
|
136
162
|
collection: options.collection || {},
|
|
137
|
-
update:
|
|
163
|
+
update: updatePolicy
|
|
138
164
|
});
|
|
139
165
|
const manifest = buildInstallManifest(targetDir, options);
|
|
140
166
|
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -166,12 +192,20 @@ export function installClient(options = {}) {
|
|
|
166
192
|
fs.chmodSync(installOrUpdateScriptFile, 0o700);
|
|
167
193
|
fs.writeFileSync(readmeFile, buildReadme(manifest), { mode: 0o600 });
|
|
168
194
|
fs.chmodSync(readmeFile, 0o600);
|
|
195
|
+
const integrations = options.writeToolConfigs === false
|
|
196
|
+
? { ok: true, skipped: true, items: [] }
|
|
197
|
+
: installToolIntegrations(manifest, {
|
|
198
|
+
homeDir,
|
|
199
|
+
integrations: options.integrations || "selected",
|
|
200
|
+
useToolCli: options.useToolCli !== false
|
|
201
|
+
});
|
|
169
202
|
return {
|
|
170
203
|
ok: true,
|
|
171
204
|
targetDir,
|
|
172
205
|
configFile,
|
|
173
206
|
manifestFile,
|
|
174
207
|
generatedFiles: manifest.generatedFiles,
|
|
208
|
+
integrations,
|
|
175
209
|
manifest
|
|
176
210
|
};
|
|
177
211
|
}
|
|
@@ -200,7 +234,7 @@ function upperFirst(value) {
|
|
|
200
234
|
return input ? `${input[0].toUpperCase()}${input.slice(1)}` : input;
|
|
201
235
|
}
|
|
202
236
|
|
|
203
|
-
function buildToolArtifacts({ targetDir, displayTargetDir, configFile, mcpCommand, mcpArgs }) {
|
|
237
|
+
function buildToolArtifacts({ targetDir, displayTargetDir, configFile, serverUrl, mcpCommand, mcpArgs }) {
|
|
204
238
|
return Object.fromEntries(listToolProfiles().map((profile) => {
|
|
205
239
|
const hookArgs = [path.join(agentSrcDir, "hook-adapter.mjs"), "--config", configFile, "--tool", profile.id];
|
|
206
240
|
const artifact = {
|
|
@@ -216,10 +250,11 @@ function buildToolArtifacts({ targetDir, displayTargetDir, configFile, mcpComman
|
|
|
216
250
|
name: profile.mcpServerName,
|
|
217
251
|
command: mcpCommand,
|
|
218
252
|
args: mcpArgs,
|
|
219
|
-
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
253
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id, WORKLENS_SERVER_URL: serverUrl }
|
|
220
254
|
},
|
|
221
255
|
config: toolConfigFor(profile, {
|
|
222
256
|
configFile,
|
|
257
|
+
serverUrl,
|
|
223
258
|
mcpCommand,
|
|
224
259
|
mcpArgs,
|
|
225
260
|
hookCommand: mcpCommand,
|
|
@@ -240,7 +275,7 @@ function buildToolArtifacts({ targetDir, displayTargetDir, configFile, mcpComman
|
|
|
240
275
|
}));
|
|
241
276
|
}
|
|
242
277
|
|
|
243
|
-
function toolConfigFor(profile, { configFile, mcpCommand, mcpArgs, hookCommand, hookArgs }) {
|
|
278
|
+
function toolConfigFor(profile, { configFile, serverUrl, mcpCommand, mcpArgs, hookCommand, hookArgs }) {
|
|
244
279
|
if (profile.id === "codex") {
|
|
245
280
|
return [
|
|
246
281
|
`[mcp_servers.${profile.mcpServerName}]`,
|
|
@@ -249,7 +284,8 @@ function toolConfigFor(profile, { configFile, mcpCommand, mcpArgs, hookCommand,
|
|
|
249
284
|
"",
|
|
250
285
|
`[mcp_servers.${profile.mcpServerName}.env]`,
|
|
251
286
|
`WORKLENS_CONFIG_FILE = ${tomlString(configFile)}`,
|
|
252
|
-
`WORKLENS_TOOL = ${tomlString(profile.id)}
|
|
287
|
+
`WORKLENS_TOOL = ${tomlString(profile.id)}`,
|
|
288
|
+
`WORKLENS_SERVER_URL = ${tomlString(serverUrl)}`
|
|
253
289
|
].join("\n");
|
|
254
290
|
}
|
|
255
291
|
if (profile.id === "claude-code") {
|
|
@@ -258,7 +294,7 @@ function toolConfigFor(profile, { configFile, mcpCommand, mcpArgs, hookCommand,
|
|
|
258
294
|
[profile.mcpServerName]: {
|
|
259
295
|
command: mcpCommand,
|
|
260
296
|
args: mcpArgs,
|
|
261
|
-
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
297
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id, WORKLENS_SERVER_URL: serverUrl }
|
|
262
298
|
}
|
|
263
299
|
}
|
|
264
300
|
}, null, 2);
|
|
@@ -271,12 +307,12 @@ function toolConfigFor(profile, { configFile, mcpCommand, mcpArgs, hookCommand,
|
|
|
271
307
|
type: "local",
|
|
272
308
|
command: [mcpCommand, ...mcpArgs],
|
|
273
309
|
enabled: true,
|
|
274
|
-
environment: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
310
|
+
environment: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id, WORKLENS_SERVER_URL: serverUrl }
|
|
275
311
|
}
|
|
276
312
|
}
|
|
277
313
|
}, null, 2);
|
|
278
314
|
}
|
|
279
|
-
return JSON.stringify({ command: mcpCommand, args: mcpArgs, env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id } }, null, 2);
|
|
315
|
+
return JSON.stringify({ command: mcpCommand, args: mcpArgs, env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id, WORKLENS_SERVER_URL: serverUrl } }, null, 2);
|
|
280
316
|
}
|
|
281
317
|
|
|
282
318
|
function extraFilesFor(profile, { targetDir, hookCommand, hookArgs }) {
|
|
@@ -332,8 +368,7 @@ function buildClaudeHooksSettings(command, baseArgs) {
|
|
|
332
368
|
hooks: [
|
|
333
369
|
{
|
|
334
370
|
type: "command",
|
|
335
|
-
command,
|
|
336
|
-
args: [...baseArgs, "--event", eventName],
|
|
371
|
+
command: commandLine(command, [...baseArgs, "--event", eventName]),
|
|
337
372
|
timeout: 10
|
|
338
373
|
}
|
|
339
374
|
]
|
|
@@ -347,6 +382,249 @@ function buildClaudeHooksSettings(command, baseArgs) {
|
|
|
347
382
|
}, null, 2);
|
|
348
383
|
}
|
|
349
384
|
|
|
385
|
+
function installToolIntegrations(manifest, { homeDir, integrations = "selected", useToolCli = true } = {}) {
|
|
386
|
+
const selected = selectedIntegrationTools(integrations, manifest);
|
|
387
|
+
const items = [];
|
|
388
|
+
const errors = [];
|
|
389
|
+
const run = (tool, fn) => {
|
|
390
|
+
if (!selected.includes(tool)) return;
|
|
391
|
+
try {
|
|
392
|
+
items.push({ tool, ok: true, ...fn() });
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const item = { tool, ok: false, error: error.message };
|
|
395
|
+
items.push(item);
|
|
396
|
+
errors.push(item);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
run("codex", () => installCodexIntegration(manifest, homeDir));
|
|
400
|
+
run("claude-code", () => installClaudeCodeIntegration(manifest, homeDir, { useToolCli }));
|
|
401
|
+
run("opencode", () => installOpenCodeIntegration(manifest, homeDir));
|
|
402
|
+
return { ok: errors.length === 0, items };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function selectedIntegrationTools(value, manifest) {
|
|
406
|
+
const raw = String(value || "selected").trim().toLowerCase();
|
|
407
|
+
if (["none", "false", "off", "0"].includes(raw)) return [];
|
|
408
|
+
if (raw === "selected") return [manifest.selectedTool];
|
|
409
|
+
if (raw === "all") return Object.keys(manifest.toolArtifacts);
|
|
410
|
+
return raw.split(",").map((item) => normalizeToolId(item.trim())).filter(Boolean);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function installCodexIntegration(manifest, homeDir) {
|
|
414
|
+
const artifact = manifest.toolArtifacts.codex;
|
|
415
|
+
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
416
|
+
const hooksPath = path.join(homeDir, ".codex", "hooks.json");
|
|
417
|
+
const config = readTextConfig(configPath);
|
|
418
|
+
const nextConfig = appendTomlSections(removeTomlSections(config, [
|
|
419
|
+
`mcp_servers.${artifact.profile.mcpServerName}`,
|
|
420
|
+
`mcp_servers.${artifact.profile.mcpServerName}.env`
|
|
421
|
+
]), artifact.config);
|
|
422
|
+
writeTextConfig(configPath, nextConfig);
|
|
423
|
+
|
|
424
|
+
const hooks = readJsonConfig(hooksPath, { hooks: {} });
|
|
425
|
+
hooks.hooks = hooks.hooks && typeof hooks.hooks === "object" ? hooks.hooks : {};
|
|
426
|
+
const command = shellQuote(artifact.hookFile);
|
|
427
|
+
for (const eventName of ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"]) {
|
|
428
|
+
const entry = {
|
|
429
|
+
hooks: [{ type: "command", command, ...(eventName === "Stop" ? { timeout: 30 } : {}) }]
|
|
430
|
+
};
|
|
431
|
+
if (eventName === "SessionStart") entry.matcher = "startup|resume";
|
|
432
|
+
hooks.hooks[eventName] = ensureHookEntry(removeWorkLensHookEntries(hooks.hooks[eventName]), entry);
|
|
433
|
+
}
|
|
434
|
+
writeJsonConfig(hooksPath, hooks);
|
|
435
|
+
enableExistingCodexHookState(configPath, hooksPath, ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"]);
|
|
436
|
+
return { configPath, hooksPath };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function installClaudeCodeIntegration(manifest, homeDir, { useToolCli = true } = {}) {
|
|
440
|
+
const artifact = manifest.toolArtifacts["claude-code"];
|
|
441
|
+
const globalConfigPath = path.join(homeDir, ".claude.json");
|
|
442
|
+
const settingsPath = path.join(homeDir, ".claude", "settings.json");
|
|
443
|
+
const serverConfig = {
|
|
444
|
+
type: "stdio",
|
|
445
|
+
command: artifact.mcp.command,
|
|
446
|
+
args: artifact.mcp.args,
|
|
447
|
+
env: artifact.mcp.env
|
|
448
|
+
};
|
|
449
|
+
const mcpConfig = readJsonConfig(globalConfigPath, {});
|
|
450
|
+
mcpConfig.mcpServers = mcpConfig.mcpServers && typeof mcpConfig.mcpServers === "object" ? mcpConfig.mcpServers : {};
|
|
451
|
+
mcpConfig.mcpServers[artifact.profile.mcpServerName] = serverConfig;
|
|
452
|
+
writeJsonConfig(globalConfigPath, mcpConfig);
|
|
453
|
+
|
|
454
|
+
const settings = readJsonConfig(settingsPath, {});
|
|
455
|
+
const hooksSettings = JSON.parse(artifact.extraFiles.hooksSettings.content);
|
|
456
|
+
settings.hooks = mergeHookSettings(settings.hooks, hooksSettings.hooks);
|
|
457
|
+
writeJsonConfig(settingsPath, settings);
|
|
458
|
+
const cli = useToolCli ? installClaudeMcpWithCli(artifact.profile.mcpServerName, serverConfig, homeDir) : { ok: true, skipped: true };
|
|
459
|
+
return { globalConfigPath, settingsPath, cli };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function installOpenCodeIntegration(manifest, homeDir) {
|
|
463
|
+
const artifact = manifest.toolArtifacts.opencode;
|
|
464
|
+
const configPath = path.join(homeDir, ".config", "opencode", "opencode.json");
|
|
465
|
+
const pluginPath = path.join(homeDir, ".config", "opencode", "plugins", "ai-worklens.js");
|
|
466
|
+
const config = readJsonConfig(configPath, { $schema: "https://opencode.ai/config.json" });
|
|
467
|
+
const generated = JSON.parse(artifact.config);
|
|
468
|
+
config.$schema = config.$schema || generated.$schema;
|
|
469
|
+
config.mcp = config.mcp && typeof config.mcp === "object" ? config.mcp : {};
|
|
470
|
+
config.mcp[artifact.profile.mcpServerName] = generated.mcp[artifact.profile.mcpServerName];
|
|
471
|
+
writeJsonConfig(configPath, config);
|
|
472
|
+
writeTextConfig(pluginPath, `${artifact.extraFiles.plugin.content}\n`, 0o600);
|
|
473
|
+
return { configPath, pluginPath };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function mergeHookSettings(current, incoming) {
|
|
477
|
+
const hooks = current && typeof current === "object" ? { ...current } : {};
|
|
478
|
+
for (const [eventName, entries] of Object.entries(incoming || {})) {
|
|
479
|
+
const base = removeWorkLensHookEntries(hooks[eventName]);
|
|
480
|
+
hooks[eventName] = (entries || []).reduce((list, entry) => ensureHookEntry(list, entry), base);
|
|
481
|
+
}
|
|
482
|
+
return hooks;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function removeWorkLensHookEntries(current) {
|
|
486
|
+
const list = Array.isArray(current) ? current : [];
|
|
487
|
+
return list.filter((entry) => !hookEntryContainsWorkLens(entry));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function hookEntryContainsWorkLens(entry = {}) {
|
|
491
|
+
return (entry.hooks || []).some((hook) => workLensHookCommandPattern().test(String(hook.command || "")));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function workLensHookCommandPattern() {
|
|
495
|
+
return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.sh|claude-code-hook\.sh|opencode-hook\.sh)/i;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function ensureHookEntry(current, entry) {
|
|
499
|
+
const list = Array.isArray(current) ? current : [];
|
|
500
|
+
const key = JSON.stringify(entry);
|
|
501
|
+
if (list.some((item) => JSON.stringify(item) === key)) return list;
|
|
502
|
+
return [...list, entry];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function commandLine(command, args) {
|
|
506
|
+
return [command, ...args].map(shellQuote).join(" ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function readTextConfig(filePath) {
|
|
510
|
+
try {
|
|
511
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
512
|
+
} catch {
|
|
513
|
+
return "";
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function readJsonConfig(filePath, fallback) {
|
|
518
|
+
if (!fs.existsSync(filePath)) return structuredClone(fallback);
|
|
519
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
520
|
+
if (!raw) return structuredClone(fallback);
|
|
521
|
+
return JSON.parse(raw);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function writeTextConfig(filePath, content, mode = 0o600) {
|
|
525
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
526
|
+
const tempFile = `${filePath}.tmp-${process.pid}`;
|
|
527
|
+
fs.writeFileSync(tempFile, content, { mode });
|
|
528
|
+
fs.renameSync(tempFile, filePath);
|
|
529
|
+
fs.chmodSync(filePath, mode);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function writeJsonConfig(filePath, value) {
|
|
533
|
+
writeTextConfig(filePath, `${JSON.stringify(value, null, 2)}\n`, 0o600);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function enableExistingCodexHookState(configPath, hooksPath, eventNames) {
|
|
537
|
+
if (!fs.existsSync(configPath)) return;
|
|
538
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
539
|
+
const next = enableHookStateSections(content, hooksPath, eventNames);
|
|
540
|
+
if (next !== content) writeTextConfig(configPath, next);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function enableHookStateSections(content, hooksPath, eventNames) {
|
|
544
|
+
const hookEvents = new Set(eventNames.map(codexHookStateEventName));
|
|
545
|
+
const lines = String(content || "").split(/\n/);
|
|
546
|
+
let inWorkLensHookState = false;
|
|
547
|
+
let changed = false;
|
|
548
|
+
const output = lines.map((line) => {
|
|
549
|
+
const section = line.match(/^\[hooks\.state\."((?:\\.|[^"\\])*)"\]\s*$/);
|
|
550
|
+
if (section) {
|
|
551
|
+
const stateKey = parseTomlQuotedString(section[1]);
|
|
552
|
+
inWorkLensHookState = isWorkLensHookStateKey(stateKey, hooksPath, hookEvents);
|
|
553
|
+
return line;
|
|
554
|
+
}
|
|
555
|
+
if (/^\[[^\]]+\]\s*$/.test(line)) {
|
|
556
|
+
inWorkLensHookState = false;
|
|
557
|
+
return line;
|
|
558
|
+
}
|
|
559
|
+
if (inWorkLensHookState && /^enabled\s*=\s*false\s*$/.test(line)) {
|
|
560
|
+
changed = true;
|
|
561
|
+
return "enabled = true";
|
|
562
|
+
}
|
|
563
|
+
return line;
|
|
564
|
+
});
|
|
565
|
+
return changed ? output.join("\n") : content;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function parseTomlQuotedString(value) {
|
|
569
|
+
try {
|
|
570
|
+
return JSON.parse(`"${value}"`);
|
|
571
|
+
} catch {
|
|
572
|
+
return String(value || "");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function isWorkLensHookStateKey(stateKey, hooksPath, hookEvents) {
|
|
577
|
+
const prefix = `${hooksPath}:`;
|
|
578
|
+
if (!String(stateKey || "").startsWith(prefix)) return false;
|
|
579
|
+
const rest = String(stateKey).slice(prefix.length);
|
|
580
|
+
const eventName = rest.split(":")[0];
|
|
581
|
+
return hookEvents.has(eventName);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function codexHookStateEventName(eventName) {
|
|
585
|
+
return String(eventName || "")
|
|
586
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
587
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
588
|
+
.replace(/^_+|_+$/g, "")
|
|
589
|
+
.toLowerCase();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function installClaudeMcpWithCli(name, serverConfig, homeDir) {
|
|
593
|
+
const result = spawnSync("claude", [
|
|
594
|
+
"mcp",
|
|
595
|
+
"add-json",
|
|
596
|
+
"-s",
|
|
597
|
+
"user",
|
|
598
|
+
name,
|
|
599
|
+
JSON.stringify(serverConfig)
|
|
600
|
+
], {
|
|
601
|
+
encoding: "utf8",
|
|
602
|
+
timeout: 10000,
|
|
603
|
+
env: { ...process.env, HOME: homeDir }
|
|
604
|
+
});
|
|
605
|
+
if (result.error) return { ok: false, error: result.error.message };
|
|
606
|
+
if (result.status !== 0) {
|
|
607
|
+
const stderr = String(result.stderr || "").trim();
|
|
608
|
+
const stdout = String(result.stdout || "").trim();
|
|
609
|
+
return { ok: false, status: result.status, error: stderr || stdout || "claude mcp add-json failed" };
|
|
610
|
+
}
|
|
611
|
+
return { ok: true };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function removeTomlSections(content, sectionNames) {
|
|
615
|
+
let output = String(content || "");
|
|
616
|
+
for (const sectionName of sectionNames) {
|
|
617
|
+
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
618
|
+
output = output.replace(new RegExp(`(^|\\n)\\[${escaped}\\]\\n[\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`, "g"), "\n");
|
|
619
|
+
}
|
|
620
|
+
return output.trimEnd();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function appendTomlSections(content, sections) {
|
|
624
|
+
const prefix = String(content || "").trimEnd();
|
|
625
|
+
return `${prefix ? `${prefix}\n\n` : ""}${sections.trim()}\n`;
|
|
626
|
+
}
|
|
627
|
+
|
|
350
628
|
function buildOpenCodePlugin(command, baseArgs) {
|
|
351
629
|
const events = [
|
|
352
630
|
"command.executed",
|
|
@@ -580,16 +858,53 @@ function buildReadme(manifest) {
|
|
|
580
858
|
].join("\n") + "\n";
|
|
581
859
|
}
|
|
582
860
|
|
|
583
|
-
function
|
|
861
|
+
function runPostInstall(result, args = {}) {
|
|
862
|
+
if (args["post-install"] === "false") return { ok: true, skipped: true };
|
|
863
|
+
const checkin = spawnSync(result.generatedFiles.checkinScript, [], {
|
|
864
|
+
encoding: "utf8",
|
|
865
|
+
timeout: Number(args["post-install-timeout-ms"] || 15000)
|
|
866
|
+
});
|
|
867
|
+
const register = spawnSync(result.generatedFiles.registerAutoUpdateScript, [], {
|
|
868
|
+
encoding: "utf8",
|
|
869
|
+
timeout: Number(args["post-install-timeout-ms"] || 15000)
|
|
870
|
+
});
|
|
871
|
+
return {
|
|
872
|
+
ok: checkin.status === 0,
|
|
873
|
+
registered: register.status === 0,
|
|
874
|
+
checkinStatus: checkin.status,
|
|
875
|
+
registerStatus: register.status,
|
|
876
|
+
error: checkin.error?.message || ""
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function formatInstallSuccess(result, args, postInstall = {}) {
|
|
584
881
|
const tool = result.manifest?.selectedTool || args.tool || "codex";
|
|
585
882
|
const employeeAlias = args["employee-pinyin"] || args["employee-id"] || "未填写";
|
|
883
|
+
const integrationLine = result.integrations?.skipped
|
|
884
|
+
? "工具配置: 已跳过"
|
|
885
|
+
: result.integrations?.ok
|
|
886
|
+
? `工具配置: 已写入 ${integrationSummary(result.integrations.items)}`
|
|
887
|
+
: `工具配置: 部分失败 ${integrationSummary(result.integrations.items)}`;
|
|
888
|
+
const checkinLine = postInstall.skipped
|
|
889
|
+
? "中心端登记: 已跳过"
|
|
890
|
+
: postInstall.ok
|
|
891
|
+
? "中心端登记: 已完成"
|
|
892
|
+
: "中心端登记: 未完成,后台任务会继续重试";
|
|
586
893
|
return [
|
|
587
894
|
"AI WorkLens 安装成功",
|
|
588
895
|
`工具: ${tool} / 注册名: ${employeeAlias}`,
|
|
589
|
-
`配置目录: ${result.targetDir}
|
|
896
|
+
`配置目录: ${result.targetDir}`,
|
|
897
|
+
integrationLine,
|
|
898
|
+
checkinLine
|
|
590
899
|
].join("\n") + "\n";
|
|
591
900
|
}
|
|
592
901
|
|
|
902
|
+
function integrationSummary(items = []) {
|
|
903
|
+
if (!items.length) return "";
|
|
904
|
+
const labels = { codex: "Codex", "claude-code": "Claude Code", opencode: "OpenCode" };
|
|
905
|
+
return items.map((item) => `${labels[item.tool] || item.tool}${item.ok ? "" : "失败"}`).join(" / ");
|
|
906
|
+
}
|
|
907
|
+
|
|
593
908
|
function formatInstallFailure(error) {
|
|
594
909
|
return [
|
|
595
910
|
"AI WorkLens 安装失败",
|
|
@@ -620,13 +935,18 @@ if (isMainModule()) {
|
|
|
620
935
|
role: args.role,
|
|
621
936
|
clientId: args["client-id"],
|
|
622
937
|
tool: args.tool,
|
|
938
|
+
homeDir: args["home-dir"],
|
|
939
|
+
integrations: args.integrations,
|
|
940
|
+
writeToolConfigs: args["write-tool-configs"] !== "false" && args["write-tool-configs"] !== "0",
|
|
941
|
+
useToolCli: args["use-tool-cli"] !== "false" && args["use-tool-cli"] !== "0",
|
|
623
942
|
modelProvider: args["model-provider"],
|
|
624
943
|
modelName: args["model-name"],
|
|
625
944
|
modelVersion: args["model-version"],
|
|
626
945
|
modelFamily: args["model-family"]
|
|
627
946
|
});
|
|
628
|
-
|
|
629
|
-
|
|
947
|
+
const postInstall = runPostInstall(result, args);
|
|
948
|
+
if (args.json) process.stdout.write(`${JSON.stringify({ ...result, postInstall }, null, 2)}\n`);
|
|
949
|
+
else process.stdout.write(formatInstallSuccess(result, args, postInstall));
|
|
630
950
|
} catch (error) {
|
|
631
951
|
if (args.json) process.stderr.write(`${JSON.stringify({ ok: false, error: error.message }, null, 2)}\n`);
|
|
632
952
|
else process.stderr.write(formatInstallFailure(error));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export const CLIENT_AGENT_VERSION = "0.1.
|
|
1
|
+
export const CLIENT_AGENT_VERSION = "0.1.4";
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_CLIENT_UPDATE_POLICY = {
|
|
4
4
|
enabled: true,
|
|
5
5
|
autoUpdate: true,
|
|
6
6
|
clientVersion: CLIENT_AGENT_VERSION,
|
|
7
|
-
artifactRevision:
|
|
7
|
+
artifactRevision: CLIENT_AGENT_VERSION,
|
|
8
8
|
checkIntervalMinutes: 360,
|
|
9
9
|
registerBackgroundTask: true,
|
|
10
10
|
refreshArtifacts: true
|
|
@@ -31,6 +31,35 @@ export function eventTypeLabel(type) {
|
|
|
31
31
|
return eventTypeDefinition(type)?.label || "工具调用";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const FALLBACK_SECONDS_BY_EVENT_TYPE = {
|
|
35
|
+
session_start: 10,
|
|
36
|
+
session_end: 10,
|
|
37
|
+
user_prompt: 30,
|
|
38
|
+
assistant_response: 45,
|
|
39
|
+
planning: 45,
|
|
40
|
+
research: 45,
|
|
41
|
+
implementation: 45,
|
|
42
|
+
document: 45,
|
|
43
|
+
tool_call: 20,
|
|
44
|
+
tool_result: 20,
|
|
45
|
+
mcp_tool_call: 20,
|
|
46
|
+
skill_use: 30,
|
|
47
|
+
plugin_use: 30,
|
|
48
|
+
command: 20,
|
|
49
|
+
mode_change: 10,
|
|
50
|
+
permission: 10,
|
|
51
|
+
verification: 30,
|
|
52
|
+
retry: 30,
|
|
53
|
+
rework: 30,
|
|
54
|
+
error: 10
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function eventDurationSeconds(event = {}) {
|
|
58
|
+
const explicit = Number(event.metrics?.durationSeconds ?? event.durationSeconds ?? 0);
|
|
59
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
60
|
+
return FALLBACK_SECONDS_BY_EVENT_TYPE[event.eventType] || 20;
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
export function eventTypeDistribution(events = [], { includeZero = false, limit = EVENT_TYPE_DEFINITIONS.length } = {}) {
|
|
35
64
|
const buckets = new Map(EVENT_TYPE_DEFINITIONS.map((item) => [
|
|
36
65
|
item.type,
|
|
@@ -41,14 +70,14 @@ export function eventTypeDistribution(events = [], { includeZero = false, limit
|
|
|
41
70
|
description: item.description,
|
|
42
71
|
order: item.order,
|
|
43
72
|
events: 0,
|
|
44
|
-
|
|
73
|
+
seconds: 0
|
|
45
74
|
}
|
|
46
75
|
]));
|
|
47
76
|
for (const event of events) {
|
|
48
77
|
const type = EVENT_TYPES.has(event.eventType) ? event.eventType : "tool_call";
|
|
49
78
|
const bucket = buckets.get(type);
|
|
50
79
|
bucket.events += 1;
|
|
51
|
-
bucket.
|
|
80
|
+
bucket.seconds += eventDurationSeconds(event);
|
|
52
81
|
}
|
|
53
82
|
return [...buckets.values()]
|
|
54
83
|
.filter((item) => includeZero || item.events > 0)
|
|
@@ -57,5 +86,8 @@ export function eventTypeDistribution(events = [], { includeZero = false, limit
|
|
|
57
86
|
return a.order - b.order;
|
|
58
87
|
})
|
|
59
88
|
.slice(0, limit)
|
|
60
|
-
.map(({ order, ...item }) =>
|
|
89
|
+
.map(({ order, seconds, ...item }) => ({
|
|
90
|
+
...item,
|
|
91
|
+
minutes: item.events ? Math.max(1, Math.round(seconds / 60)) : 0
|
|
92
|
+
}));
|
|
61
93
|
}
|
package/src/publish-npm.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export function usage() {
|
|
|
44
44
|
"Notes:",
|
|
45
45
|
" - Public npm scoped packages require --access public.",
|
|
46
46
|
" - For @scope/name, the npm account token must have permission for that scope.",
|
|
47
|
-
" - Token can be passed through NPM_TOKEN or --token; it is written only to a temporary .npmrc."
|
|
47
|
+
" - Token can be passed through NPM_TOKEN or --token; it is written only to a temporary .npmrc and deleted after publish."
|
|
48
48
|
].join("\n");
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -109,7 +109,7 @@ export function buildNpmPublishArgs(stagedDir, options = {}) {
|
|
|
109
109
|
"--access",
|
|
110
110
|
options.access || "public",
|
|
111
111
|
"--tag",
|
|
112
|
-
options.tag
|
|
112
|
+
normalizeNpmPublishTag(options.tag),
|
|
113
113
|
"--cache",
|
|
114
114
|
path.join(stagedDir, ".npm-cache"),
|
|
115
115
|
...(options.dryRun ? ["--dry-run"] : []),
|
|
@@ -118,17 +118,73 @@ export function buildNpmPublishArgs(stagedDir, options = {}) {
|
|
|
118
118
|
];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export function normalizeNpmPublishTag(value = "latest") {
|
|
122
|
+
const tag = String(value || "latest").trim() || "latest";
|
|
123
|
+
if (/^(?:v)?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(tag)) return "latest";
|
|
124
|
+
if (/^[~^<>=*]*\d/.test(tag)) return "latest";
|
|
125
|
+
return tag;
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
export function publishToNpm(options = {}) {
|
|
122
129
|
if (!options.dryRun && !options.token) {
|
|
123
130
|
throw new Error("NPM_TOKEN is required for real public npm publish; use --dry-run to preview without token");
|
|
124
131
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
let prepared = null;
|
|
133
|
+
try {
|
|
134
|
+
prepared = preparePublishDirectory(options);
|
|
135
|
+
const args = buildNpmPublishArgs(prepared.tempDir, options);
|
|
136
|
+
const result = spawnSync("npm", args, { encoding: "utf8" });
|
|
137
|
+
if (result.error) {
|
|
138
|
+
throw result.error;
|
|
139
|
+
}
|
|
140
|
+
if (result.status !== 0) {
|
|
141
|
+
throw new Error(formatNpmPublishFailure(result));
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
...prepared,
|
|
145
|
+
ok: true,
|
|
146
|
+
registry: options.registry || PUBLIC_NPM_REGISTRY,
|
|
147
|
+
tag: normalizeNpmPublishTag(options.tag),
|
|
148
|
+
dryRun: options.dryRun === true,
|
|
149
|
+
status: result.status,
|
|
150
|
+
stdout: sanitizeNpmOutput(result.stdout || ""),
|
|
151
|
+
stderr: sanitizeNpmOutput(result.stderr || "")
|
|
152
|
+
};
|
|
153
|
+
} finally {
|
|
154
|
+
if (prepared?.tempDir && !options.keepStagedDir) {
|
|
155
|
+
fs.rmSync(prepared.tempDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
130
157
|
}
|
|
131
|
-
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function formatNpmPublishFailure(result = {}) {
|
|
161
|
+
const status = result.status ?? result.signal ?? "unknown";
|
|
162
|
+
const output = summarizeNpmOutput([result.stderr, result.stdout].filter(Boolean).join("\n"));
|
|
163
|
+
const suffix = output ? `: ${output}` : "";
|
|
164
|
+
return `npm publish failed with exit code ${status}${suffix}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sanitizeNpmOutput(value = "") {
|
|
168
|
+
return String(value || "")
|
|
169
|
+
.replace(/(_authToken=)[^\s]+/gi, "$1<redacted>")
|
|
170
|
+
.replace(/(\/\/[^:\s]+\/?:_authToken=)[^\s]+/gi, "$1<redacted>")
|
|
171
|
+
.trim()
|
|
172
|
+
.slice(0, 4000);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function summarizeNpmOutput(value = "") {
|
|
176
|
+
const lines = sanitizeNpmOutput(value)
|
|
177
|
+
.split(/\r?\n/)
|
|
178
|
+
.map((line) => line.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.filter((line) => !/complete log of this run/i.test(line));
|
|
181
|
+
const errorLines = lines.filter((line) => /^npm\s+error\b/i.test(line));
|
|
182
|
+
const selected = errorLines.length
|
|
183
|
+
? errorLines
|
|
184
|
+
: lines.filter((line) => !/^npm\s+notice\b/i.test(line));
|
|
185
|
+
return (selected.length ? selected : lines.slice(-8))
|
|
186
|
+
.join("\n")
|
|
187
|
+
.slice(0, 1600);
|
|
132
188
|
}
|
|
133
189
|
|
|
134
190
|
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
|
package/src/uploader.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { EventQueue } from "./queue.mjs";
|
|
3
4
|
import { writeClientConfig } from "./config.mjs";
|
|
@@ -303,6 +304,7 @@ export class ClientAgent {
|
|
|
303
304
|
clientId: this.config.clientId,
|
|
304
305
|
clientVersion: policy.clientVersion,
|
|
305
306
|
tool: this.config.tool,
|
|
307
|
+
homeDir: this.config.homeDir,
|
|
306
308
|
modelProvider: this.config.model?.provider || "",
|
|
307
309
|
modelName: this.config.model?.name || "",
|
|
308
310
|
modelVersion: this.config.model?.version || "",
|
|
@@ -396,6 +398,20 @@ export class ClientAgent {
|
|
|
396
398
|
this.config.update?.enabled !== false && this.config.update?.autoUpdate !== false,
|
|
397
399
|
"中心端已关闭客户端静默更新"
|
|
398
400
|
));
|
|
401
|
+
if (this.config.tool === "codex") {
|
|
402
|
+
const codexHooks = codexHookDiagnostics(this.config);
|
|
403
|
+
checks.push(check(
|
|
404
|
+
"codex_hooks_configured",
|
|
405
|
+
codexHooks.configured,
|
|
406
|
+
"Codex hook 未配置到 ~/.codex/hooks.json,无法静默采集提示词、工具、MCP 和模式事件"
|
|
407
|
+
));
|
|
408
|
+
checks.push(check(
|
|
409
|
+
"codex_hooks_enabled",
|
|
410
|
+
codexHooks.enabled,
|
|
411
|
+
codexHooks.message
|
|
412
|
+
));
|
|
413
|
+
status.codexHooks = codexHooks;
|
|
414
|
+
}
|
|
399
415
|
|
|
400
416
|
const issues = checks.filter((item) => !item.ok).map((item) => item.message);
|
|
401
417
|
return {
|
|
@@ -413,6 +429,92 @@ function check(name, ok, message = "") {
|
|
|
413
429
|
return { name, ok: Boolean(ok), message: ok ? "" : message };
|
|
414
430
|
}
|
|
415
431
|
|
|
432
|
+
function codexHookDiagnostics(config) {
|
|
433
|
+
const homeDir = config.homeDir || "";
|
|
434
|
+
const hooksPath = path.join(homeDir, ".codex", "hooks.json");
|
|
435
|
+
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
436
|
+
const hookEvents = new Set(["session_start", "pre_tool_use", "post_tool_use", "user_prompt_submit", "stop"]);
|
|
437
|
+
const configuredEvents = codexConfiguredHookEvents(hooksPath);
|
|
438
|
+
const disabledStates = codexDisabledHookStates(configPath, hooksPath, hookEvents);
|
|
439
|
+
return {
|
|
440
|
+
hooksPath,
|
|
441
|
+
configPath,
|
|
442
|
+
configured: configuredEvents.length > 0,
|
|
443
|
+
configuredEvents,
|
|
444
|
+
enabled: disabledStates.length === 0,
|
|
445
|
+
disabledStates,
|
|
446
|
+
message: disabledStates.length
|
|
447
|
+
? `Codex hook 已配置但未启用:${disabledStates.join("、")},请重新运行安装命令或执行 worklens-agent-install 修复。`
|
|
448
|
+
: ""
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function codexConfiguredHookEvents(hooksPath) {
|
|
453
|
+
try {
|
|
454
|
+
if (!fs.existsSync(hooksPath)) return [];
|
|
455
|
+
const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
456
|
+
return Object.entries(hooks.hooks || {})
|
|
457
|
+
.filter(([, entries]) => entriesContainWorkLensHook(entries))
|
|
458
|
+
.map(([eventName]) => eventName);
|
|
459
|
+
} catch {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function entriesContainWorkLensHook(entries) {
|
|
465
|
+
return (Array.isArray(entries) ? entries : []).some((entry) => {
|
|
466
|
+
return (entry.hooks || []).some((hook) => workLensHookPattern().test(String(hook.command || "")));
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function workLensHookPattern() {
|
|
471
|
+
return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.sh)/i;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function codexDisabledHookStates(configPath, hooksPath, hookEvents) {
|
|
475
|
+
try {
|
|
476
|
+
if (!fs.existsSync(configPath)) return [];
|
|
477
|
+
const lines = fs.readFileSync(configPath, "utf8").split(/\n/);
|
|
478
|
+
let current = "";
|
|
479
|
+
let currentMatches = false;
|
|
480
|
+
const disabled = [];
|
|
481
|
+
for (const line of lines) {
|
|
482
|
+
const section = line.match(/^\[hooks\.state\."((?:\\.|[^"\\])*)"\]\s*$/);
|
|
483
|
+
if (section) {
|
|
484
|
+
current = parseTomlQuotedString(section[1]);
|
|
485
|
+
currentMatches = isCodexHookStateKey(current, hooksPath, hookEvents);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (/^\[[^\]]+\]\s*$/.test(line)) {
|
|
489
|
+
current = "";
|
|
490
|
+
currentMatches = false;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (currentMatches && /^enabled\s*=\s*false\s*$/.test(line)) {
|
|
494
|
+
disabled.push(current.slice(`${hooksPath}:`.length).split(":")[0]);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return [...new Set(disabled)];
|
|
498
|
+
} catch {
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function parseTomlQuotedString(value) {
|
|
504
|
+
try {
|
|
505
|
+
return JSON.parse(`"${value}"`);
|
|
506
|
+
} catch {
|
|
507
|
+
return String(value || "");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isCodexHookStateKey(stateKey, hooksPath, hookEvents) {
|
|
512
|
+
const prefix = `${hooksPath}:`;
|
|
513
|
+
if (!String(stateKey || "").startsWith(prefix)) return false;
|
|
514
|
+
const eventName = String(stateKey).slice(prefix.length).split(":")[0];
|
|
515
|
+
return hookEvents.has(eventName);
|
|
516
|
+
}
|
|
517
|
+
|
|
416
518
|
function clampAccepted(value, batchLength) {
|
|
417
519
|
const accepted = Number(value);
|
|
418
520
|
if (!Number.isFinite(accepted)) return batchLength;
|