ronds_ai 0.1.17 → 0.1.18
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 +14 -14
- package/lib/code_record.js +147 -148
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,12 +121,12 @@ npx ronds_ai@latest record cursor
|
|
|
121
121
|
|
|
122
122
|
行为说明:
|
|
123
123
|
|
|
124
|
-
- 从 `stdin` 读取一段 JSON
|
|
125
|
-
- 根据来源提取文件路径、变更内容、增删行数、仓库信息、worker_id
|
|
126
|
-
- 组装事件后通过 HTTP POST 上报
|
|
127
|
-
- Claude / Cursor 成功时输出一行 JSON
|
|
128
|
-
- Claude / Cursor 失败时会把事件写入本地失败日志,并输出保存位置
|
|
129
|
-
- Codex 作为 `PostToolUse` hook 调用时,标准输出固定为 Codex hook command output schema,避免触发 `invalid post-tool-use JSON output`
|
|
124
|
+
- 从 `stdin` 读取一段 JSON
|
|
125
|
+
- 根据来源提取文件路径、变更内容、增删行数、仓库信息、worker_id
|
|
126
|
+
- 组装事件后通过 HTTP POST 上报
|
|
127
|
+
- Claude / Cursor 成功时输出一行 JSON
|
|
128
|
+
- Claude / Cursor 失败时会把事件写入本地失败日志,并输出保存位置
|
|
129
|
+
- Codex 作为 `PostToolUse` hook 调用时,标准输出固定为 Codex hook command output schema,避免触发 `invalid post-tool-use JSON output`
|
|
130
130
|
|
|
131
131
|
成功输出示例:
|
|
132
132
|
|
|
@@ -158,13 +158,13 @@ npx ronds_ai@latest record cursor
|
|
|
158
158
|
- `Edit`
|
|
159
159
|
- `MultiEdit`
|
|
160
160
|
|
|
161
|
-
Codex 使用 `PostToolUse` hook,在 `apply_patch` 执行后直接解析本次补丁内容并生成变更事件;失败事件同样写入 `~/.ronds_ai/failed-events`。
|
|
162
|
-
|
|
163
|
-
Codex 输出示例:
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{"continue":true,"
|
|
167
|
-
```
|
|
161
|
+
Codex 使用 `PostToolUse` hook,在 `apply_patch` 执行后直接解析本次补丁内容并生成变更事件;失败事件同样写入 `~/.ronds_ai/failed-events`。
|
|
162
|
+
|
|
163
|
+
Codex 输出示例:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{"continue":true,"hookSpecificOutput":{"hookEventName":"PostToolUse"}}
|
|
167
|
+
```
|
|
168
168
|
|
|
169
169
|
### `doctor`
|
|
170
170
|
|
|
@@ -303,7 +303,7 @@ npx ronds_ai@latest skills install demo-skill --tool codex,cursor --scope projec
|
|
|
303
303
|
- `HOOK_REPORT_TIMEOUT_MS`: 请求超时时间,默认 `10000`
|
|
304
304
|
- `HOOK_REPORT_TOKEN`: 如果设置,会以 `Authorization: Bearer <token>` 发送
|
|
305
305
|
- `HOOK_REPORT_HEADERS`: 额外请求头,要求是 JSON 字符串
|
|
306
|
-
- `HOOK_REQUEST_DRY_RUN=1`: 不发请求。Claude / Cursor 会把事件打印到标准输出;Codex 仍输出合法的 PostToolUse hook command output schema
|
|
306
|
+
- `HOOK_REQUEST_DRY_RUN=1`: 不发请求。Claude / Cursor 会把事件打印到标准输出;Codex 仍输出合法的 PostToolUse hook command output schema
|
|
307
307
|
|
|
308
308
|
默认上报地址:
|
|
309
309
|
|
package/lib/code_record.js
CHANGED
|
@@ -11,20 +11,19 @@ const MAX_ADDED_CONTENT_CHARS = 20000;
|
|
|
11
11
|
const DEFAULT_HOOK_REPORT_URL = 'https://aihub.ronds.com/api/api/v1/ai-code-events';
|
|
12
12
|
const CLAUDE_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
13
13
|
const CLAUDE_SUPPORTED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
14
|
-
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
15
|
-
const CODEX_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
16
|
-
const CODEX_SUPPORTED_TOOL = 'apply_patch';
|
|
17
|
-
|
|
18
|
-
// 构造 Codex PostToolUse hook 要求的标准输出,避免 stdout 出现 schema 之外的字段。
|
|
19
|
-
function buildCodexPostToolUseOutput() {
|
|
20
|
-
return {
|
|
21
|
-
continue: true,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
14
|
+
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
15
|
+
const CODEX_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
16
|
+
const CODEX_SUPPORTED_TOOL = 'apply_patch';
|
|
17
|
+
|
|
18
|
+
// 构造 Codex PostToolUse hook 要求的标准输出,避免 stdout 出现 schema 之外的字段。
|
|
19
|
+
function buildCodexPostToolUseOutput() {
|
|
20
|
+
return {
|
|
21
|
+
continue: true,
|
|
22
|
+
hookSpecificOutput: {
|
|
23
|
+
hookEventName: CODEX_SUPPORTED_HOOK_EVENT,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
28
27
|
|
|
29
28
|
function readStdin() {
|
|
30
29
|
return new Promise((resolve, reject) => {
|
|
@@ -433,119 +432,119 @@ function buildCursorEvent(payload, source) {
|
|
|
433
432
|
};
|
|
434
433
|
}
|
|
435
434
|
|
|
436
|
-
// 从 Codex PostToolUse 的 apply_patch 命令中提取补丁正文。
|
|
437
|
-
function extractApplyPatchText(command) {
|
|
438
|
-
const rawCommand = String(command || '').replace(/\r\n/g, '\n');
|
|
439
|
-
const beginIndex = rawCommand.indexOf('*** Begin Patch');
|
|
440
|
-
const endIndex = rawCommand.indexOf('*** End Patch', beginIndex);
|
|
441
|
-
if (beginIndex === -1 || endIndex === -1) {
|
|
442
|
-
return '';
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return rawCommand.slice(beginIndex, endIndex + '*** End Patch'.length);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// 解析 apply_patch 文件头,返回变更类型和目标路径。
|
|
449
|
-
function parseApplyPatchHeader(line) {
|
|
450
|
-
const match = /^\*\*\* (Add|Update|Delete) File: (.+)$/.exec(line);
|
|
451
|
-
if (!match) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return {
|
|
456
|
-
action: match[1],
|
|
457
|
-
path: match[2].trim(),
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// 按文件聚合 apply_patch 中的新增/删除行,生成后续上报需要的变更摘要。
|
|
462
|
-
function parseApplyPatchFiles(patchText) {
|
|
463
|
-
const lines = String(patchText || '').replace(/\r\n/g, '\n').split('\n');
|
|
464
|
-
const files = [];
|
|
465
|
-
let current = null;
|
|
466
|
-
|
|
467
|
-
for (const line of lines) {
|
|
468
|
-
const header = parseApplyPatchHeader(line);
|
|
469
|
-
if (header) {
|
|
470
|
-
current = {
|
|
471
|
-
path: header.path,
|
|
472
|
-
action: header.action,
|
|
473
|
-
movedTo: '',
|
|
474
|
-
addedLines: [],
|
|
475
|
-
added: 0,
|
|
476
|
-
removed: 0,
|
|
477
|
-
};
|
|
478
|
-
files.push(current);
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!current) {
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (line.startsWith('*** Move to: ')) {
|
|
487
|
-
current.movedTo = line.slice('*** Move to: '.length).trim();
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (line.startsWith('+')) {
|
|
496
|
-
const addedLine = line.slice(1);
|
|
497
|
-
if (addedLine !== '') {
|
|
498
|
-
current.addedLines.push(addedLine);
|
|
499
|
-
current.added += 1;
|
|
500
|
-
}
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (line.startsWith('-')) {
|
|
505
|
-
if (line.slice(1) !== '') {
|
|
506
|
-
current.removed += 1;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return files;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// 将 Codex apply_patch 的文件级变更转换为统一代码事件。
|
|
515
|
-
function buildCodexApplyPatchEvents(payload, source, patchFiles) {
|
|
516
|
-
const cwd = payload.cwd || process.cwd();
|
|
517
|
-
const events = [];
|
|
518
|
-
|
|
519
|
-
for (const fileChange of patchFiles) {
|
|
520
|
-
const eventPath = fileChange.movedTo || fileChange.path;
|
|
521
|
-
const absoluteFilePath = path.resolve(cwd, eventPath);
|
|
522
|
-
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
523
|
-
const workerId = resolveWorkerId(repoRoot);
|
|
524
|
-
let addedContent = fileChange.addedLines.join('\n');
|
|
525
|
-
if (addedContent.length > MAX_ADDED_CONTENT_CHARS) {
|
|
526
|
-
addedContent = `${addedContent.slice(0, MAX_ADDED_CONTENT_CHARS)}\n... (truncated)`;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
events.push({
|
|
530
|
-
event_id: randomUUID(),
|
|
531
|
-
timestamp: new Date().toISOString(),
|
|
532
|
-
source,
|
|
533
|
-
worker_id: workerId,
|
|
534
|
-
git,
|
|
535
|
-
file: {
|
|
536
|
-
path: repoRelativePath || eventPath.replace(/\\/g, '/'),
|
|
537
|
-
operation: fileChange.action === 'Add' ? 'write' : 'edit',
|
|
538
|
-
},
|
|
539
|
-
changes: {
|
|
540
|
-
added_content: addedContent,
|
|
541
|
-
added: fileChange.added,
|
|
542
|
-
removed: fileChange.removed,
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return events;
|
|
548
|
-
}
|
|
435
|
+
// 从 Codex PostToolUse 的 apply_patch 命令中提取补丁正文。
|
|
436
|
+
function extractApplyPatchText(command) {
|
|
437
|
+
const rawCommand = String(command || '').replace(/\r\n/g, '\n');
|
|
438
|
+
const beginIndex = rawCommand.indexOf('*** Begin Patch');
|
|
439
|
+
const endIndex = rawCommand.indexOf('*** End Patch', beginIndex);
|
|
440
|
+
if (beginIndex === -1 || endIndex === -1) {
|
|
441
|
+
return '';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return rawCommand.slice(beginIndex, endIndex + '*** End Patch'.length);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 解析 apply_patch 文件头,返回变更类型和目标路径。
|
|
448
|
+
function parseApplyPatchHeader(line) {
|
|
449
|
+
const match = /^\*\*\* (Add|Update|Delete) File: (.+)$/.exec(line);
|
|
450
|
+
if (!match) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
action: match[1],
|
|
456
|
+
path: match[2].trim(),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 按文件聚合 apply_patch 中的新增/删除行,生成后续上报需要的变更摘要。
|
|
461
|
+
function parseApplyPatchFiles(patchText) {
|
|
462
|
+
const lines = String(patchText || '').replace(/\r\n/g, '\n').split('\n');
|
|
463
|
+
const files = [];
|
|
464
|
+
let current = null;
|
|
465
|
+
|
|
466
|
+
for (const line of lines) {
|
|
467
|
+
const header = parseApplyPatchHeader(line);
|
|
468
|
+
if (header) {
|
|
469
|
+
current = {
|
|
470
|
+
path: header.path,
|
|
471
|
+
action: header.action,
|
|
472
|
+
movedTo: '',
|
|
473
|
+
addedLines: [],
|
|
474
|
+
added: 0,
|
|
475
|
+
removed: 0,
|
|
476
|
+
};
|
|
477
|
+
files.push(current);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!current) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (line.startsWith('*** Move to: ')) {
|
|
486
|
+
current.movedTo = line.slice('*** Move to: '.length).trim();
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (line.startsWith('+')) {
|
|
495
|
+
const addedLine = line.slice(1);
|
|
496
|
+
if (addedLine !== '') {
|
|
497
|
+
current.addedLines.push(addedLine);
|
|
498
|
+
current.added += 1;
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (line.startsWith('-')) {
|
|
504
|
+
if (line.slice(1) !== '') {
|
|
505
|
+
current.removed += 1;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return files;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 将 Codex apply_patch 的文件级变更转换为统一代码事件。
|
|
514
|
+
function buildCodexApplyPatchEvents(payload, source, patchFiles) {
|
|
515
|
+
const cwd = payload.cwd || process.cwd();
|
|
516
|
+
const events = [];
|
|
517
|
+
|
|
518
|
+
for (const fileChange of patchFiles) {
|
|
519
|
+
const eventPath = fileChange.movedTo || fileChange.path;
|
|
520
|
+
const absoluteFilePath = path.resolve(cwd, eventPath);
|
|
521
|
+
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
522
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
523
|
+
let addedContent = fileChange.addedLines.join('\n');
|
|
524
|
+
if (addedContent.length > MAX_ADDED_CONTENT_CHARS) {
|
|
525
|
+
addedContent = `${addedContent.slice(0, MAX_ADDED_CONTENT_CHARS)}\n... (truncated)`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
events.push({
|
|
529
|
+
event_id: randomUUID(),
|
|
530
|
+
timestamp: new Date().toISOString(),
|
|
531
|
+
source,
|
|
532
|
+
worker_id: workerId,
|
|
533
|
+
git,
|
|
534
|
+
file: {
|
|
535
|
+
path: repoRelativePath || eventPath.replace(/\\/g, '/'),
|
|
536
|
+
operation: fileChange.action === 'Add' ? 'write' : 'edit',
|
|
537
|
+
},
|
|
538
|
+
changes: {
|
|
539
|
+
added_content: addedContent,
|
|
540
|
+
added: fileChange.added,
|
|
541
|
+
removed: fileChange.removed,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return events;
|
|
547
|
+
}
|
|
549
548
|
|
|
550
549
|
function buildCodexEvents(payload, source) {
|
|
551
550
|
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
@@ -636,15 +635,15 @@ function saveCliError(error, context = {}) {
|
|
|
636
635
|
return appendFailedRecord('cli', record, timestamp);
|
|
637
636
|
}
|
|
638
637
|
|
|
639
|
-
function sendJsonRequest(event, config) {
|
|
640
|
-
if (process.env.HOOK_REQUEST_DRY_RUN === '1') {
|
|
641
|
-
return Promise.resolve({
|
|
642
|
-
statusCode: 0,
|
|
643
|
-
body: 'dry-run',
|
|
644
|
-
dryRun: true,
|
|
645
|
-
event,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
638
|
+
function sendJsonRequest(event, config) {
|
|
639
|
+
if (process.env.HOOK_REQUEST_DRY_RUN === '1') {
|
|
640
|
+
return Promise.resolve({
|
|
641
|
+
statusCode: 0,
|
|
642
|
+
body: 'dry-run',
|
|
643
|
+
dryRun: true,
|
|
644
|
+
event,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
648
647
|
|
|
649
648
|
if (!config.url) {
|
|
650
649
|
return Promise.reject(new Error('Missing request URL'));
|
|
@@ -759,18 +758,18 @@ async function runCodeRecord(source) {
|
|
|
759
758
|
}
|
|
760
759
|
}
|
|
761
760
|
|
|
762
|
-
for (const result of results) {
|
|
763
|
-
if (source === 'codex') {
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (source === 'codex') {
|
|
771
|
-
process.stdout.write(`${JSON.stringify(buildCodexPostToolUseOutput())}\n`);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
761
|
+
for (const result of results) {
|
|
762
|
+
if (source === 'codex') {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (source === 'codex') {
|
|
770
|
+
process.stdout.write(`${JSON.stringify(buildCodexPostToolUseOutput())}\n`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
774
773
|
|
|
775
774
|
module.exports = {
|
|
776
775
|
runCodeRecord,
|