@helpfeel/cosense-cli 1.5.3 → 1.6.0

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.3",
3
+ "version": "1.6.0",
4
4
  "description": "Cosense (旧Scrapbox) のページを読み・調べ・編集するAgent Skill用のCLI",
5
5
  "homepage": "https://github.com/helpfeel/cosense-cli",
6
6
  "license": "MIT",
@@ -8,11 +8,17 @@ export const search1hopLinksSummary = '1-hop近傍を全文検索でフィルタ
8
8
  export const search1hopLinksHelp = `search1hopLinks - 1-hop近傍を全文検索でフィルタする
9
9
 
10
10
  Usage:
11
- cosense search1hopLinks <pageUrl> <query>
11
+ cosense search1hopLinks <pageUrl> <query> [--or]
12
12
 
13
13
  引数:
14
14
  <pageUrl> 対象ページの完全なURL
15
- <query> 全文検索クエリ(必須・空文字は弾かれる)
15
+ <query> 全文検索クエリ(必須。空文字は弾かれる)
16
+
17
+ オプション:
18
+ --or 複数語のいずれかにマッチするページを返す(既定はAND)
19
+
20
+ 例:
21
+ cosense search1hopLinks https://scrapbox.io/shokai/カレー "うどん ラーメン" --or
16
22
 
17
23
  戻り値(top-levelの主なkey):
18
24
  links1hop Array<Link> query を本文に含む1-hop近傍ページ
@@ -20,19 +26,35 @@ Usage:
20
26
 
21
27
  各 Link の field(list1hopLinksに加えて):
22
28
  search 検索ハイライト情報
23
-
24
- 検索の制約:
25
- - OR検索不可
26
29
  `;
27
30
 
28
- export const search1hopLinks = async (args: string[]): Promise<void> => {
29
- const [url, query] = args;
30
- if (args.length !== 2 || !url || !query || query.trim() === '') {
31
- throw new Error('Usage: cosense search1hopLinks <pageUrl> <query>');
31
+ const parseArgs = (
32
+ args: string[]
33
+ ): { url: string; query: string; or: boolean } => {
34
+ const usage = 'Usage: cosense search1hopLinks <pageUrl> <query> [--or]';
35
+ let or = false;
36
+ const positional: string[] = [];
37
+ for (const arg of args) {
38
+ if (arg === '--or') {
39
+ or = true;
40
+ } else if (arg.startsWith('--')) {
41
+ throw new Error(`Unknown option: ${arg}\n${usage}`);
42
+ } else {
43
+ positional.push(arg);
44
+ }
32
45
  }
46
+ const [url, query] = positional;
47
+ if (positional.length !== 2 || !url || !query || query.trim() === '') {
48
+ throw new Error(usage);
49
+ }
50
+ return { url, query, or };
51
+ };
52
+
53
+ export const search1hopLinks = async (args: string[]): Promise<void> => {
54
+ const { url, query, or } = parseArgs(args);
33
55
  const { origin, projectName } = parsePageUrl(url);
34
56
  const [data, userMap] = await Promise.all([
35
- fetchRelatedPagesWithRelations(url, query),
57
+ fetchRelatedPagesWithRelations(url, query, or),
36
58
  fetchUserMap(origin, projectName)
37
59
  ]);
38
60
  for (const page of (data as { links1hop?: unknown[] }).links1hop ?? []) {
@@ -8,11 +8,17 @@ export const search2hopLinksSummary = '2-hop近傍を全文検索でフィルタ
8
8
  export const search2hopLinksHelp = `search2hopLinks - 2-hop近傍を全文検索でフィルタする
9
9
 
10
10
  Usage:
11
- cosense search2hopLinks <pageUrl> <query>
11
+ cosense search2hopLinks <pageUrl> <query> [--or]
12
12
 
13
13
  引数:
14
14
  <pageUrl> 対象ページの完全なURL
15
- <query> 全文検索クエリ(必須・空文字は弾かれる)
15
+ <query> 全文検索クエリ(必須。空文字は弾かれる)
16
+
17
+ オプション:
18
+ --or 複数語のいずれかにマッチするページを返す(既定はAND)
19
+
20
+ 例:
21
+ cosense search2hopLinks https://scrapbox.io/shokai/カレー "うどん ラーメン" --or
16
22
 
17
23
  戻り値(top-levelの主なkey):
18
24
  links2hop Array<Link> query を本文に含む2-hop近傍ページ
@@ -20,19 +26,35 @@ Usage:
20
26
 
21
27
  各 Link の field(list2hopLinksに加えて):
22
28
  search 検索ハイライト情報
23
-
24
- 検索の制約:
25
- - OR検索不可
26
29
  `;
27
30
 
28
- export const search2hopLinks = async (args: string[]): Promise<void> => {
29
- const [url, query] = args;
30
- if (args.length !== 2 || !url || !query || query.trim() === '') {
31
- throw new Error('Usage: cosense search2hopLinks <pageUrl> <query>');
31
+ const parseArgs = (
32
+ args: string[]
33
+ ): { url: string; query: string; or: boolean } => {
34
+ const usage = 'Usage: cosense search2hopLinks <pageUrl> <query> [--or]';
35
+ let or = false;
36
+ const positional: string[] = [];
37
+ for (const arg of args) {
38
+ if (arg === '--or') {
39
+ or = true;
40
+ } else if (arg.startsWith('--')) {
41
+ throw new Error(`Unknown option: ${arg}\n${usage}`);
42
+ } else {
43
+ positional.push(arg);
44
+ }
32
45
  }
46
+ const [url, query] = positional;
47
+ if (positional.length !== 2 || !url || !query || query.trim() === '') {
48
+ throw new Error(usage);
49
+ }
50
+ return { url, query, or };
51
+ };
52
+
53
+ export const search2hopLinks = async (args: string[]): Promise<void> => {
54
+ const { url, query, or } = parseArgs(args);
33
55
  const { origin, projectName } = parsePageUrl(url);
34
56
  const [data, userMap] = await Promise.all([
35
- fetchRelatedPages(url, 2, query),
57
+ fetchRelatedPages(url, 2, query, or),
36
58
  fetchUserMap(origin, projectName)
37
59
  ]);
38
60
  for (const page of (data as { links2hop?: unknown[] }).links2hop ?? []) {
@@ -9,12 +9,19 @@ export const searchFullTextSummary = '本文全文を対象に検索する';
9
9
  export const searchFullTextHelp = `searchFullText - 本文全文を対象に検索する
10
10
 
11
11
  Usage:
12
- cosense searchFullText <projectUrl> <query>
12
+ cosense searchFullText <projectUrl> <query> [--or] [--sort <pageRank|updated>]
13
13
 
14
14
  引数:
15
15
  <projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai/)
16
16
  <query> 検索クエリ
17
17
 
18
+ オプション:
19
+ --or 複数語のいずれかにマッチするページを返す(既定はAND)
20
+ --sort <pageRank|updated> 並び順(既定はpageRank)
21
+
22
+ 例:
23
+ cosense searchFullText https://scrapbox.io/shokai/ "デザイン 設計 design UI UX プロトタイプ" --or
24
+
18
25
  戻り値(top-levelの主なkey):
19
26
  projectName string プロジェクト名
20
27
  searchQuery string 実行されたクエリ
@@ -63,13 +70,56 @@ interface SearchFullTextData {
63
70
  }[];
64
71
  }
65
72
 
66
- export const searchFullText = async (args: string[]): Promise<void> => {
67
- const [url, query] = args;
68
- if (!url || !query) {
69
- throw new Error('Usage: cosense searchFullText <projectUrl> <query>');
73
+ interface ParsedArgs {
74
+ projectUrl: string;
75
+ query: string;
76
+ or: boolean;
77
+ sort?: string;
78
+ }
79
+
80
+ const parseArgs = (args: string[]): ParsedArgs => {
81
+ const usage =
82
+ 'Usage: cosense searchFullText <projectUrl> <query> [--or] [--sort <pageRank|updated>]';
83
+ let or = false;
84
+ let sort: string | undefined;
85
+ const positional: string[] = [];
86
+ for (let i = 0; i < args.length; i += 1) {
87
+ const arg = args[i] as string;
88
+ if (arg === '--or') {
89
+ or = true;
90
+ } else if (arg === '--sort') {
91
+ const value = args[i + 1];
92
+ if (value === undefined || value.startsWith('--')) {
93
+ throw new Error(`--sort requires a value\n${usage}`);
94
+ }
95
+ if (value !== 'pageRank' && value !== 'updated') {
96
+ throw new Error(`--sort must be pageRank or updated\n${usage}`);
97
+ }
98
+ sort = value;
99
+ i += 1;
100
+ } else if (arg.startsWith('--')) {
101
+ throw new Error(`Unknown option: ${arg}\n${usage}`);
102
+ } else {
103
+ positional.push(arg);
104
+ }
70
105
  }
71
- const { origin, projectName } = parseProjectUrl(url);
72
- const apiUrl = `${origin}/api/pages/${projectName}/search/query?q=${encodeURIComponent(query)}`;
106
+ if (positional.length !== 2 || !positional[1]) {
107
+ throw new Error(usage);
108
+ }
109
+ return {
110
+ projectUrl: positional[0] as string,
111
+ query: positional[1] as string,
112
+ or,
113
+ sort
114
+ };
115
+ };
116
+
117
+ export const searchFullText = async (args: string[]): Promise<void> => {
118
+ const { projectUrl, query, or, sort } = parseArgs(args);
119
+ const { origin, projectName } = parseProjectUrl(projectUrl);
120
+ let apiUrl = `${origin}/api/pages/${projectName}/search/query?q=${encodeURIComponent(query)}`;
121
+ if (or) apiUrl += '&op=or';
122
+ if (sort) apiUrl += `&sort=${sort}`;
73
123
  const credential = resolveCredential(origin, projectName);
74
124
  const data = (await requestJson(apiUrl, {
75
125
  credential
@@ -41,10 +41,12 @@ const computeRelation = (
41
41
 
42
42
  export const fetchRelatedPagesWithRelations = async (
43
43
  url: string,
44
- query?: string
44
+ query?: string,
45
+ or?: boolean
45
46
  ): Promise<RelatedPagesData> => {
46
47
  const { origin, projectName, encodedTitle } = parsePageUrl(url);
47
- const queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
48
+ let queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
49
+ if (or) queryParam += queryParam ? '&op=or' : '?op=or';
48
50
  const startPageUrl = `${origin}/api/pages/v2/${projectName}/${encodedTitle}`;
49
51
  const relatedUrl = `${startPageUrl}/links1hop${queryParam}`;
50
52
  const credential = resolveCredential(origin, projectName);
@@ -5,10 +5,12 @@ import { resolveCredential } from './settings.ts';
5
5
  export const fetchRelatedPages = async (
6
6
  url: string,
7
7
  hop: 1 | 2,
8
- query?: string
8
+ query?: string,
9
+ or?: boolean
9
10
  ): Promise<unknown> => {
10
11
  const { origin, projectName, encodedTitle } = parsePageUrl(url);
11
- const queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
12
+ let queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
13
+ if (or) queryParam += queryParam ? '&op=or' : '?op=or';
12
14
  const apiUrl = `${origin}/api/pages/v2/${projectName}/${encodedTitle}/links${hop}hop${queryParam}`;
13
15
  const credential = resolveCredential(origin, projectName);
14
16
  return requestJson(apiUrl, { credential });