@helpfeel/cosense-cli 1.5.1 → 1.5.3

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.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Cosense (旧Scrapbox) のページを読み・調べ・編集するAgent Skill用のCLI",
5
5
  "homepage": "https://github.com/helpfeel/cosense-cli",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@ import { requestJson } from '../lib/request.ts';
11
11
  import {
12
12
  enrichPageUsers,
13
13
  fetchUserMap,
14
+ serviceAccountSuffix,
14
15
  type UserMap
15
16
  } from '../lib/resolveUsers.ts';
16
17
  import { resolveCredential } from '../lib/settings.ts';
@@ -85,6 +86,8 @@ interface UserRef {
85
86
  id?: string;
86
87
  name?: string;
87
88
  displayName?: string;
89
+ isServiceAccount?: boolean;
90
+ isServiceAccountDeleted?: boolean;
88
91
  }
89
92
 
90
93
  interface InfoboxResult {
@@ -129,8 +132,12 @@ const extractFragment = (input: string): string | null => {
129
132
 
130
133
  const formatUserDisplay = (u: UserRef | null | undefined): string | null => {
131
134
  if (!u) return null;
132
- if (u.displayName && u.name) return `${u.displayName} (${u.name})`;
133
- return u.displayName ?? u.name ?? u.id ?? null;
135
+ const base =
136
+ u.displayName && u.name
137
+ ? `${u.displayName} (${u.name})`
138
+ : (u.displayName ?? u.name ?? u.id ?? null);
139
+ if (base === null) return null;
140
+ return `${base}${serviceAccountSuffix(u)}`;
134
141
  };
135
142
 
136
143
  const formatDateYMD = (unixSec: number): string => {
@@ -232,7 +239,8 @@ const renderTelomere = (
232
239
  if (entries.length === 0) return null;
233
240
  const lines = entries.map(e => {
234
241
  const info = userMap.get(e.userId);
235
- const name = info?.displayName ?? info?.name ?? e.userId;
242
+ const base = info?.displayName ?? info?.name ?? e.userId;
243
+ const name = info ? `${base}${serviceAccountSuffix(info)}` : base;
236
244
  return `- ${name}\t更新期間 ${formatDateYMD(e.minUpdated)} 〜 ${formatDateYMD(e.maxUpdated)}\t${e.lineCount}行更新`;
237
245
  });
238
246
  return `## テロメアのサマリー\n\n${lines.join('\n')}`;
@@ -2,7 +2,11 @@ import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
2
2
  import { formatTimestamp } from '../lib/formatTimestamp.ts';
3
3
  import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
4
4
  import { requestJson } from '../lib/request.ts';
5
- import { fetchUserMap, type UserMap } from '../lib/resolveUsers.ts';
5
+ import {
6
+ fetchUserMap,
7
+ serviceAccountSuffix,
8
+ type UserMap
9
+ } from '../lib/resolveUsers.ts';
6
10
  import { resolveCredential } from '../lib/settings.ts';
7
11
 
8
12
  export const browsePageChangesSummary =
@@ -102,7 +106,9 @@ const resolveUserName = (
102
106
  ): string => {
103
107
  if (!userId) return '不明なユーザー';
104
108
  const info = userMap.get(userId);
105
- return info?.displayName ?? info?.name ?? userId;
109
+ if (!info) return userId;
110
+ const base = info.displayName ?? info.name ?? userId;
111
+ return `${base}${serviceAccountSuffix(info)}`;
106
112
  };
107
113
 
108
114
  const quote = (text: string | undefined): string => `「${text ?? ''}」`;
@@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
4
4
  import { requestJson } from '../lib/request.ts';
5
- import { resolveUserCredential } from '../lib/settings.ts';
5
+ import { resolveCredential } from '../lib/settings.ts';
6
6
 
7
7
  export const previewEditSummary =
8
8
  'ページ編集opsをdry-runしてpreviewIdを取得する';
@@ -56,7 +56,7 @@ stdinから受け取る入力形式(既存ページ編集モード, JSON):
56
56
 
57
57
  HTTPエラー:
58
58
  HTTP 401 認証なし
59
- HTTP 403 memberまたはService Account(書き込みはPAT限定)
59
+ HTTP 403 権限不足(PAT利用時、projectのmemberでない 等)
60
60
  HTTP 404 pageId に対応するpageが存在しない / pageId が不正な形式
61
61
  HTTP 409 {"error":"NotFastForward","latest":...}
62
62
  preview生成後にページが更新された。最新stateを再取得して ops を作り直す必要がある
@@ -384,8 +384,8 @@ export const previewEdit = async (args: string[]): Promise<void> => {
384
384
  }
385
385
  const { changes, newLineIds, updatedLineIds } = translateOps(ops);
386
386
 
387
- // 書き込み系 API は PAT 限定 (Service Account 拒否) なので、PAT を直接解決する
388
- const credential = resolveUserCredential(origin);
387
+ // projectに紐づくService Accountがあればそれを、無ければPATを使う(読み取りと同じ)
388
+ const credential = resolveCredential(origin, projectName);
389
389
 
390
390
  const apiUrl = `${origin}/api/pages/v2/${projectName}/page-edit-for-ai/preview`;
391
391
  const requestBody = pageId ? { pageId, changes } : { changes };
@@ -14,8 +14,10 @@ Usage:
14
14
  <projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai)
15
15
 
16
16
  戻り値(top-levelの主なkey):
17
- users Array<User> 現メンバー一覧
18
- memberSnapshots Array<Snapshot>? 退去済みメンバーの記録
17
+ users Array<User> 現メンバー一覧
18
+ memberSnapshots Array<Snapshot>? 退去済みメンバーの記録
19
+ serviceAccounts Array<ServiceAccount>? Service Account一覧
20
+ serviceAccountSnapshots Array<ServiceAccount>? 削除済みService Accountの記録
19
21
 
20
22
  User の field:
21
23
  id string Cosense内部のID
@@ -34,6 +36,10 @@ Snapshot の field:
34
36
  updated string 退去者snapshot更新時刻
35
37
  data { id, name?, displayName?, email? } 退去時のユーザー情報
36
38
 
39
+ ServiceAccount の field:
40
+ id string Service AccountのID(このIDでページ・行の著者を同定できる)
41
+ usage string 用途ラベル(著者の表示名として使う)
42
+
37
43
  戻り値のJSON抜粋例:
38
44
  {
39
45
  "users": [
@@ -60,6 +66,12 @@ Snapshot の field:
60
66
  "email": "former@example.com"
61
67
  }
62
68
  }
69
+ ],
70
+ "serviceAccounts": [
71
+ { "id": "6a44...", "usage": "CLI test" }
72
+ ],
73
+ "serviceAccountSnapshots": [
74
+ { "id": "6a1a...", "usage": "Cosense CLI" }
63
75
  ]
64
76
  }
65
77
  `;
@@ -1,7 +1,7 @@
1
1
  import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
2
2
  import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
3
3
  import { requestJson } from '../lib/request.ts';
4
- import { resolveUserCredential } from '../lib/settings.ts';
4
+ import { resolveCredential } from '../lib/settings.ts';
5
5
 
6
6
  export const submitEditSummary =
7
7
  'previewEditで取得したpreviewIdを使ってページ編集を確定する';
@@ -26,7 +26,7 @@ Usage:
26
26
  HTTPエラー:
27
27
  HTTP 400 preview を生成した時と違う project の URL を渡している
28
28
  HTTP 401 認証なし
29
- HTTP 403 memberまたはService Account(書き込みはPAT限定)
29
+ HTTP 403 権限不足(PAT利用時、projectのmemberでない 等)
30
30
  HTTP 404 preview が見つからない / 期限切れ (5分) / 既にconsume済み / 他userのpreview
31
31
  HTTP 409 {"error":"NotFastForward","latest":...}
32
32
  preview生成後にページが更新された。最新stateを再取得して ops を作り直し、
@@ -50,8 +50,8 @@ export const submitEdit = async (args: string[]): Promise<void> => {
50
50
 
51
51
  const { origin, projectName } = parseProjectUrlStrict(projectUrl);
52
52
  const apiUrl = `${origin}/api/pages/v2/${projectName}/page-edit-for-ai/submit`;
53
- // 書き込み系 API は PAT 限定 (Service Account 拒否) なので、PAT を直接解決する
54
- const credential = resolveUserCredential(origin);
53
+ // projectに紐づくService Accountがあればそれを、無ければPATを使う(読み取りと同じ)
54
+ const credential = resolveCredential(origin, projectName);
55
55
  const response = (await requestJson(apiUrl, {
56
56
  credential,
57
57
  method: 'POST',
@@ -5,6 +5,8 @@ interface UserInfo {
5
5
  name?: string;
6
6
  displayName?: string;
7
7
  email?: string;
8
+ isServiceAccount?: boolean;
9
+ isServiceAccountDeleted?: boolean;
8
10
  }
9
11
 
10
12
  export type UserMap = Map<string, UserInfo>;
@@ -20,9 +22,16 @@ interface MemberSnapshotEntry {
20
22
  data?: UserEntry;
21
23
  }
22
24
 
25
+ interface ServiceAccountEntry {
26
+ id?: unknown;
27
+ usage?: unknown;
28
+ }
29
+
23
30
  interface UsersResponse {
24
31
  users?: UserEntry[];
25
32
  memberSnapshots?: MemberSnapshotEntry[];
33
+ serviceAccounts?: ServiceAccountEntry[];
34
+ serviceAccountSnapshots?: ServiceAccountEntry[];
26
35
  }
27
36
 
28
37
  const cache = new Map<string, UserMap>();
@@ -30,6 +39,18 @@ const cache = new Map<string, UserMap>();
30
39
  const stringOrUndefined = (value: unknown): string | undefined =>
31
40
  typeof value === 'string' && value !== '' ? value : undefined;
32
41
 
42
+ // Service Account著者の表示名末尾に付ける注記。名前を文字列整形するコマンド
43
+ // (browsePage / browsePageChanges) で共用し、usageが人間名に見えるのを防ぐ
44
+ export const serviceAccountSuffix = (info: {
45
+ isServiceAccount?: boolean;
46
+ isServiceAccountDeleted?: boolean;
47
+ }): string => {
48
+ if (!info.isServiceAccount) return '';
49
+ return info.isServiceAccountDeleted
50
+ ? ' (deleted service account)'
51
+ : ' (service account)';
52
+ };
53
+
33
54
  const buildUserInfo = (entry: UserEntry): UserInfo => {
34
55
  const info: UserInfo = {};
35
56
  const name = stringOrUndefined(entry.name);
@@ -66,6 +87,23 @@ export const fetchUserMap = async (
66
87
  if (!id || map.has(id)) continue;
67
88
  map.set(id, buildUserInfo(entry));
68
89
  }
90
+ for (const entry of data.serviceAccounts ?? []) {
91
+ const id = stringOrUndefined(entry.id);
92
+ if (!id || map.has(id)) continue;
93
+ map.set(id, {
94
+ displayName: stringOrUndefined(entry.usage),
95
+ isServiceAccount: true
96
+ });
97
+ }
98
+ for (const entry of data.serviceAccountSnapshots ?? []) {
99
+ const id = stringOrUndefined(entry.id);
100
+ if (!id || map.has(id)) continue;
101
+ map.set(id, {
102
+ displayName: stringOrUndefined(entry.usage),
103
+ isServiceAccount: true,
104
+ isServiceAccountDeleted: true
105
+ });
106
+ }
69
107
 
70
108
  cache.set(cacheKey, map);
71
109
  return map;