@helpfeel/cosense-cli 1.17.0 → 1.17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helpfeel/cosense-cli",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "Cosense (旧Scrapbox) のページを読み・調べ・編集するAgent Skillのハーネス",
5
5
  "homepage": "https://github.com/helpfeel/cosense-cli",
6
6
  "license": "MIT",
@@ -27,10 +27,10 @@
27
27
  "tsx": "4.23.15"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "24.19.1",
30
+ "@types/node": "24.19.2",
31
31
  "npm-run-all": "4.1.5",
32
- "oxfmt": "0.71.0",
33
- "oxlint": "1.86.0",
32
+ "oxfmt": "0.72.0",
33
+ "oxlint": "1.87.0",
34
34
  "typescript": "7.0.2"
35
35
  },
36
36
  "engines": {
@@ -10,7 +10,7 @@ import {
10
10
  serviceAccountSuffix,
11
11
  type UserMap
12
12
  } from '../lib/resolveUsers.ts';
13
- import { resolveCredential } from '../lib/settings.ts';
13
+ import { resolveCredential, type Credential } from '../lib/settings.ts';
14
14
 
15
15
  export const browsePageChangesSummary =
16
16
  'ページの編集履歴(commit)をpageId起点で取得し、誰がいつ何をどう変えたかを自然言語で説明する。タイトル変更(リネーム)も検出する';
@@ -58,6 +58,7 @@ interface Change {
58
58
 
59
59
  interface Commit {
60
60
  id?: string;
61
+ parentId?: string | null;
61
62
  changes?: Change[];
62
63
  userId?: string;
63
64
  created?: number;
@@ -65,6 +66,7 @@ interface Commit {
65
66
 
66
67
  interface CommitsResponse {
67
68
  commits?: Commit[];
69
+ hasMore?: boolean;
68
70
  }
69
71
 
70
72
  interface ParsedArgs {
@@ -116,6 +118,46 @@ const resolveUserName = (
116
118
 
117
119
  const quote = (text: string | undefined): string => `「${text ?? ''}」`;
118
120
 
121
+ // 1回の応答は件数と大きさで打ち切られ、続きがある時はhasMoreが真になる
122
+ const fetchAllCommits = async (
123
+ apiUrl: string,
124
+ since: string | undefined,
125
+ credential: Credential | undefined
126
+ ): Promise<Commit[]> => {
127
+ const commits: Commit[] = [];
128
+ let head = since;
129
+ let continuing = false;
130
+ const requestedHeads = new Set<string>(since ? [since] : []);
131
+ while (true) {
132
+ const url = head ? `${apiUrl}?head=${encodeURIComponent(head)}` : apiUrl;
133
+ const data = (await requestJson(url, { credential })) as CommitsResponse;
134
+ const batch = data.commits ?? [];
135
+ const first = batch[0];
136
+ // 続きが空だと、取れた分を完全な結果として出してしまうので止める。hasMoreは次の要求が空でない事まで保証しない
137
+ if (continuing && !first) {
138
+ throw new Error(`no commits follow head "${head}"`);
139
+ }
140
+ // headに続かない応答を辿ると、同じ応答を取り続けて終わらないので止める
141
+ if (head && first && first.parentId !== head) {
142
+ throw new Error(`commits do not follow head "${head}"`);
143
+ }
144
+ // spreadで足すと、1回の応答が大きい時に引数の上限を超える
145
+ for (const commit of batch) commits.push(commit);
146
+ if (!data.hasMore) return commits;
147
+ const next = batch.at(-1)?.id;
148
+ if (!next) {
149
+ throw new Error('hasMore is true but the last commit has no id');
150
+ }
151
+ // 親子が循環した応答はparentIdの確認を通るので、同じheadを2度要求しない事で止める
152
+ if (requestedHeads.has(next)) {
153
+ throw new Error(`commit "${next}" appears again as head`);
154
+ }
155
+ requestedHeads.add(next);
156
+ head = next;
157
+ continuing = true;
158
+ }
159
+ };
160
+
119
161
  // 時系列の変更イベント。同一行への連続updateは1件に畳む
120
162
  type ChangeEvent =
121
163
  | { kind: 'title'; userIds: string[]; created?: number; title: string }
@@ -205,18 +247,14 @@ export const browsePageChanges = async (args: string[]): Promise<void> => {
205
247
  const { projectUrl, pageId, since } = parseArgs(args);
206
248
  const { origin, projectName } = parseProjectUrlStrict(projectUrl);
207
249
 
208
- let apiUrl = `${origin}/api/commits/${projectName}/${pageId}`;
209
- if (since) {
210
- apiUrl += `?head=${encodeURIComponent(since)}`;
211
- }
250
+ const apiUrl = `${origin}/api/commits/${projectName}/${pageId}`;
212
251
  const credential = resolveCredential(origin, projectName);
213
252
 
214
- const [data, userMap] = await Promise.all([
215
- requestJson(apiUrl, { credential }) as Promise<CommitsResponse>,
253
+ const [commits, userMap] = await Promise.all([
254
+ fetchAllCommits(apiUrl, since, credential),
216
255
  fetchUserMap(origin, projectName)
217
256
  ]);
218
257
 
219
- const commits = data.commits ?? [];
220
258
  const events = buildEvents(commits);
221
259
 
222
260
  const titleEvents = events.filter(e => e.kind === 'title');