ai-worklens-agent 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/package.json +7 -2
- package/src/config.mjs +10 -0
- package/src/hook-adapter.mjs +187 -8
- package/src/protocol/client-update-policy.mjs +1 -1
- package/src/publish-npm.mjs +4 -3
- package/src/runtime-state.mjs +79 -0
- package/src/uploader.mjs +69 -1
package/README.md
CHANGED
|
@@ -38,18 +38,20 @@ npm run mcp
|
|
|
38
38
|
Windows 命令提示符或 PowerShell 使用一行命令,不要混用 macOS/Linux 的反斜杠换行:
|
|
39
39
|
|
|
40
40
|
```bat
|
|
41
|
-
npx -y --loglevel=error --registry https://registry.npmjs.org -p ai-worklens-agent@0.1.
|
|
41
|
+
npx -y --loglevel=error --registry https://registry.npmjs.org -p ai-worklens-agent@0.1.7 worklens-agent-install --server-url http://192.168.1.241:8797 --tool codex --employee-pinyin zhangpeng
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
macOS / Linux:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.
|
|
47
|
+
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.7 worklens-agent-install \
|
|
48
48
|
--server-url http://192.168.1.241:8797 \
|
|
49
49
|
--tool codex \
|
|
50
50
|
--employee-pinyin zhangsan
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
安装完成后请重新打开一个新的 Codex 会话。第一次触发 Hook 时,如果 Codex 提示信任或允许 Hook,请选择信任/允许;否则 `codex-hook.cmd` 手动验证可以成功,但真实 Codex 会话不会触发采集。
|
|
54
|
+
|
|
53
55
|
发布到公共 npm 源需要 npm 账号 token。Scoped 包名需要账号拥有对应 scope。
|
|
54
56
|
|
|
55
57
|
```bash
|
|
@@ -68,7 +70,7 @@ NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
|
|
|
68
70
|
如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
|
-
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.
|
|
73
|
+
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.7.sh \
|
|
72
74
|
-o ai-worklens-install.sh
|
|
73
75
|
chmod +x ai-worklens-install.sh
|
|
74
76
|
./ai-worklens-install.sh zhangsan
|
|
@@ -97,6 +99,8 @@ Windows 命令提示符验证 Codex hook 是否真实可执行:
|
|
|
97
99
|
echo {"hook_event_name":"UserPromptSubmit","prompt":"worklens windows smoke","session_id":"manual-smoke"} | "%USERPROFILE%\.ai-worklens\codex-hook.cmd"
|
|
98
100
|
```
|
|
99
101
|
|
|
102
|
+
如果这条命令成功上报,但真实 Codex 会话仍没有数据,优先检查是否已经在 Codex 中信任/允许了 Hook,并确认使用的是安装后新打开的 Codex 会话。
|
|
103
|
+
|
|
100
104
|
模拟 Claude Code 工具 hook 输入:
|
|
101
105
|
|
|
102
106
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-worklens-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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
|
@@ -121,6 +121,14 @@ export function defaultQueueFile(configFile = defaultConfigFile()) {
|
|
|
121
121
|
return envValue(process.env, "WORKLENS_QUEUE_FILE") || path.join(path.dirname(configFile), "queue.json");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export function defaultStateFile(configFile = defaultConfigFile()) {
|
|
125
|
+
return envValue(process.env, "WORKLENS_STATE_FILE") || path.join(path.dirname(configFile), "state.json");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function defaultLogDir(configFile = defaultConfigFile()) {
|
|
129
|
+
return envValue(process.env, "WORKLENS_LOG_DIR") || path.join(path.dirname(configFile), "logs");
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
export function loadClientConfig(options = {}) {
|
|
125
133
|
const configFile = options.configFile || defaultConfigFile();
|
|
126
134
|
const fileConfig = readJson(configFile);
|
|
@@ -150,6 +158,8 @@ export function loadClientConfig(options = {}) {
|
|
|
150
158
|
configFile,
|
|
151
159
|
homeDir,
|
|
152
160
|
queueFile: options.queueFile || defaultQueueFile(configFile),
|
|
161
|
+
stateFile: options.stateFile || defaultStateFile(configFile),
|
|
162
|
+
logDir: options.logDir || defaultLogDir(configFile),
|
|
153
163
|
serverUrl,
|
|
154
164
|
collectorToken: options.collectorToken || envValue(env, "WORKLENS_COLLECTOR_TOKEN") || fileConfig.collectorToken || "",
|
|
155
165
|
clientId: options.clientId || envValue(env, "WORKLENS_CLIENT_ID") || fileConfig.clientId || defaultClientId(),
|
package/src/hook-adapter.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { loadClientConfig } from "./config.mjs";
|
|
3
3
|
import { buildEvent } from "./event-builder.mjs";
|
|
4
4
|
import { ClientAgent } from "./uploader.mjs";
|
|
5
|
+
import { appendRuntimeLog, eventLogEntry, updateRuntimeState } from "./runtime-state.mjs";
|
|
6
|
+
import fs from "node:fs";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
|
|
@@ -152,6 +154,10 @@ function modeFromPayload(payload) {
|
|
|
152
154
|
);
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
function sessionIdFromPayload(payload) {
|
|
158
|
+
return firstText(payload.sessionId, payload.session_id, payload.localSessionId, payload.conversationId, payload.conversation_id);
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
function skillNameFromPayload(payload) {
|
|
156
162
|
return firstText(
|
|
157
163
|
payload.skillName,
|
|
@@ -341,7 +347,51 @@ function fileRefsFromPayload(payload) {
|
|
|
341
347
|
payload.toolInput?.filePath;
|
|
342
348
|
}
|
|
343
349
|
|
|
344
|
-
function
|
|
350
|
+
function compactSummaryText(value, options = {}) {
|
|
351
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
352
|
+
if (!text) return "";
|
|
353
|
+
if (options.allowRaw) return text;
|
|
354
|
+
const longPatch = /\*\*\*\s+Begin Patch|\bdiff --git\b|(?:^|\s)@@\s|^\s*[+-]{3}\s/m.test(String(value || ""));
|
|
355
|
+
if (longPatch && text.length > 240) {
|
|
356
|
+
return "围绕代码补丁或文件修改进行协作,包含较长 diff/patch 内容,已在采集端摘要化。";
|
|
357
|
+
}
|
|
358
|
+
const longCommand = options.kind && ["command", "verification", "tool_call", "tool_result"].includes(options.kind);
|
|
359
|
+
if (longCommand && text.length > 420) {
|
|
360
|
+
return `${text.slice(0, 220)} ...(较长工具输入已摘要)`;
|
|
361
|
+
}
|
|
362
|
+
if (text.length <= 420) return text;
|
|
363
|
+
return `${text.slice(0, 260)} ... ${text.slice(-100)}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function allowRawContent(config, eventType) {
|
|
367
|
+
if (eventType === "user_prompt") return config.collection?.storeRawPrompts === true;
|
|
368
|
+
if (eventType === "assistant_response") return config.collection?.storeFullReplies === true;
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isDiagnosticPayload(payload, args, name) {
|
|
373
|
+
const sessionId = sessionIdFromPayload(payload);
|
|
374
|
+
const text = [
|
|
375
|
+
payload.prompt,
|
|
376
|
+
payload.message,
|
|
377
|
+
payload.summary,
|
|
378
|
+
payload.content,
|
|
379
|
+
payload.title,
|
|
380
|
+
payload.input?.prompt,
|
|
381
|
+
payload.input?.message
|
|
382
|
+
].map((item) => String(item || "")).join(" ");
|
|
383
|
+
return Boolean(
|
|
384
|
+
args.diagnostic === true ||
|
|
385
|
+
args.diagnostic === "true" ||
|
|
386
|
+
payload.diagnostic === true ||
|
|
387
|
+
payload.metadata?.diagnostic === true ||
|
|
388
|
+
/^smoke-|^hook-smoke$|^manual-smoke$/i.test(sessionId) ||
|
|
389
|
+
/worklens\s+(?:windows\s+)?smoke|hook\s+smoke|ai-worklens\s+smoke/i.test(text) ||
|
|
390
|
+
/hook-smoke/i.test(name)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function pickSummary(payload, name, eventType, config) {
|
|
345
395
|
if (eventType === "mode_change") {
|
|
346
396
|
const previousMode = previousModeFromPayload(payload);
|
|
347
397
|
const nextMode = nextModeFromPayload(payload);
|
|
@@ -390,18 +440,22 @@ function pickSummary(payload, name, eventType) {
|
|
|
390
440
|
payload.error ||
|
|
391
441
|
payload.status ||
|
|
392
442
|
`hook:${name}`;
|
|
393
|
-
return {
|
|
443
|
+
return {
|
|
444
|
+
title: compactSummaryText(title, { allowRaw: true }),
|
|
445
|
+
content: compactSummaryText(content, { allowRaw: allowRawContent(config, eventType), kind: eventType })
|
|
446
|
+
};
|
|
394
447
|
}
|
|
395
448
|
|
|
396
449
|
export function normalizeHookPayload(payload, args, config) {
|
|
397
450
|
const name = hookName(payload, args);
|
|
398
451
|
const eventType = eventTypeFromHook(name, payload);
|
|
399
|
-
const summary = pickSummary(payload, name, eventType);
|
|
452
|
+
const summary = pickSummary(payload, name, eventType, config);
|
|
400
453
|
const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
|
|
401
454
|
const skillName = skillNameFromPayload(payload);
|
|
402
455
|
const pluginName = pluginNameFromPayload(payload);
|
|
403
456
|
const mcpServer = mcpServerFromPayload(payload);
|
|
404
457
|
const toolName = toolNameFromPayload(payload);
|
|
458
|
+
const diagnostic = isDiagnosticPayload(payload, args, name);
|
|
405
459
|
return buildEvent({
|
|
406
460
|
...payload,
|
|
407
461
|
eventType,
|
|
@@ -409,7 +463,7 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
409
463
|
title: summary.title,
|
|
410
464
|
content: summary.content,
|
|
411
465
|
hookName: name,
|
|
412
|
-
localSessionId: payload
|
|
466
|
+
localSessionId: sessionIdFromPayload(payload),
|
|
413
467
|
turnIndex: payload.turnIndex || payload.turn_index,
|
|
414
468
|
skillName,
|
|
415
469
|
pluginName,
|
|
@@ -435,7 +489,8 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
435
489
|
permissionDecision: permissionDecisionFromPayload(payload),
|
|
436
490
|
exitCode: payload.exitCode ?? payload.exit_code,
|
|
437
491
|
success: payload.success,
|
|
438
|
-
status: statusFromPayload(payload)
|
|
492
|
+
status: statusFromPayload(payload),
|
|
493
|
+
diagnostic
|
|
439
494
|
},
|
|
440
495
|
process: {
|
|
441
496
|
interactionType: eventType,
|
|
@@ -453,6 +508,108 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
453
508
|
}, config);
|
|
454
509
|
}
|
|
455
510
|
|
|
511
|
+
function textFromContent(value, depth = 0) {
|
|
512
|
+
if (depth > 5 || value === undefined || value === null) return "";
|
|
513
|
+
if (typeof value === "string") return value;
|
|
514
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
515
|
+
if (Array.isArray(value)) return value.map((item) => textFromContent(item, depth + 1)).filter(Boolean).join(" ");
|
|
516
|
+
if (typeof value === "object") {
|
|
517
|
+
if (typeof value.text === "string") return value.text;
|
|
518
|
+
if (typeof value.content === "string") return value.content;
|
|
519
|
+
if (Array.isArray(value.content)) return textFromContent(value.content, depth + 1);
|
|
520
|
+
if (typeof value.message === "string") return value.message;
|
|
521
|
+
if (value.message && typeof value.message === "object") return textFromContent(value.message, depth + 1);
|
|
522
|
+
if (typeof value.summary === "string") return value.summary;
|
|
523
|
+
}
|
|
524
|
+
return "";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function transcriptAssistantSummary(filePath) {
|
|
528
|
+
if (!filePath) return null;
|
|
529
|
+
try {
|
|
530
|
+
if (!fs.existsSync(filePath)) return null;
|
|
531
|
+
const stats = fs.statSync(filePath);
|
|
532
|
+
const maxBytes = 1_000_000;
|
|
533
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
534
|
+
const content = stats.size > maxBytes ? raw.slice(-maxBytes) : raw;
|
|
535
|
+
let lastAssistantText = "";
|
|
536
|
+
let turnIndex = 0;
|
|
537
|
+
for (const line of content.split(/\r?\n/)) {
|
|
538
|
+
if (!line.trim()) continue;
|
|
539
|
+
let item = null;
|
|
540
|
+
try {
|
|
541
|
+
item = JSON.parse(line);
|
|
542
|
+
} catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const message = item.message && typeof item.message === "object" ? item.message : item;
|
|
546
|
+
const role = String(message.role || item.role || item.type || "").toLowerCase();
|
|
547
|
+
const text = textFromContent(message.content || message.text || message.summary || message);
|
|
548
|
+
if (!text) continue;
|
|
549
|
+
if (role.includes("user")) turnIndex += 1;
|
|
550
|
+
if (role.includes("assistant")) lastAssistantText = text;
|
|
551
|
+
}
|
|
552
|
+
if (!lastAssistantText) return null;
|
|
553
|
+
return { text: lastAssistantText, turnIndex };
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function assistantSummaryFromPayload(payload) {
|
|
560
|
+
const direct = firstText(
|
|
561
|
+
payload.assistantSummary,
|
|
562
|
+
payload.assistant_summary,
|
|
563
|
+
payload.assistantResponse,
|
|
564
|
+
payload.assistant_response,
|
|
565
|
+
payload.responseSummary,
|
|
566
|
+
payload.response_summary,
|
|
567
|
+
payload.outputSummary,
|
|
568
|
+
payload.output_summary,
|
|
569
|
+
payload.output?.summary,
|
|
570
|
+
payload.output?.responseSummary,
|
|
571
|
+
payload.result?.summary
|
|
572
|
+
);
|
|
573
|
+
if (direct) return { text: direct, turnIndex: Number(payload.turnIndex || payload.turn_index || 0) };
|
|
574
|
+
return transcriptAssistantSummary(payload.transcript_path || payload.transcriptPath);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function assistantEventFromHook(payload, args, config, baseEvent) {
|
|
578
|
+
const name = hookName(payload, args);
|
|
579
|
+
if (baseEvent.eventType === "assistant_response") return null;
|
|
580
|
+
if (baseEvent.eventType !== "session_end") return null;
|
|
581
|
+
const summary = assistantSummaryFromPayload(payload);
|
|
582
|
+
if (!summary?.text) return null;
|
|
583
|
+
const diagnostic = Boolean(baseEvent.metadata?.diagnostic);
|
|
584
|
+
return buildEvent({
|
|
585
|
+
eventType: "assistant_response",
|
|
586
|
+
source: payload.source || `${config.tool}_hook`,
|
|
587
|
+
title: "AI 回复摘要",
|
|
588
|
+
content: compactSummaryText(summary.text, { allowRaw: allowRawContent(config, "assistant_response"), kind: "assistant_response" }),
|
|
589
|
+
localSessionId: sessionIdFromPayload(payload) || baseEvent.session?.localSessionId,
|
|
590
|
+
turnIndex: summary.turnIndex || payload.turnIndex || payload.turn_index,
|
|
591
|
+
durationSeconds: payload.assistantDurationSeconds || payload.assistant_duration_seconds || 0,
|
|
592
|
+
metadata: {
|
|
593
|
+
hookAdapter: "ai-worklens",
|
|
594
|
+
sourceTool: config.tool,
|
|
595
|
+
rawHookEvent: name,
|
|
596
|
+
hookEventName: payload.hook_event_name || payload.hookEventName || payload.event || payload.type || name,
|
|
597
|
+
derivedFrom: payload.transcript_path || payload.transcriptPath ? "transcript" : "hook_payload",
|
|
598
|
+
transcriptPath: payload.transcript_path || payload.transcriptPath,
|
|
599
|
+
diagnostic
|
|
600
|
+
},
|
|
601
|
+
process: {
|
|
602
|
+
interactionType: "assistant_response"
|
|
603
|
+
}
|
|
604
|
+
}, config);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function normalizeHookEvents(payload, args, config) {
|
|
608
|
+
const baseEvent = normalizeHookPayload(payload, args, config);
|
|
609
|
+
const assistantEvent = assistantEventFromHook(payload, args, config, baseEvent);
|
|
610
|
+
return assistantEvent ? [baseEvent, assistantEvent] : [baseEvent];
|
|
611
|
+
}
|
|
612
|
+
|
|
456
613
|
async function main() {
|
|
457
614
|
const args = parseArgs(process.argv.slice(2));
|
|
458
615
|
const payload = await readStdin();
|
|
@@ -475,10 +632,32 @@ async function main() {
|
|
|
475
632
|
workspaceRoot: args.workspace,
|
|
476
633
|
branch: args.branch
|
|
477
634
|
});
|
|
478
|
-
const
|
|
635
|
+
const events = normalizeHookEvents(payload, args, config);
|
|
479
636
|
const agent = new ClientAgent(config);
|
|
480
|
-
const
|
|
481
|
-
|
|
637
|
+
const results = [];
|
|
638
|
+
for (const event of events) {
|
|
639
|
+
appendRuntimeLog(config, "hook", {
|
|
640
|
+
action: "received",
|
|
641
|
+
rawHookEvent: event.metadata.rawHookEvent,
|
|
642
|
+
...eventLogEntry(event)
|
|
643
|
+
});
|
|
644
|
+
updateRuntimeState(config, {
|
|
645
|
+
lastHookAt: event.occurredAt,
|
|
646
|
+
lastHookEventType: event.eventType,
|
|
647
|
+
lastHookEventId: event.eventId,
|
|
648
|
+
lastHookRawEvent: event.metadata.rawHookEvent || "",
|
|
649
|
+
lastHookDiagnostic: Boolean(event.metadata.diagnostic)
|
|
650
|
+
});
|
|
651
|
+
results.push({ eventId: event.eventId, eventType: event.eventType, result: await agent.record(event) });
|
|
652
|
+
}
|
|
653
|
+
process.stdout.write(`${JSON.stringify({
|
|
654
|
+
ok: true,
|
|
655
|
+
hook: events[0]?.metadata.rawHookEvent,
|
|
656
|
+
eventType: events[0]?.eventType,
|
|
657
|
+
eventId: events[0]?.eventId,
|
|
658
|
+
eventIds: events.map((event) => event.eventId),
|
|
659
|
+
events: results
|
|
660
|
+
})}\n`);
|
|
482
661
|
}
|
|
483
662
|
|
|
484
663
|
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
|
package/src/publish-npm.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export function parseArgs(argv = []) {
|
|
|
31
31
|
}
|
|
32
32
|
options.packageName = options.packageName || process.env.WORKLENS_NPM_PACKAGE_NAME || "";
|
|
33
33
|
options.version = options.version || process.env.WORKLENS_NPM_VERSION || "";
|
|
34
|
-
options.token = options.token || process.env.NPM_TOKEN || "";
|
|
34
|
+
options.token = options.token || process.env.NPM_TOKEN || process.env.WORKLENS_NPM_TOKEN || "";
|
|
35
35
|
return options;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -40,11 +40,12 @@ export function usage() {
|
|
|
40
40
|
"Usage:",
|
|
41
41
|
" npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest --dry-run",
|
|
42
42
|
" NPM_TOKEN=<token> npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest",
|
|
43
|
+
" WORKLENS_NPM_TOKEN=<token> npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest",
|
|
43
44
|
"",
|
|
44
45
|
"Notes:",
|
|
45
46
|
" - Public npm scoped packages require --access public.",
|
|
46
47
|
" - 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 and deleted after publish."
|
|
48
|
+
" - Token can be passed through NPM_TOKEN, WORKLENS_NPM_TOKEN or --token; it is written only to a temporary .npmrc and deleted after publish."
|
|
48
49
|
].join("\n");
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -127,7 +128,7 @@ export function normalizeNpmPublishTag(value = "latest") {
|
|
|
127
128
|
|
|
128
129
|
export function publishToNpm(options = {}) {
|
|
129
130
|
if (!options.dryRun && !options.token) {
|
|
130
|
-
throw new Error("NPM_TOKEN is required for real public npm publish; use --dry-run to preview without token");
|
|
131
|
+
throw new Error("NPM_TOKEN or WORKLENS_NPM_TOKEN is required for real public npm publish; use --dry-run to preview without token");
|
|
131
132
|
}
|
|
132
133
|
let prepared = null;
|
|
133
134
|
try {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function baseDir(config = {}) {
|
|
5
|
+
return path.dirname(config.configFile || config.queueFile || path.join(process.cwd(), "client.json"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function runtimeStateFile(config = {}) {
|
|
9
|
+
return config.stateFile || path.join(baseDir(config), "state.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function runtimeLogDir(config = {}) {
|
|
13
|
+
return config.logDir || path.join(baseDir(config), "logs");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson(filePath, fallback) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJsonAtomic(filePath, value) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
27
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
28
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
29
|
+
fs.renameSync(tempPath, filePath);
|
|
30
|
+
fs.chmodSync(filePath, 0o600);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readRuntimeState(config = {}) {
|
|
34
|
+
return readJson(runtimeStateFile(config), {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateRuntimeState(config = {}, patch = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const filePath = runtimeStateFile(config);
|
|
40
|
+
const current = readJson(filePath, {});
|
|
41
|
+
const next = {
|
|
42
|
+
...current,
|
|
43
|
+
...patch,
|
|
44
|
+
updatedAt: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
writeJsonAtomic(filePath, next);
|
|
47
|
+
return next;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appendRuntimeLog(config = {}, name, entry = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const directory = runtimeLogDir(config);
|
|
56
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
57
|
+
const filePath = path.join(directory, `${name}.log`);
|
|
58
|
+
const line = {
|
|
59
|
+
at: new Date().toISOString(),
|
|
60
|
+
...entry
|
|
61
|
+
};
|
|
62
|
+
fs.appendFileSync(filePath, `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
63
|
+
fs.chmodSync(filePath, 0o600);
|
|
64
|
+
return filePath;
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function eventLogEntry(event = {}) {
|
|
71
|
+
return {
|
|
72
|
+
eventId: event.eventId,
|
|
73
|
+
eventType: event.eventType,
|
|
74
|
+
source: event.source,
|
|
75
|
+
sessionId: event.session?.localSessionId || "",
|
|
76
|
+
turnIndex: event.session?.turnIndex || 0,
|
|
77
|
+
diagnostic: Boolean(event.metadata?.diagnostic)
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/uploader.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { writeClientConfig } from "./config.mjs";
|
|
|
5
5
|
import { normalizeCollectionSettings } from "./protocol/collection-settings.mjs";
|
|
6
6
|
import { normalizeClientUpdatePolicy, updateNeeded } from "./protocol/client-update-policy.mjs";
|
|
7
7
|
import { installClient } from "./install.mjs";
|
|
8
|
+
import { appendRuntimeLog, eventLogEntry, readRuntimeState, updateRuntimeState } from "./runtime-state.mjs";
|
|
8
9
|
|
|
9
10
|
async function requestJson(url, config, options = {}) {
|
|
10
11
|
const controller = new AbortController();
|
|
@@ -69,7 +70,30 @@ export class ClientAgent {
|
|
|
69
70
|
|
|
70
71
|
async record(event) {
|
|
71
72
|
const length = await this.queue.enqueue(event);
|
|
73
|
+
appendRuntimeLog(this.config, "events", {
|
|
74
|
+
action: "enqueue",
|
|
75
|
+
queueSize: length,
|
|
76
|
+
...eventLogEntry(event)
|
|
77
|
+
});
|
|
78
|
+
updateRuntimeState(this.config, {
|
|
79
|
+
lastEventAt: event.occurredAt,
|
|
80
|
+
lastEventId: event.eventId,
|
|
81
|
+
lastEventType: event.eventType,
|
|
82
|
+
lastQueueSize: length
|
|
83
|
+
});
|
|
72
84
|
if (this.config.collection?.enabled === false) {
|
|
85
|
+
appendRuntimeLog(this.config, "upload", {
|
|
86
|
+
action: "skip",
|
|
87
|
+
reason: "collection_disabled",
|
|
88
|
+
queueSize: length,
|
|
89
|
+
...eventLogEntry(event)
|
|
90
|
+
});
|
|
91
|
+
updateRuntimeState(this.config, {
|
|
92
|
+
lastUploadStatus: "skipped",
|
|
93
|
+
lastUploadError: "collection_disabled",
|
|
94
|
+
lastUploadRemaining: length,
|
|
95
|
+
lastQueueSize: length
|
|
96
|
+
});
|
|
73
97
|
return {
|
|
74
98
|
ok: true,
|
|
75
99
|
queued: length,
|
|
@@ -108,6 +132,22 @@ export class ClientAgent {
|
|
|
108
132
|
await this.queue.markFailed(failedItems.map((item) => item.id), "partial_upload_not_accepted");
|
|
109
133
|
}
|
|
110
134
|
const stats = await this.queue.stats();
|
|
135
|
+
appendRuntimeLog(this.config, "upload", {
|
|
136
|
+
action: "flush",
|
|
137
|
+
ok: failedItems.length === 0,
|
|
138
|
+
sent: accepted,
|
|
139
|
+
remaining: stats.total,
|
|
140
|
+
postponed: stats.waiting,
|
|
141
|
+
partial: failedItems.length > 0
|
|
142
|
+
});
|
|
143
|
+
updateRuntimeState(this.config, {
|
|
144
|
+
lastUploadAt: new Date().toISOString(),
|
|
145
|
+
lastUploadStatus: failedItems.length === 0 ? "ok" : "partial",
|
|
146
|
+
lastUploadError: failedItems.length ? "partial_upload_not_accepted" : "",
|
|
147
|
+
lastUploadSent: accepted,
|
|
148
|
+
lastUploadRemaining: stats.total,
|
|
149
|
+
lastQueueSize: stats.total
|
|
150
|
+
});
|
|
111
151
|
return {
|
|
112
152
|
ok: failedItems.length === 0,
|
|
113
153
|
sent: accepted,
|
|
@@ -120,6 +160,22 @@ export class ClientAgent {
|
|
|
120
160
|
} catch (error) {
|
|
121
161
|
await this.queue.markFailed(batch.map((item) => item.id), error);
|
|
122
162
|
const stats = await this.queue.stats();
|
|
163
|
+
appendRuntimeLog(this.config, "upload", {
|
|
164
|
+
action: "flush",
|
|
165
|
+
ok: false,
|
|
166
|
+
sent: 0,
|
|
167
|
+
remaining: stats.total,
|
|
168
|
+
postponed: stats.waiting,
|
|
169
|
+
error: error.message
|
|
170
|
+
});
|
|
171
|
+
updateRuntimeState(this.config, {
|
|
172
|
+
lastUploadAt: new Date().toISOString(),
|
|
173
|
+
lastUploadStatus: "failed",
|
|
174
|
+
lastUploadError: error.message,
|
|
175
|
+
lastUploadSent: 0,
|
|
176
|
+
lastUploadRemaining: stats.total,
|
|
177
|
+
lastQueueSize: stats.total
|
|
178
|
+
});
|
|
123
179
|
return {
|
|
124
180
|
ok: false,
|
|
125
181
|
sent: 0,
|
|
@@ -180,6 +236,7 @@ export class ClientAgent {
|
|
|
180
236
|
|
|
181
237
|
async checkin(overrides = {}) {
|
|
182
238
|
const queueItems = await this.queue.list();
|
|
239
|
+
const runtime = readRuntimeState(this.config);
|
|
183
240
|
return postJson("/api/clients/checkin", {
|
|
184
241
|
employeeId: this.config.employee.id,
|
|
185
242
|
employeeName: this.config.employee.name,
|
|
@@ -193,7 +250,15 @@ export class ClientAgent {
|
|
|
193
250
|
mcpReady: overrides.mcpReady ?? true,
|
|
194
251
|
hookReady: overrides.hookReady ?? true,
|
|
195
252
|
uploadQueueSize: queueItems.length,
|
|
196
|
-
issues: overrides.issues || []
|
|
253
|
+
issues: overrides.issues || [],
|
|
254
|
+
lastHookAt: overrides.lastHookAt ?? runtime.lastHookAt ?? "",
|
|
255
|
+
lastHookEventType: overrides.lastHookEventType ?? runtime.lastHookEventType ?? "",
|
|
256
|
+
lastHookEventId: overrides.lastHookEventId ?? runtime.lastHookEventId ?? "",
|
|
257
|
+
lastUploadAt: overrides.lastUploadAt ?? runtime.lastUploadAt ?? "",
|
|
258
|
+
lastUploadStatus: overrides.lastUploadStatus ?? runtime.lastUploadStatus ?? "",
|
|
259
|
+
lastUploadError: overrides.lastUploadError ?? runtime.lastUploadError ?? "",
|
|
260
|
+
lastUploadSent: overrides.lastUploadSent ?? runtime.lastUploadSent ?? 0,
|
|
261
|
+
lastUploadRemaining: overrides.lastUploadRemaining ?? runtime.lastUploadRemaining ?? queueItems.length
|
|
197
262
|
}, this.config);
|
|
198
263
|
}
|
|
199
264
|
|
|
@@ -355,8 +420,11 @@ export class ClientAgent {
|
|
|
355
420
|
clientId: this.config.clientId,
|
|
356
421
|
configFile: this.config.configFile,
|
|
357
422
|
queueFile: this.config.queueFile,
|
|
423
|
+
stateFile: this.config.stateFile,
|
|
424
|
+
logDir: this.config.logDir,
|
|
358
425
|
queueSize: queueItems.length,
|
|
359
426
|
queue: queueStats,
|
|
427
|
+
runtime: readRuntimeState(this.config),
|
|
360
428
|
collection: this.config.collection,
|
|
361
429
|
update: this.config.update
|
|
362
430
|
};
|