ai-worklens-agent 0.1.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.
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import { loadClientConfig } from "./config.mjs";
3
+ import { buildEvent } from "./event-builder.mjs";
4
+ import { ClientAgent } from "./uploader.mjs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ async function readStdin() {
9
+ const chunks = [];
10
+ for await (const chunk of process.stdin) chunks.push(chunk);
11
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
12
+ if (!raw) return {};
13
+ return JSON.parse(raw);
14
+ }
15
+
16
+ function parseArgs(argv) {
17
+ const result = {};
18
+ for (let index = 0; index < argv.length; index += 1) {
19
+ const arg = argv[index];
20
+ if (!arg.startsWith("--")) continue;
21
+ const key = arg.slice(2);
22
+ const next = argv[index + 1];
23
+ if (!next || next.startsWith("--")) result[key] = true;
24
+ else {
25
+ result[key] = next;
26
+ index += 1;
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+
32
+ function hookName(payload, args) {
33
+ return String(
34
+ args.event ||
35
+ args.hook ||
36
+ payload.event ||
37
+ payload.eventName ||
38
+ payload.event_name ||
39
+ payload.hook ||
40
+ payload.hook_event ||
41
+ payload.hook_event_name ||
42
+ payload.hookEventName ||
43
+ payload.type ||
44
+ ""
45
+ ).trim().toLowerCase();
46
+ }
47
+
48
+ function eventTypeFromHook(name, payload) {
49
+ const compact = name.replace(/[^a-z0-9]/g, "");
50
+ const explicit = payload.eventType || payload.event_type;
51
+ if (explicit) return explicit;
52
+ if (["modechange", "modechanged", "codexmodechange", "planmodestart", "planmodeend"].includes(compact)) return "mode_change";
53
+ if (["sessionstatus", "sessionupdated"].includes(compact) && modeFromPayload(payload)) return "mode_change";
54
+ if (["sessionstart", "setup", "sessioncreated", "serverconnected"].includes(compact)) return "session_start";
55
+ if (["sessionend", "stop", "subagentstop", "teammateidle", "sessionidle", "sessiondeleted"].includes(compact)) return "session_end";
56
+ if (["userpromptsubmit", "userprompt", "promptsubmit"].includes(compact)) return "user_prompt";
57
+ if (["tuipromptappend"].includes(compact)) return "user_prompt";
58
+ if (["user_prompt", "prompt", "user_message", "user_input"].includes(name)) return "user_prompt";
59
+ if (["messageupdated", "messagepartupdated", "assistantresponse", "assistantturn", "assistantmessage"].includes(compact)) return "assistant_response";
60
+ if (["assistant_response", "assistant_turn", "assistant_message", "ai_response"].includes(name)) return "assistant_response";
61
+ if (["skill_use", "skill"].includes(name)) return "skill_use";
62
+ if (["plugin_use", "plugin"].includes(name)) return "plugin_use";
63
+ if (["mcp_tool_call", "mcp"].includes(name)) return "mcp_tool_call";
64
+ if (["permissionrequest", "permissionasked", "permissionreplied", "permissiondenied"].includes(compact)) return "permission";
65
+ if (["posttoolusefailure", "stopfailure", "permissiondenied", "sessionerror"].includes(compact)) return "error";
66
+ if (["filechanged", "fileedited", "sessiondiff", "worktreecreate", "worktreeremove"].includes(compact)) return "implementation";
67
+ if (["taskcreated", "todoupdated", "precompact"].includes(compact)) return "planning";
68
+ if (["taskcompleted", "postcompact"].includes(compact)) return "implementation";
69
+ if (["lspclientdiagnostics"].includes(compact)) return "verification";
70
+ if (["pretooluse", "toolcall", "tooluse", "toolusebefore", "toolexecutebefore"].includes(compact)) {
71
+ const toolName = toolNameFromPayload(payload);
72
+ const command = commandFromPayload(payload);
73
+ return /bash|shell|terminal|command/i.test(toolName) ? commandEventType(command) : "tool_call";
74
+ }
75
+ if (["posttooluse", "posttoolbatch", "tooluseafter", "toolexecuteafter"].includes(compact)) {
76
+ const toolName = toolNameFromPayload(payload);
77
+ const command = commandFromPayload(payload);
78
+ return /bash|shell|terminal|command/i.test(toolName) ? commandEventType(command) : "tool_result";
79
+ }
80
+ if (["commandexecuted", "tuicommandexecute"].includes(compact)) return commandEventType(commandFromPayload(payload));
81
+ if (["command", "command_exit", "post_command"].includes(name)) {
82
+ return commandEventType(commandFromPayload(payload));
83
+ }
84
+ if (["tool_call", "tool"].includes(name)) return "tool_call";
85
+ if (["verification", "test", "build"].includes(name)) return "verification";
86
+ if (["error", "failure"].includes(name)) return "error";
87
+ if (["retry", "rerun"].includes(name)) return "retry";
88
+ if (["rework", "fix"].includes(name)) return "rework";
89
+ if (["session_start"].includes(name)) return "session_start";
90
+ if (["session_end"].includes(name)) return "session_end";
91
+ return "tool_call";
92
+ }
93
+
94
+ function commandEventType(command) {
95
+ return /test|lint|typecheck|build|playwright|smoke|check/i.test(command) ? "verification" : "command";
96
+ }
97
+
98
+ function toolNameFromPayload(payload) {
99
+ return payload.toolName ||
100
+ payload.tool_name ||
101
+ payload.tool ||
102
+ payload.input?.tool ||
103
+ payload.output?.tool ||
104
+ payload.request?.tool ||
105
+ "";
106
+ }
107
+
108
+ function firstText(...values) {
109
+ for (const value of values) {
110
+ if (value === undefined || value === null || value === "") continue;
111
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
112
+ }
113
+ return "";
114
+ }
115
+
116
+ function modeFromPayload(payload) {
117
+ return firstText(
118
+ payload.mode,
119
+ payload.codexMode,
120
+ payload.codex_mode,
121
+ payload.status?.mode,
122
+ payload.session?.mode,
123
+ payload.input?.mode,
124
+ payload.output?.mode,
125
+ payload.output?.status,
126
+ payload.status
127
+ );
128
+ }
129
+
130
+ function previousModeFromPayload(payload) {
131
+ return firstText(
132
+ payload.previousMode,
133
+ payload.previous_mode,
134
+ payload.fromMode,
135
+ payload.from_mode,
136
+ payload.oldMode,
137
+ payload.old_mode,
138
+ payload.input?.previousMode,
139
+ payload.input?.fromMode,
140
+ payload.output?.previousMode
141
+ );
142
+ }
143
+
144
+ function nextModeFromPayload(payload) {
145
+ return firstText(
146
+ payload.nextMode,
147
+ payload.next_mode,
148
+ payload.toMode,
149
+ payload.to_mode,
150
+ payload.newMode,
151
+ payload.new_mode,
152
+ payload.input?.nextMode,
153
+ payload.input?.toMode,
154
+ payload.output?.nextMode,
155
+ modeFromPayload(payload)
156
+ );
157
+ }
158
+
159
+ function commandFromPayload(payload) {
160
+ return payload.commands ||
161
+ payload.command ||
162
+ payload.cmd ||
163
+ payload.args?.command ||
164
+ payload.input?.command ||
165
+ payload.output?.command ||
166
+ payload.tool_input?.command ||
167
+ payload.toolInput?.command ||
168
+ payload.tool_args?.command ||
169
+ "";
170
+ }
171
+
172
+ function statusFromPayload(payload) {
173
+ if (payload.success === true || payload.output?.success === true) return "success";
174
+ if (payload.success === false || payload.output?.success === false) return "failed";
175
+ return firstText(
176
+ payload.status,
177
+ payload.output?.status,
178
+ payload.result?.status,
179
+ payload.error ? "failed" : ""
180
+ );
181
+ }
182
+
183
+ function permissionDecisionFromPayload(payload) {
184
+ return firstText(
185
+ payload.decision,
186
+ payload.permission?.decision,
187
+ payload.output?.decision,
188
+ payload.output?.permission?.decision,
189
+ payload.input?.decision,
190
+ /denied|deny/i.test(String(payload.status || payload.error || "")) ? "denied" : ""
191
+ );
192
+ }
193
+
194
+ function fileRefsFromPayload(payload) {
195
+ return payload.files ||
196
+ payload.changedFiles ||
197
+ payload.changed_files ||
198
+ payload.file ||
199
+ payload.filePath ||
200
+ payload.file_path ||
201
+ payload.path ||
202
+ payload.input?.file ||
203
+ payload.input?.path ||
204
+ payload.input?.filePath ||
205
+ payload.input?.file_path ||
206
+ payload.output?.file ||
207
+ payload.output?.path ||
208
+ payload.tool_input?.file_path ||
209
+ payload.tool_input?.filePath ||
210
+ payload.toolInput?.file_path ||
211
+ payload.toolInput?.filePath;
212
+ }
213
+
214
+ function pickSummary(payload, name, eventType) {
215
+ if (eventType === "mode_change") {
216
+ const previousMode = previousModeFromPayload(payload);
217
+ const nextMode = nextModeFromPayload(payload);
218
+ return {
219
+ title: nextMode ? `模式切换:${nextMode}` : "模式切换",
220
+ content: previousMode && nextMode ? `${previousMode} -> ${nextMode}` : (nextMode || modeFromPayload(payload) || `hook:${name}`)
221
+ };
222
+ }
223
+ if (eventType === "permission") {
224
+ const decision = permissionDecisionFromPayload(payload);
225
+ const toolName = toolNameFromPayload(payload);
226
+ return {
227
+ title: decision ? `权限:${decision}` : "权限决策",
228
+ content: [toolName, decision, payload.reason || payload.error || payload.message].filter(Boolean).join(" / ") || `hook:${name}`
229
+ };
230
+ }
231
+ const title = payload.title ||
232
+ payload.summary?.title ||
233
+ payload.skillName ||
234
+ payload.skill_name ||
235
+ payload.pluginName ||
236
+ payload.plugin_name ||
237
+ payload.mcpServer ||
238
+ payload.mcp_server ||
239
+ payload.toolName ||
240
+ payload.tool_name ||
241
+ payload.tool ||
242
+ payload.input?.tool ||
243
+ payload.output?.tool ||
244
+ eventType;
245
+ const content = payload.content ||
246
+ payload.summary?.content ||
247
+ payload.summary ||
248
+ payload.message ||
249
+ payload.prompt ||
250
+ payload.promptText ||
251
+ payload.responseSummary ||
252
+ payload.command ||
253
+ payload.cmd ||
254
+ payload.args?.command ||
255
+ payload.input?.command ||
256
+ payload.output?.command ||
257
+ payload.tool_input?.command ||
258
+ payload.toolInput?.command ||
259
+ payload.reason ||
260
+ payload.error ||
261
+ payload.status ||
262
+ `hook:${name}`;
263
+ return { title, content };
264
+ }
265
+
266
+ export function normalizeHookPayload(payload, args, config) {
267
+ const name = hookName(payload, args);
268
+ const eventType = eventTypeFromHook(name, payload);
269
+ const summary = pickSummary(payload, name, eventType);
270
+ const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
271
+ return buildEvent({
272
+ ...payload,
273
+ eventType,
274
+ source: payload.source || `${config.tool}_hook`,
275
+ title: summary.title,
276
+ content: summary.content,
277
+ hookName: name,
278
+ localSessionId: payload.sessionId || payload.session_id || payload.localSessionId,
279
+ turnIndex: payload.turnIndex || payload.turn_index,
280
+ skillName: payload.skillName || payload.skill_name || payload.skill,
281
+ pluginName: payload.pluginName || payload.plugin_name || payload.plugin,
282
+ mcpServer: payload.mcpServer || payload.mcp_server || payload.server,
283
+ files: fileRefsFromPayload(payload),
284
+ commands: commandFromPayload(payload),
285
+ durationSeconds: payload.durationSeconds || payload.duration_seconds || payload.duration_ms && Number(payload.duration_ms) / 1000,
286
+ metadata: {
287
+ ...metadata,
288
+ hookAdapter: "ai-worklens",
289
+ sourceTool: config.tool,
290
+ rawHookEvent: name,
291
+ hookEventName: payload.hook_event_name || payload.hookEventName || payload.event || payload.type || name,
292
+ toolName: toolNameFromPayload(payload),
293
+ codexMode: nextModeFromPayload(payload),
294
+ interactionType: eventType,
295
+ cwd: payload.cwd || payload.directory || payload.project?.directory,
296
+ transcriptPath: payload.transcript_path || payload.transcriptPath,
297
+ permissionDecision: permissionDecisionFromPayload(payload),
298
+ exitCode: payload.exitCode ?? payload.exit_code,
299
+ success: payload.success,
300
+ status: statusFromPayload(payload)
301
+ },
302
+ process: {
303
+ interactionType: eventType,
304
+ codexMode: nextModeFromPayload(payload),
305
+ previousMode: previousModeFromPayload(payload),
306
+ nextMode: nextModeFromPayload(payload),
307
+ phase: payload.phase || payload.input?.phase || payload.output?.phase,
308
+ toolName: toolNameFromPayload(payload),
309
+ toolStatus: statusFromPayload(payload),
310
+ exitCode: payload.exitCode ?? payload.exit_code ?? payload.output?.exitCode ?? payload.output?.exit_code,
311
+ permissionDecision: permissionDecisionFromPayload(payload),
312
+ retryAttempt: payload.retryAttempt || payload.retry_attempt || payload.attempt || payload.input?.attempt,
313
+ failureKind: payload.failureKind || payload.failure_kind || payload.errorCode || payload.error_code || payload.error?.code
314
+ }
315
+ }, config);
316
+ }
317
+
318
+ async function main() {
319
+ const args = parseArgs(process.argv.slice(2));
320
+ const payload = await readStdin();
321
+ const config = loadClientConfig({
322
+ configFile: args.config,
323
+ queueFile: args.queue,
324
+ serverUrl: args["server-url"],
325
+ collectorToken: args["collector-token"],
326
+ employeeId: args["employee-id"],
327
+ employeeName: args["employee-name"],
328
+ department: args.department,
329
+ role: args.role,
330
+ clientId: args["client-id"],
331
+ tool: args.tool,
332
+ modelProvider: args["model-provider"] || payload.modelProvider || payload.model_provider,
333
+ modelName: args["model-name"] || payload.modelName || payload.model_name || payload.model,
334
+ modelVersion: args["model-version"] || payload.modelVersion || payload.model_version,
335
+ modelFamily: args["model-family"] || payload.modelFamily || payload.model_family,
336
+ repoName: args["repo-name"],
337
+ workspaceRoot: args.workspace,
338
+ branch: args.branch
339
+ });
340
+ const event = normalizeHookPayload(payload, args, config);
341
+ const agent = new ClientAgent(config);
342
+ const result = await agent.record(event);
343
+ process.stdout.write(`${JSON.stringify({ ok: true, hook: event.metadata.rawHookEvent, eventType: event.eventType, eventId: event.eventId, result })}\n`);
344
+ }
345
+
346
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
347
+ main().catch((error) => {
348
+ process.stderr.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
349
+ process.exitCode = 1;
350
+ });
351
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { loadClientConfig } from "./config.mjs";
5
+ import { normalizeHookPayload } from "./hook-adapter.mjs";
6
+ import { normalizeToolId } from "../../collector-protocol/src/tool-profiles.mjs";
7
+
8
+ const SAMPLE_PAYLOADS = {
9
+ codex: {
10
+ event: "plugin_use",
11
+ pluginName: "Figma",
12
+ message: "使用 Figma 插件同步设计稿信息",
13
+ sessionId: "smoke-codex"
14
+ },
15
+ "claude-code": {
16
+ hook_event_name: "PostToolUse",
17
+ tool_name: "Bash",
18
+ tool_input: { command: "npm test" },
19
+ session_id: "smoke-claude"
20
+ },
21
+ opencode: {
22
+ event: "tool.execute.after",
23
+ input: { tool: "bash", command: "npm run build" },
24
+ sessionId: "smoke-opencode"
25
+ }
26
+ };
27
+
28
+ function parseArgs(argv) {
29
+ const result = {};
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const arg = argv[index];
32
+ if (!arg.startsWith("--")) continue;
33
+ const key = arg.slice(2);
34
+ const next = argv[index + 1];
35
+ if (!next || next.startsWith("--")) result[key] = true;
36
+ else {
37
+ result[key] = next;
38
+ index += 1;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function selectedTools(tool) {
45
+ if (!tool || tool === "all") return Object.keys(SAMPLE_PAYLOADS);
46
+ return [normalizeToolId(tool)];
47
+ }
48
+
49
+ export function runHookSmoke(config, options = {}) {
50
+ const tools = selectedTools(options.tool);
51
+ const items = tools.map((tool) => {
52
+ const payload = SAMPLE_PAYLOADS[tool];
53
+ if (!payload) {
54
+ return { tool, ok: false, error: "unsupported tool" };
55
+ }
56
+ try {
57
+ const event = normalizeHookPayload(payload, { event: payload.event || payload.hook_event_name }, {
58
+ ...config,
59
+ tool
60
+ });
61
+ return {
62
+ tool,
63
+ ok: true,
64
+ source: event.source,
65
+ eventType: event.eventType,
66
+ summary: event.summary.title,
67
+ commands: event.refs.commands,
68
+ files: event.refs.files
69
+ };
70
+ } catch (error) {
71
+ return { tool, ok: false, error: error.message };
72
+ }
73
+ });
74
+ return {
75
+ ok: items.every((item) => item.ok),
76
+ checkedAt: new Date().toISOString(),
77
+ items
78
+ };
79
+ }
80
+
81
+ async function main() {
82
+ const args = parseArgs(process.argv.slice(2));
83
+ const config = loadClientConfig({
84
+ configFile: args.config,
85
+ queueFile: args.queue,
86
+ employeeId: args["employee-id"] || "SMOKE",
87
+ employeeName: args["employee-name"] || "Hook Smoke",
88
+ department: args.department || "验证",
89
+ role: args.role || "验证",
90
+ clientId: args["client-id"] || "hook-smoke",
91
+ tool: args.tool === "all" ? "codex" : args.tool,
92
+ modelProvider: args["model-provider"],
93
+ modelName: args["model-name"],
94
+ modelVersion: args["model-version"],
95
+ modelFamily: args["model-family"]
96
+ });
97
+ process.stdout.write(`${JSON.stringify(runHookSmoke(config, { tool: args.tool || "all" }), null, 2)}\n`);
98
+ }
99
+
100
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
101
+ main().catch((error) => {
102
+ process.stderr.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
103
+ process.exitCode = 1;
104
+ });
105
+ }