@kanf/wema-kake 0.1.1 → 0.1.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 +1 -1
- package/dist/lib/shared/hierarchy.d.ts +5 -0
- package/dist/lib/shared/hierarchy.js +2 -0
- package/dist/lib/shared/tools.d.ts +7 -1
- package/dist/lib/shared/tools.js +133 -3
- package/dist/lib/worker/mcp/board-access.d.ts +3 -0
- package/dist/lib/worker/mcp/board-access.js +11 -0
- package/dist/lib/worker/page-do.d.ts +1 -1
- package/dist/web/assets/{index-CVwMafiH.js → index-B8NVsNXL.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +3 -3
- package/template/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npm run deploy
|
|
|
62
62
|
- 複数のブラウザで同じページを開くと、変更がその場で反映される
|
|
63
63
|
- ページの中に子ページを置ける。子ページは、中の付箋の配置を縮小した図つきの付箋として表示され、押すとズームして入る
|
|
64
64
|
- ページの一覧もボードで、ページが付箋、ページ間のリンクが線になる。全文検索で絞り込める
|
|
65
|
-
- リモート MCP サーバーを備えていて、claude.ai のコネクタとして登録できる。LLM
|
|
65
|
+
- リモート MCP サーバーを備えていて、claude.ai のコネクタとして登録できる。LLM がボードを読み、付箋を足し、整理する。ページを作り、子ページとしてまとめることもできる
|
|
66
66
|
- LLM の操作は、操作の単位で記録される。ページの画面から、まとめて取り消せる
|
|
67
67
|
|
|
68
68
|
## wema との関係
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { HistoryDelta, WemaNote } from './delta';
|
|
2
2
|
/** 子ページの付箋の meta のキー。値は、子ページのスラッグ */
|
|
3
3
|
export declare const CHILD_PAGE_KEY = "page";
|
|
4
|
+
/** 子ページの付箋の大きさ。配置の再現が入るよう、ふつうの付箋より少し大きくする */
|
|
5
|
+
export declare const CHILD_NOTE_SIZE: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
4
9
|
/** 階層の深さの上限。ルートを 1 段目と数える */
|
|
5
10
|
export declare const MAX_DEPTH = 5;
|
|
6
11
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** 子ページの付箋の meta のキー。値は、子ページのスラッグ */
|
|
2
2
|
export const CHILD_PAGE_KEY = 'page';
|
|
3
|
+
/** 子ページの付箋の大きさ。配置の再現が入るよう、ふつうの付箋より少し大きくする */
|
|
4
|
+
export const CHILD_NOTE_SIZE = { width: 240, height: 180 };
|
|
3
5
|
/** 階層の深さの上限。ルートを 1 段目と数える */
|
|
4
6
|
export const MAX_DEPTH = 5;
|
|
5
7
|
/** 置けなかったときの `reject` の `reason` の先頭。続けて、理由と、置こうとしたページが入る */
|
|
@@ -42,11 +42,17 @@ export interface BoardAccess {
|
|
|
42
42
|
}[]>;
|
|
43
43
|
/** ページがまだ作られていなければ null */
|
|
44
44
|
readBoard(page: string): Promise<BoardState | null>;
|
|
45
|
+
/** 親ページのスラッグ(ルートなら null)。ページがなければ undefined。索引ではなく、ページの今の値を返す */
|
|
46
|
+
parentOf(page: string): Promise<string | null | undefined>;
|
|
47
|
+
/** ページの親が `parent` なら、ルートへ戻す(親の記録が古く、親ページに子ページの付箋がないときに使う) */
|
|
48
|
+
clearParent(page: string, parent: string): Promise<void>;
|
|
49
|
+
/** ページを、付箋のない状態で作る。すでにあれば作らずに `exists` を返す */
|
|
50
|
+
createPage(page: string, title: string): Promise<'created' | 'exists' | 'invalid title'>;
|
|
45
51
|
/** 付箋の text(HTML)をプレーンテキストにする */
|
|
46
52
|
plainText(html: string): Promise<string>;
|
|
47
53
|
/**
|
|
48
54
|
* デルタを 1 つの操作として適用する。`summary` は履歴に出る説明。
|
|
49
|
-
* ページがなければ適用せず、`ok: false`
|
|
55
|
+
* ページがなければ適用せず、`ok: false` を返すこと(ページを作るのは create_page だけ)
|
|
50
56
|
*/
|
|
51
57
|
apply(page: string, deltas: HistoryDelta[], summary: string): Promise<ApplyOutcome>;
|
|
52
58
|
/** 自分(この agent)の最近の操作 */
|
package/dist/lib/shared/tools.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// 渡せば、同じツールがそのまま使える。
|
|
6
6
|
import { computeAutoLayout } from '@kanf/wema';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { MAX_LIST_LIMIT, MAX_QUERY_LENGTH } from './api';
|
|
9
|
-
import { childPageOf } from './hierarchy';
|
|
8
|
+
import { MAX_LIST_LIMIT, MAX_QUERY_LENGTH, MAX_TITLE_LENGTH } from './api';
|
|
9
|
+
import { CHILD_NOTE_SIZE, CHILD_PAGE_KEY, childPageOf, MAX_DEPTH } from './hierarchy';
|
|
10
10
|
import { textToHtml } from './note-text';
|
|
11
11
|
import { SLUG_RE } from './slug';
|
|
12
12
|
/** ツールが入力や状態の問題で実行できなかった。メッセージは LLM に返す */
|
|
@@ -60,7 +60,7 @@ function placeNote(size, near, taken) {
|
|
|
60
60
|
}
|
|
61
61
|
async function existingBoard(access, slug) {
|
|
62
62
|
const board = await access.readBoard(slug);
|
|
63
|
-
//
|
|
63
|
+
// 書き込みのツールは、ページを作らない(作るのは create_page だけ)
|
|
64
64
|
if (!board)
|
|
65
65
|
throw new ToolError(`page not found: ${slug}`);
|
|
66
66
|
return { ...board, notesById: new Map(board.notes.map((n) => [n.id, n])) };
|
|
@@ -87,6 +87,33 @@ function assertUnique(ids) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
const summaryOf = (tool, why) => (why ? `${tool}: ${why}` : tool);
|
|
90
|
+
const messageOf = (e) => (e instanceof Error ? e.message : String(e));
|
|
91
|
+
/** `page` が `ancestor` の子孫(`ancestor` 自身を含む)か。親をたどる(階層の深さの上限までで止まる) */
|
|
92
|
+
async function isDescendant(access, page, ancestor) {
|
|
93
|
+
let name = page;
|
|
94
|
+
for (let depth = 0; name && depth <= MAX_DEPTH; depth++) {
|
|
95
|
+
if (name === ancestor)
|
|
96
|
+
return true;
|
|
97
|
+
name = await access.parentOf(name);
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `parent` のボードに、`child` の子ページの付箋を置く(既存の付箋の下に、重ならないように)。
|
|
103
|
+
* 置けるかどうか(親は 1 つだけ、輪にならない、深さ)は、ページの DO が確かめる。記録した操作の番号を返す
|
|
104
|
+
*/
|
|
105
|
+
async function placeChild(access, parent, parentSlug, child, summary) {
|
|
106
|
+
const rect = placeNote(CHILD_NOTE_SIZE, undefined, parent.notes);
|
|
107
|
+
const note = {
|
|
108
|
+
id: crypto.randomUUID(),
|
|
109
|
+
...rect,
|
|
110
|
+
text: '',
|
|
111
|
+
color: NOTE_COLOR,
|
|
112
|
+
zIndex: 1,
|
|
113
|
+
meta: { [CHILD_PAGE_KEY]: child },
|
|
114
|
+
};
|
|
115
|
+
return applyOrThrow(access, parentSlug, [{ type: 'note:create', note }], summary);
|
|
116
|
+
}
|
|
90
117
|
function findNote(board, id) {
|
|
91
118
|
const note = board.notesById.get(id);
|
|
92
119
|
if (!note)
|
|
@@ -321,6 +348,109 @@ export const TOOLS = [
|
|
|
321
348
|
return { operation: seq, moved: deltas.length };
|
|
322
349
|
},
|
|
323
350
|
}),
|
|
351
|
+
tool({
|
|
352
|
+
name: 'create_page',
|
|
353
|
+
description: 'ページを新しく作る。付箋のない空のページができる。parent を指定すると、そのページの中に子ページとして置く(parent のボードに、子ページを表す付箋が足される)。すでにある名前では作れない(既存のページを別のページの中へ移すときは move_page を使う)。ページの作成そのものは、revert_operation では取り消せない。子ページとして置いた操作は、parent のページで取り消せる(取り消すと、作ったページはルートのページとして残る)。',
|
|
354
|
+
readOnly: false,
|
|
355
|
+
input: z.object({
|
|
356
|
+
name: z
|
|
357
|
+
.string()
|
|
358
|
+
.regex(SLUG_RE)
|
|
359
|
+
.describe('新しいページのスラッグ(URL に使う名前。英小文字、数字、ハイフン。後から変えられない)'),
|
|
360
|
+
title: z.string().max(MAX_TITLE_LENGTH).optional().describe('ページの表示名。省略すると、スラッグが表示される'),
|
|
361
|
+
parent: page.optional().describe('子ページとして置く先のページのスラッグ。省略すると、ルートのページになる'),
|
|
362
|
+
reason,
|
|
363
|
+
}),
|
|
364
|
+
async run(access, input) {
|
|
365
|
+
const { name, parent } = input;
|
|
366
|
+
if (parent === name)
|
|
367
|
+
throw new ToolError(`a page cannot be placed in itself: ${name}`);
|
|
368
|
+
// 置く先が無ければ、ページを作る前に断る
|
|
369
|
+
const parentBoard = parent === undefined ? undefined : await existingBoard(access, parent);
|
|
370
|
+
const created = await access.createPage(name, input.title ?? '');
|
|
371
|
+
if (created === 'exists') {
|
|
372
|
+
throw new ToolError(`page already exists: ${name} (use move_page to place an existing page)`);
|
|
373
|
+
}
|
|
374
|
+
if (created === 'invalid title')
|
|
375
|
+
throw new ToolError('invalid title (control characters are not allowed)');
|
|
376
|
+
if (parent === undefined || parentBoard === undefined)
|
|
377
|
+
return { page: name, parent: null };
|
|
378
|
+
try {
|
|
379
|
+
const seq = await placeChild(access, parentBoard, parent, name, summaryOf('create_page', input.reason));
|
|
380
|
+
return { page: name, parent, operation: seq };
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
// ページはできているので、どんな失敗でも、そのことを伝える(作り直そうとすると exists で断られる)
|
|
384
|
+
throw new ToolError(`created ${name} as a root page, but could not place it in ${parent}: ${messageOf(e)}`);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
tool({
|
|
389
|
+
name: 'move_page',
|
|
390
|
+
description: '既存のページを、別のページの中へ子ページとして移す(今の親ページにある子ページの付箋を消し、移す先に置く)。parent に null を指定すると、親から外してルートのページにする。子ページを持つページは、子孫ごと移る。今の親ページで子ページの付箋につながっていた接続線は消える。移す先に置けなかった場合(輪になる、階層が深すぎるなど)は、元の親ページに戻してエラーを返す。operations は、変更したページと操作の番号の組で、それぞれのページで revert_operation に渡せる。',
|
|
391
|
+
readOnly: false,
|
|
392
|
+
input: z.object({
|
|
393
|
+
name: page.describe('移すページのスラッグ'),
|
|
394
|
+
parent: page
|
|
395
|
+
.nullable()
|
|
396
|
+
.describe('移す先の親ページのスラッグ。null なら、親から外してルートのページにする'),
|
|
397
|
+
reason,
|
|
398
|
+
}),
|
|
399
|
+
async run(access, input) {
|
|
400
|
+
const { name, parent: target } = input;
|
|
401
|
+
const current = await access.parentOf(name);
|
|
402
|
+
if (current === undefined)
|
|
403
|
+
throw new ToolError(`page not found: ${name}`);
|
|
404
|
+
if (target === name)
|
|
405
|
+
throw new ToolError(`a page cannot be placed in itself: ${name}`);
|
|
406
|
+
if (current === target) {
|
|
407
|
+
throw new ToolError(target === null ? `already a root page: ${name}` : `already in ${target}: ${name}`);
|
|
408
|
+
}
|
|
409
|
+
const summary = summaryOf('move_page', input.reason);
|
|
410
|
+
// 移す先が無いか、移す先が自分の子孫(輪になる)なら、今の親から外す前に断る。
|
|
411
|
+
// 深さの上限は、置くときに DO が確かめる(断られたら、下で元に戻す)
|
|
412
|
+
const targetBoard = target === null ? undefined : await existingBoard(access, target);
|
|
413
|
+
if (target !== null && (await isDescendant(access, target, name))) {
|
|
414
|
+
throw new ToolError(`could not place ${name} in ${target}: ${target} is inside ${name}`);
|
|
415
|
+
}
|
|
416
|
+
const operations = [];
|
|
417
|
+
let removed;
|
|
418
|
+
if (current !== null) {
|
|
419
|
+
const board = await access.readBoard(current);
|
|
420
|
+
const notes = board?.notes.filter((n) => childPageOf(n) === name) ?? [];
|
|
421
|
+
if (notes.length > 0) {
|
|
422
|
+
removed = await applyOrThrow(access, current, notes.map((note) => ({ type: 'note:delete', note })), summary);
|
|
423
|
+
operations.push({ page: current, operation: removed });
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
// 親の記録が古い(親ページに付箋がない、親ページがもうない)。記録だけを消す
|
|
427
|
+
await access.clearParent(name, current);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (target === null || targetBoard === undefined)
|
|
431
|
+
return { page: name, from: current, to: null, operations };
|
|
432
|
+
try {
|
|
433
|
+
const placed = await placeChild(access, targetBoard, target, name, summary);
|
|
434
|
+
operations.push({ page: target, operation: placed });
|
|
435
|
+
return { page: name, from: current, to: target, operations };
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
const why = `could not place ${name} in ${target}: ${messageOf(e)}`;
|
|
439
|
+
if (current === null || removed === undefined)
|
|
440
|
+
throw new ToolError(why);
|
|
441
|
+
// 置けなかったので、外した操作を取り消して、元の親ページに戻す
|
|
442
|
+
await access
|
|
443
|
+
.revert(current, removed, summaryOf('move_page', `restore ${name} after a failed move`))
|
|
444
|
+
.catch(() => undefined);
|
|
445
|
+
// 戻せたかは、取り消しの結果ではなく、今の親で判断する(間に人が別のページへ置いたこともある)
|
|
446
|
+
const now = await access.parentOf(name).catch(() => undefined);
|
|
447
|
+
if (now === current)
|
|
448
|
+
throw new ToolError(`${why} (left it in ${current})`);
|
|
449
|
+
const where = now === null ? 'is now a root page' : now === undefined ? 'could not be checked' : `is now in ${now}`;
|
|
450
|
+
throw new ToolError(`${why}. It was removed from ${current} and ${where}; operation ${removed} on ${current} is the removal`);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
}),
|
|
324
454
|
tool({
|
|
325
455
|
name: 'revert_operation',
|
|
326
456
|
description: '自分が行った操作を取り消す。operation を省略すると、まだ取り消していない直前の操作が対象になる。その後に人が変更した付箋は戻さず、skipped で返す。何も戻せなかった場合(applied が 0)は、それより前の自分の操作の番号を earlier_operations で返すので、必要なら operation に指定して呼び直す。',
|
|
@@ -21,6 +21,9 @@ export declare class ServerBoardAccess implements BoardAccess {
|
|
|
21
21
|
snippet: string | undefined;
|
|
22
22
|
}[]>;
|
|
23
23
|
readBoard(slug: string): Promise<BoardState | null>;
|
|
24
|
+
parentOf(slug: string): Promise<string | null | undefined>;
|
|
25
|
+
clearParent(slug: string, parent: string): Promise<void>;
|
|
26
|
+
createPage(slug: string, title: string): Promise<'created' | 'exists' | 'invalid title'>;
|
|
24
27
|
plainText(html: string): Promise<string>;
|
|
25
28
|
apply(slug: string, deltas: HistoryDelta[], summary: string): Promise<ApplyOutcome>;
|
|
26
29
|
ownOps(slug: string): Promise<OpSummary[]>;
|
|
@@ -28,6 +28,17 @@ export class ServerBoardAccess {
|
|
|
28
28
|
async readBoard(slug) {
|
|
29
29
|
return this.page(slug).getBoardState();
|
|
30
30
|
}
|
|
31
|
+
async parentOf(slug) {
|
|
32
|
+
const ref = await this.page(slug).getPageRef();
|
|
33
|
+
return ref === null ? undefined : ref.parent;
|
|
34
|
+
}
|
|
35
|
+
async clearParent(slug, parent) {
|
|
36
|
+
await this.page(slug).clearParent(parent);
|
|
37
|
+
}
|
|
38
|
+
async createPage(slug, title) {
|
|
39
|
+
const result = await this.page(slug).createPage(title);
|
|
40
|
+
return result.ok ? 'created' : result.reason;
|
|
41
|
+
}
|
|
31
42
|
async plainText(html) {
|
|
32
43
|
// ページへのリンクは、書くときと同じ書式([[slug]])で残す
|
|
33
44
|
return (await extractContent(html, { siteOrigin: this.env.SITE_ORIGIN })).text;
|
|
@@ -16,7 +16,7 @@ export interface ApplyInput {
|
|
|
16
16
|
/** 外から来た値をそのまま渡す。検証は applyOps が行う */
|
|
17
17
|
deltas: unknown;
|
|
18
18
|
summary?: string;
|
|
19
|
-
/** ページがまだ作られていなければ、適用せずに断る(MCP
|
|
19
|
+
/** ページがまだ作られていなければ、適用せずに断る(MCP の書き込みのツールは、ページを作らない。作るのは create_page だけ) */
|
|
20
20
|
mustExist?: boolean;
|
|
21
21
|
}
|
|
22
22
|
/** ページがないときの `reason`。Worker は、これを 404 にする */
|