@helpfeel/cosense-cli 1.16.0 → 1.16.2

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
@@ -46,7 +46,6 @@ cosense --help
46
46
  - install [direnv](https://direnv.net/)
47
47
  - run `npm install`
48
48
  - install [Codex CLI](https://github.com/openai/codex) and run `codex login`
49
- - modelは `gpt-5.6-sol`、reasoning effortは `high` 以上を推奨
50
49
  - run `claude` or `codex`
51
50
 
52
51
  Claude Codeでこのディレクトリをtrustすると、開発フローで使うpluginが `.claude/settings.json` によりインストールされる。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helpfeel/cosense-cli",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
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.13.6",
30
+ "@types/node": "24.19.1",
31
31
  "npm-run-all": "4.1.5",
32
- "oxfmt": "0.70.0",
33
- "oxlint": "1.85.0",
32
+ "oxfmt": "0.71.0",
33
+ "oxlint": "1.86.0",
34
34
  "typescript": "7.0.2"
35
35
  },
36
36
  "engines": {
@@ -1,5 +1,8 @@
1
1
  import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
2
- import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
2
+ import {
3
+ encodeTitleForReadableLink,
4
+ encodeTitleForUrl
5
+ } from '../lib/encodeTitle.ts';
3
6
  import { applyFileMarkup, fetchFileMarkups } from '../lib/fileMarkup.ts';
4
7
  import { renderLiterateDatabase } from '../lib/literateDatabase.ts';
5
8
  import { parsePageUrl } from '../lib/parseUrl.ts';
@@ -35,10 +38,11 @@ Usage:
35
38
  # <title>
36
39
 
37
40
  ## メタデータ
38
- タイトル / URL / pageId / commitId / 作成日時 / 最終更新日時 / 最終アクセス日時 /
39
- 被リンク数 / pageRank / views / snapshot / 行数 / 文字数 / 作成者 / 最終更新者 /
40
- 関わったユーザー
41
+ タイトル / URL / Readable link / pageId / commitId / 作成日時 / 最終更新日時 /
42
+ 最終アクセス日時 / 被リンク数 / pageRank / views / snapshot / 行数 / 文字数 /
43
+ 作成者 / 最終更新者 / 関わったユーザー
41
44
  URL はタイトルをパーセントエンコードしたページURL
45
+ Readable link は日本語等を生で残した同じページのURL(Cosenseの「Copy readable link」相当)
42
46
  pageId はページの不変ID、commitId は閲覧時点の最新コミット。共同編集中に
43
47
  ページを見失ったら、これらを browsePageChanges に渡して変更(リネーム含む)を辿れる
44
48
 
@@ -92,7 +96,7 @@ Usage:
92
96
  ## 1 hop link(表に載っていないページ) に出力する
93
97
 
94
98
  persistent: false の時:
95
- アイコン・テロメア・Infoboxは省略し、メタデータはURLだけを出力する。
99
+ アイコン・テロメア・Infoboxは省略し、メタデータはURLとReadable linkだけを出力する。
96
100
  (このページはまだ作成されていません) と 本文(テンプレート)と Related Pages を出力する
97
101
 
98
102
  URLに #<lineId> fragmentが指定された時:
@@ -208,10 +212,15 @@ const buildTelomere = (lines: PageLine[]): TelomereEntry[] => {
208
212
  return [...byUser.values()].sort((a, b) => b.lineCount - a.lineCount);
209
213
  };
210
214
 
211
- const renderMetadata = (page: PageData, pageUrl: string): string => {
215
+ const renderMetadata = (
216
+ page: PageData,
217
+ pageUrl: string,
218
+ readableLink: string
219
+ ): string => {
212
220
  const items: string[] = [];
213
221
  if (typeof page.title === 'string') items.push(`- タイトル: ${page.title}`);
214
222
  items.push(`- URL: ${pageUrl}`);
223
+ items.push(`- Readable link: ${readableLink}`);
215
224
  if (typeof page.id === 'string') items.push(`- pageId: ${page.id}`);
216
225
  if (typeof page.commitId === 'string') {
217
226
  items.push(`- commitId: ${page.commitId}`);
@@ -362,11 +371,14 @@ export const browsePage = async (args: string[]): Promise<void> => {
362
371
  const title = page.title ?? '';
363
372
  const persistent = page.persistent !== false;
364
373
  const pageUrl = `${origin}/${projectName}/${encodeTitleForUrl(title)}`;
374
+ const readableLink = `${origin}/${projectName}/${encodeTitleForReadableLink(title)}`;
365
375
  const sections: string[] = [`# ${title}`];
366
376
 
367
377
  if (!persistent) {
368
378
  sections.push('(このページはまだ作成されていません)');
369
- sections.push(`## メタデータ\n\n- URL: ${pageUrl}`);
379
+ sections.push(
380
+ `## メタデータ\n\n- URL: ${pageUrl}\n- Readable link: ${readableLink}`
381
+ );
370
382
  const { body } = renderBody(page.lines ?? [], null, new Map());
371
383
  sections.push(`## 本文(テンプレート)\n\n${body}`);
372
384
  const related = renderRelatedPages(hopValue, page);
@@ -414,7 +426,7 @@ export const browsePage = async (args: string[]): Promise<void> => {
414
426
  ]);
415
427
  enrichPageUsers(page, userMap);
416
428
 
417
- sections.push(renderMetadata(page, pageUrl));
429
+ sections.push(renderMetadata(page, pageUrl, readableLink));
418
430
 
419
431
  const iconsSection = renderHumanIcons(page.icons, userMap);
420
432
  if (iconsSection) sections.push(iconsSection);
@@ -1,4 +1,7 @@
1
- import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
1
+ import {
2
+ encodeTitleForReadableLink,
3
+ encodeTitleForUrl
4
+ } from '../lib/encodeTitle.ts';
2
5
  import { formatTimestamp } from '../lib/formatTimestamp.ts';
3
6
  import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
4
7
  import { requestJson } from '../lib/request.ts';
@@ -32,7 +35,7 @@ Usage:
32
35
  出力形式(Markdown plain text。JSONではない):
33
36
  # ページ変更履歴
34
37
  pageId / 範囲(--since より後か全履歴か)/ commit数 / title change(変更後タイトル、無ければ なし)
35
- タイトルが変わっていた時のみ、新URLも出力する
38
+ タイトルが変わっていた時のみ、新URLと新Readable linkも出力する
36
39
  ## 変更
37
40
  時系列の変更イベントを「<日時>\t<誰>が<何を>」の形(TAB区切り)で列挙する。
38
41
  同一行への連続した編集は1件にまとめ、変更前→最終のテキストを示す。
@@ -231,6 +234,8 @@ export const browsePageChanges = async (args: string[]): Promise<void> => {
231
234
  if (latestTitle !== undefined) {
232
235
  const newUrl = `${origin}/${projectName}/${encodeTitleForUrl(latestTitle)}`;
233
236
  meta.push(`- 新URL: ${newUrl}`);
237
+ const newReadableLink = `${origin}/${projectName}/${encodeTitleForReadableLink(latestTitle)}`;
238
+ meta.push(`- 新Readable link: ${newReadableLink}`);
234
239
  }
235
240
  sections.push(meta.join('\n'));
236
241
 
@@ -30,9 +30,6 @@ Usage:
30
30
  HTTPエラー:
31
31
  401/403: 認証・権限が無い。private projectのファイルはproject member権限が必要
32
32
  404: fileIdに対応するファイルが存在しない
33
-
34
- 注記:
35
- 動画ファイルはサーバー側の制限により取得できない場合がある
36
33
  `;
37
34
 
38
35
  interface ParsedArgs {
@@ -1,4 +1,7 @@
1
- import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
1
+ import {
2
+ encodeTitleForReadableLink,
3
+ encodeTitleForUrl
4
+ } from '../lib/encodeTitle.ts';
2
5
  import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
3
6
  import { requestJson } from '../lib/request.ts';
4
7
  import { resolveCredential } from '../lib/settings.ts';
@@ -19,10 +22,11 @@ Usage:
19
22
  commitId: <生成されたcommitのID>
20
23
  title: <実際に書き込まれたpage title>
21
24
  url: <作成または更新された page の URL。タイトルはパーセントエンコード済み>
25
+ readableLink: <同じ page の URL。日本語等を生で残す(Cosenseの「Copy readable link」相当)>
22
26
 
23
- url はサーバーが返す title から再構築される。 同名ページが既に存在する場合、 サーバーは
24
- タイトルに auto-suffix を付ける (新規作成でも既存ページのタイトル変更でも起きる)。
25
- title/url にはその suffix が反映される。
27
+ url と readableLink はサーバーが返す title から再構築される。 同名ページが既に存在する場合、
28
+ サーバーはタイトルに auto-suffix を付ける (新規作成でも既存ページのタイトル変更でも起きる)。
29
+ title/url/readableLink にはその suffix が反映される。
26
30
 
27
31
  既存ページのタイトルが変わった編集では、続けて以下を出力する:
28
32
  titleChanged: "<変更前title>" -> "<変更後title>"
@@ -89,8 +93,9 @@ export const submitEdit = async (args: string[]): Promise<void> => {
89
93
  );
90
94
  }
91
95
  const url = `${origin}/${projectName}/${encodeTitleForUrl(title)}`;
96
+ const readableLink = `${origin}/${projectName}/${encodeTitleForReadableLink(title)}`;
92
97
  process.stdout.write(
93
- `commitId: ${response.commitId}\ntitle: ${title}\nurl: ${url}\n`
98
+ `commitId: ${response.commitId}\ntitle: ${title}\nurl: ${url}\nreadableLink: ${readableLink}\n`
94
99
  );
95
100
 
96
101
  // titleChangedはサーバーが対応している場合のみ返る
@@ -76,8 +76,8 @@ const parseArgs = (args: string[]): ParsedArgs => {
76
76
  };
77
77
  };
78
78
 
79
- // request.tsのHttpErrorは401/403でcosense loginを案内するが、GCSのsigned URLへの
80
- // PUT失敗にログイン案内は誤誘導なので専用のエラーメッセージを組み立てる
79
+ // request.tsのHttpErrorは401/403でcosense loginか権限の確認を案内するが、GCSのsigned URLへの
80
+ // PUT失敗にはどちらも誤誘導なので専用のエラーメッセージを組み立てる
81
81
  const uploadToSignedUrl = async (
82
82
  url: string,
83
83
  body: Uint8Array,
@@ -8,3 +8,12 @@ export const encodeTitleForUrl = (title: string): string => {
8
8
  c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
9
9
  );
10
10
  };
11
+
12
+ // Cosenseの「Copy readable link」と同じく、日本語等を生で残して人が読めるURLにする
13
+ // URLパーサーは`\`を`/`と解釈し、末尾のC0制御文字を取り除く。空白文字と`<>`は貼った先でURLの自動リンクを途中で切り、`'`はshellのクォートを壊す。これらを`/` `%` `?` `#`と共にエスケープする
14
+ export const encodeTitleForReadableLink = (title: string): string =>
15
+ title
16
+ .replace(/ /g, '_')
17
+ .replace(/[/%?#\\<>\p{Cc}]|\s/gu, c => encodeURIComponent(c))
18
+ // encodeURIComponentは`'`をエスケープしない
19
+ .replace(/'/g, '%27');
@@ -5,6 +5,11 @@ import { Readable } from 'node:stream';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import type { Credential } from './settings.ts';
7
7
 
8
+ const describeCredentialDenied = (credential: Credential): string =>
9
+ credential.type === 'serviceAccount'
10
+ ? 'Service Account key was sent, but the server denied access. Check that the key belongs to the project of the target resource and that this operation is allowed.'
11
+ : 'Personal Access Token was sent, but the server denied access. Check that the account has permission for the target resource and this operation.';
12
+
8
13
  export class HttpError extends Error {
9
14
  readonly status: number;
10
15
  readonly statusText: string;
@@ -16,9 +21,13 @@ export class HttpError extends Error {
16
21
  statusText: string;
17
22
  url: string;
18
23
  body: string;
24
+ credential?: Credential;
19
25
  }) {
20
26
  let message = `HTTP ${params.status} ${params.statusText}\n${params.url}\n${params.body.slice(0, 500)}`;
21
- if (params.status === 401 || params.status === 403) {
27
+ if (params.status === 403 && params.credential) {
28
+ // credential付きの403はtokenの入れ直しでは直らないので、login案内ではなく権限の確認を促す
29
+ message += `\n\n${describeCredentialDenied(params.credential)}`;
30
+ } else if (params.status === 401 || params.status === 403) {
22
31
  let origin: string | undefined;
23
32
  try {
24
33
  origin = new URL(params.url).origin;
@@ -76,7 +85,8 @@ export const requestJson = async (
76
85
  status: res.status,
77
86
  statusText: res.statusText,
78
87
  url,
79
- body
88
+ body,
89
+ credential: options?.credential
80
90
  });
81
91
  }
82
92
  return res.json();
@@ -129,7 +139,8 @@ export const downloadToFile = async (
129
139
  status: res.status,
130
140
  statusText: res.statusText,
131
141
  url,
132
- body
142
+ body,
143
+ credential: options?.credential
133
144
  });
134
145
  }
135
146
  if (!res.body) {