co-maintainer 0.4.0-beta.1

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.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,78 @@
1
+ import { basename, dirname } from "node:path";
2
+ import { isNotFound, isWindows, mkdir, readDir, remove, rename, stat, writeTextFile, } from "../util/runtime.js";
3
+ function tempName(dir, base) {
4
+ return `${dir}/.${base}.tmp-${crypto.randomUUID()}`;
5
+ }
6
+ /** Remove leftover `.<name>.tmp-*` files from an interrupted write.
7
+ * Only deletes temps older than `minAgeMs` so concurrent writers are not disturbed. */
8
+ export async function cleanStaleTempFiles(directory, baseName, minAgeMs = 60_000) {
9
+ const prefix = `.${baseName}.tmp-`;
10
+ const cutoff = Date.now() - minAgeMs;
11
+ try {
12
+ for await (const entry of readDir(directory)) {
13
+ if (!entry.isFile || !entry.name.startsWith(prefix))
14
+ continue;
15
+ const path = `${directory}/${entry.name}`;
16
+ const info = await stat(path);
17
+ const mtime = info.mtimeMs;
18
+ if (mtime > cutoff)
19
+ continue;
20
+ await remove(path).catch(() => { });
21
+ }
22
+ }
23
+ catch (error) {
24
+ if (!isNotFound(error))
25
+ throw error;
26
+ }
27
+ }
28
+ async function pathExists(path) {
29
+ try {
30
+ await stat(path);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /** Windows `rename` does not replace an existing file; swap via a backup
38
+ * name so a failed promote can restore the previous contents. */
39
+ async function promoteTempOnWindows(temp, path) {
40
+ if (!(await pathExists(path))) {
41
+ await rename(temp, path);
42
+ return;
43
+ }
44
+ const backup = `${path}.atomic-backup`;
45
+ await remove(backup).catch(() => { });
46
+ await rename(path, backup);
47
+ try {
48
+ await rename(temp, path);
49
+ }
50
+ catch (error) {
51
+ await rename(backup, path).catch(() => { });
52
+ throw error;
53
+ }
54
+ await remove(backup).catch(() => { });
55
+ }
56
+ /** Write then rename into place so readers never see a half-written file. */
57
+ async function recoverWindowsAtomicBackup(path) {
58
+ if (!isWindows())
59
+ return;
60
+ const backup = `${path}.atomic-backup`;
61
+ if (!(await pathExists(path)) && (await pathExists(backup))) {
62
+ await rename(backup, path);
63
+ }
64
+ }
65
+ export async function writeTextFileAtomic(path, text) {
66
+ await recoverWindowsAtomicBackup(path);
67
+ const dir = dirname(path);
68
+ await mkdir(dir, { recursive: true });
69
+ const base = basename(path);
70
+ const temp = tempName(dir, base);
71
+ await writeTextFile(temp, text);
72
+ if (isWindows()) {
73
+ await promoteTempOnWindows(temp, path);
74
+ }
75
+ else {
76
+ await rename(temp, path);
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ /** @internal For tests — number of keys with an active or queued lock. */
2
+ export declare function keyedLockActiveKeys(): number;
3
+ export declare function withKeyedLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,33 @@
1
+ const queues = new Map();
2
+ /** @internal For tests — number of keys with an active or queued lock. */
3
+ export function keyedLockActiveKeys() {
4
+ return queues.size;
5
+ }
6
+ export async function withKeyedLock(key, fn) {
7
+ let queue = queues.get(key);
8
+ if (!queue) {
9
+ queue = { running: false, waiters: [] };
10
+ queues.set(key, queue);
11
+ }
12
+ if (queue.running) {
13
+ await new Promise((resolve) => {
14
+ queue.waiters.push(resolve);
15
+ });
16
+ }
17
+ else {
18
+ queue.running = true;
19
+ }
20
+ try {
21
+ return await fn();
22
+ }
23
+ finally {
24
+ const current = queues.get(key);
25
+ if (current) {
26
+ const wake = current.waiters.shift();
27
+ if (wake)
28
+ wake();
29
+ else
30
+ queues.delete(key);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ /** Outside a job, `log()` just prints to the console. Inside one (wrapped
2
+ * with `withLogSink`), it also reaches `job_logs` and any live SSE
3
+ * subscriber, without threading a logger through every function signature. */
4
+ type Sink = (phase: string, message: string) => void;
5
+ /** Plan §8.4 — progress and logs on stderr during CLI review. */
6
+ export declare function withCliLogsToStderr<T>(fn: () => Promise<T>): Promise<T>;
7
+ export declare function withLogSink<T>(sink: Sink, fn: () => Promise<T>): Promise<T>;
8
+ export declare function log(phase: string, message: string): void;
9
+ export declare function startHeartbeat(phase: string | (() => string)): () => void;
10
+ export declare function timed<T>(label: string, enabled: boolean, operation: () => Promise<T>): Promise<T>;
11
+ export {};
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const sinkStorage = new AsyncLocalStorage();
3
+ let cliLogsToStderr = false;
4
+ /** Plan §8.4 — progress and logs on stderr during CLI review. */
5
+ export function withCliLogsToStderr(fn) {
6
+ const prev = cliLogsToStderr;
7
+ cliLogsToStderr = true;
8
+ return fn().finally(() => {
9
+ cliLogsToStderr = prev;
10
+ });
11
+ }
12
+ export function withLogSink(sink, fn) {
13
+ return sinkStorage.run(sink, fn);
14
+ }
15
+ export function log(phase, message) {
16
+ const sink = sinkStorage.getStore();
17
+ if (sink)
18
+ sink(phase, message);
19
+ else if (cliLogsToStderr)
20
+ console.error(`[${phase}] ${message}`);
21
+ else
22
+ console.log(`[${phase}] ${message}`);
23
+ }
24
+ export function startHeartbeat(phase) {
25
+ const started = Date.now();
26
+ const label = () => (typeof phase === "function" ? phase() : phase);
27
+ const timer = setInterval(() => {
28
+ log("progress", `still running ${label()} · ${Math.round((Date.now() - started) / 1000)}s elapsed`);
29
+ }, 15_000);
30
+ return () => clearInterval(timer);
31
+ }
32
+ export async function timed(label, enabled, operation) {
33
+ const started = performance.now();
34
+ try {
35
+ return await operation();
36
+ }
37
+ finally {
38
+ if (enabled) {
39
+ log("time", `${label} · ${((performance.now() - started) / 1000).toFixed(2)}s`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ export declare function redact(text: string): string;
2
+ export declare function outsideCode(text: string, clean: (prose: string) => string): string;
3
+ /** Dashboard and other human-readable copy. Secrets out, no dash, no
4
+ * semicolon. Code keeps its own punctuation or it stops compiling. */
5
+ export declare function safeCopy(text: string): string;
@@ -0,0 +1,30 @@
1
+ /** Matches known secret shapes, not a generic "long random string"
2
+ * heuristic — too many false positives on commit SHAs and hashes. */
3
+ const PATTERNS = [
4
+ /gh[pousr]_[A-Za-z0-9]{20,}/g, // ghp_, gho_, ghu_, ghs_, ghr_ (classic GitHub PATs and tokens)
5
+ /github_pat_[A-Za-z0-9_]{20,}/g, // fine-grained GitHub PATs
6
+ /sk-or-[A-Za-z0-9-]{10,}/g, // OpenRouter keys
7
+ /cmr_[A-Za-z0-9_-]{20,}/g, // remote review bearer tokens
8
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
9
+ ];
10
+ export function redact(text) {
11
+ let result = text;
12
+ for (const pattern of PATTERNS) {
13
+ result = result.replace(pattern, "[redacted]");
14
+ }
15
+ return result;
16
+ }
17
+ /** Fenced blocks and inline code spans. The capture group puts code on odd
18
+ * split indices, so prose can be cleaned without rewriting the code. */
19
+ const CODE = /(````[\s\S]*?````|```[\s\S]*?```|`[^`\n]*`)/;
20
+ export function outsideCode(text, clean) {
21
+ return text
22
+ .split(CODE)
23
+ .map((part, index) => (index % 2 === 1 ? part : clean(part)))
24
+ .join("");
25
+ }
26
+ /** Dashboard and other human-readable copy. Secrets out, no dash, no
27
+ * semicolon. Code keeps its own punctuation or it stops compiling. */
28
+ export function safeCopy(text) {
29
+ return outsideCode(redact(text), (prose) => prose.replaceAll("\u2014", ", ").replaceAll(";", "."));
30
+ }
@@ -0,0 +1,90 @@
1
+ import type { Stats } from "node:fs";
2
+ export declare function isWindows(): boolean;
3
+ export type Platform = "windows" | "darwin" | "linux";
4
+ /** The current OS in the vocabulary the config paths already used under Deno
5
+ * (`"windows"`, not Node's `"win32"`). */
6
+ export declare function currentPlatform(): Platform;
7
+ /** A missing path: Node's `ENOENT`, plus `ENOTDIR` (a parent is a file). */
8
+ export declare function isNotFound(error: unknown): boolean;
9
+ export declare function getEnv(name: string): string | undefined;
10
+ export declare function setEnv(name: string, value: string): void;
11
+ export declare function deleteEnv(name: string): void;
12
+ export declare function envToObject(): Record<string, string>;
13
+ export declare function readTextFileSync(path: string | URL): string;
14
+ export declare function writeTextFileSync(path: string | URL, text: string): void;
15
+ export declare function mkdirSync(path: string | URL, options?: {
16
+ recursive?: boolean;
17
+ }): void;
18
+ export declare function removeSync(path: string | URL, options?: {
19
+ recursive?: boolean;
20
+ }): void;
21
+ export declare function statSync(path: string | URL): Stats;
22
+ export declare function readTextFile(path: string | URL): Promise<string>;
23
+ export declare function readFile(path: string | URL): Promise<Uint8Array>;
24
+ export declare function writeTextFile(path: string | URL, text: string): Promise<void>;
25
+ export declare function writeFile(path: string | URL, data: Uint8Array | string): Promise<void>;
26
+ export declare function mkdir(path: string | URL, options?: {
27
+ recursive?: boolean;
28
+ }): Promise<void>;
29
+ export declare function remove(path: string | URL, options?: {
30
+ recursive?: boolean;
31
+ }): Promise<void>;
32
+ export declare function stat(path: string | URL): Promise<Stats>;
33
+ export declare function lstat(path: string | URL): Promise<Stats>;
34
+ export declare function readLink(path: string | URL): Promise<string>;
35
+ export declare function rename(from: string, to: string): Promise<void>;
36
+ export declare function chmod(path: string, mode: number): Promise<void>;
37
+ export declare function makeTempDir(options?: {
38
+ prefix?: string;
39
+ }): Promise<string>;
40
+ export declare function makeTempDirSync(options?: {
41
+ prefix?: string;
42
+ }): string;
43
+ /** A uniquely named temp file (created empty), mirroring `Deno.makeTempFile`. */
44
+ export declare function makeTempFile(options?: {
45
+ prefix?: string;
46
+ suffix?: string;
47
+ }): Promise<string>;
48
+ export type DirEntry = {
49
+ name: string;
50
+ isFile: boolean;
51
+ isDirectory: boolean;
52
+ isSymlink: boolean;
53
+ };
54
+ export declare function readDir(dir: string | URL): AsyncGenerator<DirEntry>;
55
+ export type CommandOptions = {
56
+ args?: string[];
57
+ cwd?: string;
58
+ env?: Record<string, string | undefined>;
59
+ stdin?: "piped";
60
+ stdout?: "piped";
61
+ stderr?: "piped";
62
+ /** Windows runs `.cmd`/shell builtins only through a shell. */
63
+ shell?: boolean;
64
+ };
65
+ export type CommandOutput = {
66
+ success: boolean;
67
+ code: number;
68
+ stdout: Uint8Array;
69
+ stderr: Uint8Array;
70
+ };
71
+ /** `Deno.Command(...).output()` equivalent: run to completion, capture both
72
+ * streams, never throw on a non-zero exit. */
73
+ export declare function commandOutput(command: string, options?: CommandOptions): Promise<CommandOutput>;
74
+ /** `Deno.Command(...).spawn()` then write to stdin: used by the `gh` client
75
+ * to pass a JSON body with `--input -`. */
76
+ export declare function commandWithInput(command: string, options: CommandOptions, input: string): Promise<CommandOutput>;
77
+ /** A spawned process that can be killed on timeout, mirroring
78
+ * `Deno.Command(...).spawn()` + `proc.kill()`. */
79
+ export declare function commandSpawn(command: string, options?: CommandOptions): {
80
+ kill: (signal?: NodeJS.Signals) => void;
81
+ output: () => Promise<CommandOutput>;
82
+ };
83
+ /** Whether `pid` names a live process.
84
+ *
85
+ * Windows: `process.kill(pid, 0)` is unreliable (Node emulates signals and
86
+ * would terminate the target), so `tasklist` answers without touching it.
87
+ * POSIX: signal 0 checks existence without delivering; `ESRCH` means dead,
88
+ * while `EPERM` (or anything else) means it exists but is not ours — assume
89
+ * alive rather than risk two writers on the same database. */
90
+ export declare function isProcessAlive(pid: number): boolean;
@@ -0,0 +1,206 @@
1
+ /** Production runtime helpers for the Node migration.
2
+ *
3
+ * These deliberately mirror the Deno API names and shapes the codebase was
4
+ * written against, so the migration is a mechanical `Deno.foo(` → `foo(`
5
+ * substitution and every deviation lives in exactly one file. Centralizes the
6
+ * two silent-break classes too: `"windows"` vs `"win32"`, and
7
+ * `Deno.errors.NotFound` vs `ENOENT`.
8
+ */
9
+ import { chmod as fsChmod, lstat as fsLstat, mkdir as fsMkdir, mkdtemp as fsMkdtemp, readFile as fsReadFile, readdir, readlink as fsReadLink, rename as fsRename, rm as fsRm, stat as fsStat, writeFile as fsWriteFile, } from "node:fs/promises";
10
+ import { mkdirSync as fsMkdirSync, mkdtempSync as fsMkdtempSync, readFileSync as fsReadFileSync, rmSync as fsRmSync, statSync as fsStatSync, writeFileSync as fsWriteFileSync, } from "node:fs";
11
+ import { spawn, spawnSync } from "node:child_process";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ export function isWindows() {
15
+ return process.platform === "win32";
16
+ }
17
+ /** The current OS in the vocabulary the config paths already used under Deno
18
+ * (`"windows"`, not Node's `"win32"`). */
19
+ export function currentPlatform() {
20
+ return isWindows()
21
+ ? "windows"
22
+ : process.platform === "darwin"
23
+ ? "darwin"
24
+ : "linux";
25
+ }
26
+ /** A missing path: Node's `ENOENT`, plus `ENOTDIR` (a parent is a file). */
27
+ export function isNotFound(error) {
28
+ const code = error?.code;
29
+ return code === "ENOENT" || code === "ENOTDIR";
30
+ }
31
+ // ---------------------------------------------------------------- environment
32
+ export function getEnv(name) {
33
+ return process.env[name];
34
+ }
35
+ export function setEnv(name, value) {
36
+ process.env[name] = value;
37
+ }
38
+ export function deleteEnv(name) {
39
+ delete process.env[name];
40
+ }
41
+ export function envToObject() {
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(process.env)) {
44
+ if (value !== undefined)
45
+ out[key] = value;
46
+ }
47
+ return out;
48
+ }
49
+ // ------------------------------------------------------------------- sync fs
50
+ export function readTextFileSync(path) {
51
+ return fsReadFileSync(path, "utf8");
52
+ }
53
+ export function writeTextFileSync(path, text) {
54
+ fsWriteFileSync(path, text);
55
+ }
56
+ export function mkdirSync(path, options) {
57
+ fsMkdirSync(path, { recursive: options?.recursive ?? false });
58
+ }
59
+ export function removeSync(path, options) {
60
+ fsRmSync(path, { recursive: options?.recursive ?? false, force: false });
61
+ }
62
+ export function statSync(path) {
63
+ return fsStatSync(path);
64
+ }
65
+ // ------------------------------------------------------------------ async fs
66
+ export function readTextFile(path) {
67
+ return fsReadFile(path, "utf8");
68
+ }
69
+ export function readFile(path) {
70
+ return fsReadFile(path);
71
+ }
72
+ export function writeTextFile(path, text) {
73
+ return fsWriteFile(path, text);
74
+ }
75
+ export function writeFile(path, data) {
76
+ return fsWriteFile(path, data);
77
+ }
78
+ export function mkdir(path, options) {
79
+ return fsMkdir(path, { recursive: options?.recursive ?? false }).then(() => { });
80
+ }
81
+ export function remove(path, options) {
82
+ return fsRm(path, { recursive: options?.recursive ?? false, force: false });
83
+ }
84
+ export function stat(path) {
85
+ return fsStat(path);
86
+ }
87
+ export function lstat(path) {
88
+ return fsLstat(path);
89
+ }
90
+ export function readLink(path) {
91
+ return fsReadLink(path);
92
+ }
93
+ export function rename(from, to) {
94
+ return fsRename(from, to);
95
+ }
96
+ export function chmod(path, mode) {
97
+ return fsChmod(path, mode);
98
+ }
99
+ export function makeTempDir(options) {
100
+ return fsMkdtemp(join(tmpdir(), options?.prefix ?? "cm-"));
101
+ }
102
+ export function makeTempDirSync(options) {
103
+ return fsMkdtempSync(join(tmpdir(), options?.prefix ?? "cm-"));
104
+ }
105
+ /** A uniquely named temp file (created empty), mirroring `Deno.makeTempFile`. */
106
+ export async function makeTempFile(options) {
107
+ const dir = await fsMkdtemp(join(tmpdir(), options?.prefix ?? "cm-"));
108
+ const name = `${crypto.randomUUID()}${options?.suffix ?? ""}`;
109
+ const path = join(dir, name);
110
+ await fsWriteFile(path, "");
111
+ return path;
112
+ }
113
+ export async function* readDir(dir) {
114
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
115
+ yield {
116
+ name: entry.name,
117
+ isFile: entry.isFile(),
118
+ isDirectory: entry.isDirectory(),
119
+ isSymlink: entry.isSymbolicLink(),
120
+ };
121
+ }
122
+ }
123
+ function baseSpawnOptions(options) {
124
+ return {
125
+ cwd: options.cwd,
126
+ env: options.env,
127
+ shell: options.shell ?? false,
128
+ };
129
+ }
130
+ function collect(child) {
131
+ return new Promise((resolve, reject) => {
132
+ const stdout = [];
133
+ const stderr = [];
134
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
135
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
136
+ child.on("error", reject);
137
+ child.on("close", (code) => {
138
+ resolve({
139
+ success: code === 0,
140
+ code: code ?? 1,
141
+ stdout: new Uint8Array(Buffer.concat(stdout)),
142
+ stderr: new Uint8Array(Buffer.concat(stderr)),
143
+ });
144
+ });
145
+ });
146
+ }
147
+ /** `Deno.Command(...).output()` equivalent: run to completion, capture both
148
+ * streams, never throw on a non-zero exit. */
149
+ export function commandOutput(command, options = {}) {
150
+ const child = spawn(command, options.args ?? [], {
151
+ ...baseSpawnOptions(options),
152
+ stdio: ["ignore", "pipe", "pipe"],
153
+ });
154
+ return collect(child);
155
+ }
156
+ /** `Deno.Command(...).spawn()` then write to stdin: used by the `gh` client
157
+ * to pass a JSON body with `--input -`. */
158
+ export function commandWithInput(command, options, input) {
159
+ const child = spawn(command, options.args ?? [], {
160
+ ...baseSpawnOptions(options),
161
+ stdio: ["pipe", "pipe", "pipe"],
162
+ });
163
+ child.stdin?.end(input);
164
+ return collect(child);
165
+ }
166
+ /** A spawned process that can be killed on timeout, mirroring
167
+ * `Deno.Command(...).spawn()` + `proc.kill()`. */
168
+ export function commandSpawn(command, options = {}) {
169
+ const child = spawn(command, options.args ?? [], {
170
+ ...baseSpawnOptions(options),
171
+ stdio: [options.stdin === "piped" ? "pipe" : "ignore", "pipe", "pipe"],
172
+ });
173
+ return {
174
+ kill: (signal = "SIGTERM") => {
175
+ try {
176
+ child.kill(signal);
177
+ }
178
+ catch {
179
+ // already exited
180
+ }
181
+ },
182
+ output: () => collect(child),
183
+ };
184
+ }
185
+ /** Whether `pid` names a live process.
186
+ *
187
+ * Windows: `process.kill(pid, 0)` is unreliable (Node emulates signals and
188
+ * would terminate the target), so `tasklist` answers without touching it.
189
+ * POSIX: signal 0 checks existence without delivering; `ESRCH` means dead,
190
+ * while `EPERM` (or anything else) means it exists but is not ours — assume
191
+ * alive rather than risk two writers on the same database. */
192
+ export function isProcessAlive(pid) {
193
+ if (isWindows()) {
194
+ const result = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
195
+ encoding: "utf8",
196
+ });
197
+ return (result.stdout ?? "").trim().startsWith('"');
198
+ }
199
+ try {
200
+ process.kill(pid, 0);
201
+ return true;
202
+ }
203
+ catch (error) {
204
+ return error.code !== "ESRCH";
205
+ }
206
+ }
@@ -0,0 +1,9 @@
1
+ /** The one place every stored timestamp comes from. PLAN.md Section 3's
2
+ * "every timestamp is ISO8601 UTC" rule lives here instead of as a
3
+ * convention repeated at each call site — `store/*` never calls
4
+ * `new Date().toISOString()` directly. */
5
+ export declare function nowIso(): string;
6
+ /** Short relative label for dashboard tables. Never emits a dash or
7
+ * semicolon. */
8
+ export declare function relativeTime(iso: string, nowMs?: number): string;
9
+ export declare function daysAgoIso(days: number, nowMs?: number): string;
@@ -0,0 +1,36 @@
1
+ /** The one place every stored timestamp comes from. PLAN.md Section 3's
2
+ * "every timestamp is ISO8601 UTC" rule lives here instead of as a
3
+ * convention repeated at each call site — `store/*` never calls
4
+ * `new Date().toISOString()` directly. */
5
+ export function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+ /** Short relative label for dashboard tables. Never emits a dash or
9
+ * semicolon. */
10
+ export function relativeTime(iso, nowMs = Date.now()) {
11
+ const then = Date.parse(iso);
12
+ if (!Number.isFinite(then))
13
+ return "";
14
+ const sec = Math.max(0, Math.round((nowMs - then) / 1000));
15
+ if (sec < 60)
16
+ return "just now";
17
+ const min = Math.round(sec / 60);
18
+ if (min < 60)
19
+ return min === 1 ? "1 minute ago" : `${min} minutes ago`;
20
+ const hour = Math.round(min / 60);
21
+ if (hour < 24)
22
+ return hour === 1 ? "1 hour ago" : `${hour} hours ago`;
23
+ const day = Math.round(hour / 24);
24
+ if (day === 1)
25
+ return "yesterday";
26
+ if (day < 30)
27
+ return `${day} days ago`;
28
+ const month = Math.round(day / 30);
29
+ if (month < 12)
30
+ return month === 1 ? "1 month ago" : `${month} months ago`;
31
+ const year = Math.round(day / 365);
32
+ return year === 1 ? "1 year ago" : `${year} years ago`;
33
+ }
34
+ export function daysAgoIso(days, nowMs = Date.now()) {
35
+ return new Date(nowMs - days * 24 * 60 * 60 * 1000).toISOString();
36
+ }
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,5 @@
1
+ /** The single source of truth for the released version: `package.json`.
2
+ * Import attributes work identically under Node 24 and Deno 2, and reading
3
+ * it once here keeps the six call sites independent of the file layout. */
4
+ import pkg from "../package.json" with { type: "json" };
5
+ export const VERSION = pkg.version;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "co-maintainer",
3
+ "version": "0.4.0-beta.1",
4
+ "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "co-maintainer": "./dist/main.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "LICENSE",
12
+ "README.md"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": "./dist/mod.js"
17
+ },
18
+ "scripts": {
19
+ "build": "npm run assets:embed && tsc -p tsconfig.build.json",
20
+ "assets:embed": "node scripts/embed_assets.mjs",
21
+ "prepack": "npm run build",
22
+ "test": "node --test \"src/**/*.test.ts\" \"benchmark/**/*.test.ts\"",
23
+ "check": "tsc --noEmit",
24
+ "lint": "oxlint",
25
+ "fmt": "oxfmt --write .",
26
+ "fmt:check": "oxfmt --check .",
27
+ "dev": "node --watch main.ts",
28
+ "probe": "node main.ts probe",
29
+ "init": "node main.ts init",
30
+ "remake": "node main.ts remake",
31
+ "review": "node main.ts review",
32
+ "serve": "node main.ts serve",
33
+ "bench": "node benchmark/core_v2/run.ts",
34
+ "bench-rereview": "node benchmark/core_v2/run_rereview.ts",
35
+ "bench-swe-prbench": "node benchmark/swe-prbench/run.ts",
36
+ "bench-core-v3": "node benchmark/core_v3/run.ts",
37
+ "swe-prbench-prepare": "node benchmark/swe-prbench/prepare_dataset.ts",
38
+ "e2e": "node scripts/e2e.ts",
39
+ "review-local-e2e": "node scripts/review_local_e2e.ts",
40
+ "docs:build": "node scripts/build_docs.ts"
41
+ },
42
+ "dependencies": {
43
+ "marked": "^15.0.7"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.0.0",
47
+ "oxfmt": "^0.68.0",
48
+ "oxlint": "^1.83.0",
49
+ "typescript": "^5.7.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=24"
53
+ }
54
+ }