ronds_ai 0.1.17 → 0.1.19

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 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,"suppressOutput":true,"hookSpecificOutput":{"hookEventName":"PostToolUse"}}
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
 
@@ -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
- suppressOutput: true,
23
- hookSpecificOutput: {
24
- hookEventName: CODEX_SUPPORTED_HOOK_EVENT,
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,
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const commentJson = require('comment-json');
4
5
 
5
6
  const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
6
7
  const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
@@ -42,7 +43,10 @@ function readJsonFile(filePath, fallback) {
42
43
 
43
44
  let data;
44
45
  try {
45
- data = JSON.parse(raw);
46
+ // comment-json 容忍 JSONC 注释和尾随逗号。~/.claude/settings.json 等
47
+ // Claude/Cursor 配置文件官方支持 JSONC,必须容忍用户加的注释。
48
+ // 注意:写回时仍用 JSON.stringify,原文件中的注释会被丢弃。
49
+ data = commentJson.parse(raw);
46
50
  } catch (error) {
47
51
  const detail = error instanceof Error ? error.message : String(error);
48
52
  throw new Error(`Invalid JSON in ${filePath}: ${detail}`);
@@ -424,19 +428,19 @@ function generateRondsHookBlocks(newline) {
424
428
  ].join(newline);
425
429
  }
426
430
 
427
- function ensureCodexRequirementsToml(reqPath) {
428
- const exists = fs.existsSync(reqPath);
429
- let content = exists ? fs.readFileSync(reqPath, 'utf8') : '';
430
- if (content.charCodeAt(0) === 0xFEFF) {
431
- content = content.slice(1);
432
- }
433
- const newline = content.includes('\r\n') ? '\r\n' : '\n';
431
+ function ensureCodexRequirementsToml(reqPath) {
432
+ const exists = fs.existsSync(reqPath);
433
+ let content = exists ? fs.readFileSync(reqPath, 'utf8') : '';
434
+ if (content.charCodeAt(0) === 0xFEFF) {
435
+ content = content.slice(1);
436
+ }
437
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
434
438
 
435
439
  content = ensureCodexHooksFeatureFlag(content);
436
440
  content = ensureCodexManagedDir(content, reqPath);
437
441
  content = removeRondsBlocksFromToml(content);
438
- content = content.replace(/[\t ]*\r?\n*$/, newline);
439
- content += generateRondsHookBlocks(newline);
442
+ content = content.replace(/[\t ]*\r?\n*$/, newline);
443
+ content += generateRondsHookBlocks(newline);
440
444
 
441
445
  const changed = writeTextFileIfChanged(reqPath, content);
442
446
  return { exists, changed };
@@ -527,16 +531,16 @@ function cleanupClaudeLocalSettings(localCandidates, result) {
527
531
  }
528
532
  }
529
533
 
530
- // 将系统级 Codex requirements.toml 的权限错误转换成面向用户的部署提示。
531
- function wrapCodexPermissionError(error, codexRequirementsPath) {
532
- const code = isPlainObject(error) ? error.code : '';
534
+ // 将系统级 Codex requirements.toml 的权限错误转换成面向用户的部署提示。
535
+ function wrapCodexPermissionError(error, codexRequirementsPath) {
536
+ const code = isPlainObject(error) ? error.code : '';
533
537
  if (code !== 'EACCES' && code !== 'EPERM') {
534
538
  return error;
535
539
  }
536
540
 
537
- const wrapped = new Error(
538
- `Codex 部署需要管理员/root 权限,无法写入 ${codexRequirementsPath}。请以管理员身份重新运行 hooks deploy --scope user。`,
539
- );
541
+ const wrapped = new Error(
542
+ `Codex 部署需要管理员/root 权限,无法写入 ${codexRequirementsPath}。请以管理员身份重新运行 hooks deploy --scope user。`,
543
+ );
540
544
  wrapped.cause = error;
541
545
  wrapped.code = code;
542
546
  return wrapped;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds_ai",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for reporting AI code edit events.",
5
5
  "bin": {
6
6
  "ronds_ai": "bin/ronds_ai.js"
@@ -12,5 +12,8 @@
12
12
  "engines": {
13
13
  "node": ">=16"
14
14
  },
15
+ "dependencies": {
16
+ "comment-json": "^4.2.5"
17
+ },
15
18
  "license": "MIT"
16
19
  }