dsh-tiddlywiki 0.4.0 → 0.5.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/lib/index.js CHANGED
@@ -275,6 +275,47 @@ var GitFace = class {
275
275
  message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
276
276
  };
277
277
  }
278
+ /** `git fetch` (no remote configured → failure reported by the caller). */
279
+ async fetch(dir) {
280
+ const r = await this.exec(["fetch"], {
281
+ cwd: dir,
282
+ timeout: HEAVY_TIMEOUT_MS
283
+ });
284
+ return r.ok ? {
285
+ ok: true,
286
+ message: r.stdout.trim() || "fetch ok"
287
+ } : {
288
+ ok: false,
289
+ message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
290
+ };
291
+ }
292
+ /**
293
+ * Restore the given files from the freshly fetched remote HEAD (FETCH_HEAD)
294
+ * into the working tree + index — the "keep remote version" half of
295
+ * tiddler-granular conflict resolution. Callers must `git fetch` first.
296
+ */
297
+ async checkoutFetchHead(dir, files) {
298
+ if (files.length === 0) return {
299
+ ok: true,
300
+ message: "no files given"
301
+ };
302
+ const r = await this.exec([
303
+ "checkout",
304
+ "FETCH_HEAD",
305
+ "--",
306
+ ...files
307
+ ], {
308
+ cwd: dir,
309
+ timeout: HEAVY_TIMEOUT_MS
310
+ });
311
+ return r.ok ? {
312
+ ok: true,
313
+ message: `已从远端检出 ${files.length} 个文件`
314
+ } : {
315
+ ok: false,
316
+ message: (r.stderr.trim() || r.stdout.trim()).slice(0, 500)
317
+ };
318
+ }
278
319
  /** Ensure `origin` points at `url` (add or set-url). */
279
320
  async ensureRemote(dir, url) {
280
321
  const cur = await this.exec([
@@ -701,6 +742,11 @@ function json$1(res, payload, status = 200) {
701
742
  function pad(n) {
702
743
  return n < 10 ? `0${n}` : String(n);
703
744
  }
745
+ /** Flat one-line snippet for the recent-notes picker. */
746
+ function snippetOf$1(text, max = 120) {
747
+ const flat = text.replace(/\s+/g, " ").trim();
748
+ return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
749
+ }
704
750
  /** Default note title: `YYYY-MM-DD HH:mm` (design doc D6). */
705
751
  function timestampTitle(date = /* @__PURE__ */ new Date()) {
706
752
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
@@ -867,6 +913,85 @@ function registerRoutes(ctx, deps) {
867
913
  }, 500);
868
914
  }
869
915
  };
916
+ /** Recent non-system tiddlers for the quick-note "最近" picker (newest first). */
917
+ const handleRecent = async (req, res) => {
918
+ const client = deps.getClient();
919
+ if (client === void 0) {
920
+ json$1(res, {
921
+ ok: false,
922
+ error: "wiki service is not running"
923
+ }, 503);
924
+ return;
925
+ }
926
+ try {
927
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
928
+ const limitRaw = Number(url.searchParams.get("limit") ?? 15);
929
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 15;
930
+ json$1(res, {
931
+ ok: true,
932
+ limit,
933
+ items: (await client.recent(limit)).map((t) => ({
934
+ title: t.title,
935
+ tags: t.tags ?? [],
936
+ modified: typeof t.modified === "string" ? t.modified : null,
937
+ snippet: snippetOf$1(t.text ?? "")
938
+ }))
939
+ });
940
+ } catch (err) {
941
+ json$1(res, {
942
+ ok: false,
943
+ error: err instanceof Error ? err.message : String(err)
944
+ }, 500);
945
+ }
946
+ };
947
+ /** Full tiddler for the quick-note "最近" picker (load into the editor). */
948
+ const handleGet = async (req, res) => {
949
+ const client = deps.getClient();
950
+ if (client === void 0) {
951
+ json$1(res, {
952
+ ok: false,
953
+ error: "wiki service is not running"
954
+ }, 503);
955
+ return;
956
+ }
957
+ try {
958
+ const title = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("title") ?? "";
959
+ if (title.length === 0) {
960
+ json$1(res, {
961
+ ok: false,
962
+ error: "missing title"
963
+ }, 400);
964
+ return;
965
+ }
966
+ const t = await client.get(title);
967
+ if (t === void 0) {
968
+ json$1(res, {
969
+ ok: false,
970
+ notFound: true,
971
+ title
972
+ }, 404);
973
+ return;
974
+ }
975
+ const fields = {};
976
+ for (const [k, v] of Object.entries(t)) {
977
+ if (k === "title" || k === "text" || k === "tags") continue;
978
+ fields[k] = v;
979
+ }
980
+ json$1(res, {
981
+ ok: true,
982
+ title: t.title,
983
+ text: t.text ?? "",
984
+ tags: t.tags ?? [],
985
+ type: t.type ?? "text/vnd.tiddlywiki",
986
+ fields
987
+ });
988
+ } catch (err) {
989
+ json$1(res, {
990
+ ok: false,
991
+ error: err instanceof Error ? err.message : String(err)
992
+ }, 500);
993
+ }
994
+ };
870
995
  const handleRestart = async (_req, res) => {
871
996
  try {
872
997
  await deps.server.restart();
@@ -1061,6 +1186,20 @@ function registerRoutes(ctx, deps) {
1061
1186
  handleTags(req, res);
1062
1187
  }
1063
1188
  }),
1189
+ ctx.webServer.register({
1190
+ kind: "exact",
1191
+ path: `${ROUTE_PREFIX}/recent`,
1192
+ handler: (req, res) => {
1193
+ handleRecent(req, res);
1194
+ }
1195
+ }),
1196
+ ctx.webServer.register({
1197
+ kind: "exact",
1198
+ path: `${ROUTE_PREFIX}/get`,
1199
+ handler: (req, res) => {
1200
+ handleGet(req, res);
1201
+ }
1202
+ }),
1064
1203
  ctx.webServer.register({
1065
1204
  kind: "exact",
1066
1205
  path: `${ROUTE_PREFIX}/sync`,
@@ -1542,17 +1681,19 @@ const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
1542
1681
 
1543
1682
  !! 它能做什么
1544
1683
 
1545
- * **5 个 agent 工具**:\`tiddlywiki_search\`(检索)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_git_sync\`(git 同步)。
1684
+ * **10 个 agent 工具**:\`tiddlywiki_search\`(检索,支持 tags/since/type/limit 过滤)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_batch_put\`(批量写)/ \`tiddlywiki_rename\`(重命名+同步引用)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_recent\`(最近修改)/ \`tiddlywiki_list_tags\`(标签清单)/ \`tiddlywiki_git_sync\`(git 同步)/ \`tiddlywiki_git_resolve\`(冲突按 tiddler 二选一)。
1546
1685
  * **TW 编辑器面板**:侧边栏「TiddlyWiki」按钮 → 在界面中央打开完整版 TW 编辑器。
1547
- * **快速笔记**:右下角悬浮「📝 快速笔记」写随手记(可多选/自动补全 tag),\`Ctrl+Enter\` 保存;点「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
1686
+ * **快速笔记**:右下角「知识库」悬浮按钮 → 「📝 快速笔记」写随手记(Markdown 高亮、文件上传、多选/自动补全 tag,草稿自动保存到本地),\`Ctrl+Enter\` 保存;「🕘 最近」可一键载入旧笔记继续编辑;「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
1687
+ * **一键同步**:「知识库」按钮 → 「🔁 同步」一键 pull → commit → push,按钮上的状态点实时反映 git 状态。
1548
1688
  * **git 同步**:写入自动防抖 commit(默认 60 秒);手动 \`tiddlywiki_git_sync action=sync\` 做 pull → commit → push。
1549
- * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置(含「快速笔记」显示开关、面板「状态/重载」悬浮按钮开关)。
1689
+ * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置(含「知识库」按钮相关显示开关)。
1550
1690
 
1551
1691
  !! 知识库纪律(三条)
1552
1692
 
1553
1693
  1. 开工先 \`tiddlywiki_git_sync action=pull\`(rebase + autostash,真冲突会自动 abort 并报文件)。
1554
- 2. 收工 \`tiddlywiki_git_sync action=sync\`。
1555
- 3. 插件自动 commit 兜底,手动 sync 用于需要主动推送的场合。
1694
+ 2. 冲突后:\`tiddlywiki_git_resolve files=[冲突文件] strategy=keep-local|keep-remote\` 按 tiddler 二选一解决,再重新 sync。
1695
+ 3. 收工 \`tiddlywiki_git_sync action=sync\`。
1696
+ 4. 插件自动 commit 兜底,手动 sync 用于需要主动推送的场合。
1556
1697
 
1557
1698
  !! 主题与语言
1558
1699
 
@@ -1689,20 +1830,82 @@ var TiddlyWebClient = class {
1689
1830
  /**
1690
1831
  * Search non-system tiddlers: one request (default listing with text) plus
1691
1832
  * local case-insensitive substring matching on title + text, optional exact
1692
- * tag, capped at `limit`. Robust against the server's external-filter 403.
1833
+ * tags (AND), a `since` modified-time floor, an exact `type`, capped at
1834
+ * `limit`. Robust against the server's external-filter 403.
1693
1835
  */
1694
- async search(query, tag, limit = 30) {
1836
+ async search(query, options = {}) {
1695
1837
  const items = await this.list(void 0, true);
1696
1838
  const needle = query.toLowerCase();
1697
- return items.filter((t) => {
1839
+ const sinceTime = parseSince(options.since);
1840
+ const wantedTags = [...options.tags ?? [], options.tag].filter((t) => typeof t === "string" && t.trim().length > 0);
1841
+ const limit = options.limit ?? 30;
1842
+ const matched = items.filter((t) => {
1843
+ if (t.title.startsWith("$:/")) return false;
1698
1844
  if (!t.title.toLowerCase().includes(needle) && !(t.text ?? "").toLowerCase().includes(needle)) return false;
1699
- if (tag !== void 0 && tag.length > 0) {
1700
- if (!(t.tags ?? []).some((t2) => t2.toLowerCase() === tag.toLowerCase())) return false;
1845
+ if (sinceTime !== void 0) {
1846
+ const modified = typeof t.modified === "string" ? new Date(t.modified).getTime() : NaN;
1847
+ if (Number.isNaN(modified) || modified < sinceTime) return false;
1848
+ }
1849
+ if (options.type !== void 0 && options.type.length > 0 && (t.type ?? "text/vnd.tiddlywiki") !== options.type) return false;
1850
+ if (wantedTags.length > 0) {
1851
+ const tags = (t.tags ?? []).map((tag) => tag.toLowerCase());
1852
+ if (!wantedTags.every((w) => tags.includes(w.toLowerCase()))) return false;
1853
+ }
1854
+ return true;
1855
+ });
1856
+ return {
1857
+ items: matched.slice(0, limit),
1858
+ total: matched.length
1859
+ };
1860
+ }
1861
+ /**
1862
+ * List the most recently modified NON-SYSTEM tiddlers, newest first
1863
+ * (missing/modified-less tiddlers sort to the tail). `since` keeps only
1864
+ * tiddlers modified at/after that instant.
1865
+ */
1866
+ async recent(limit = 15, since) {
1867
+ const items = await this.list(void 0, true);
1868
+ const sinceTime = parseSince(since);
1869
+ const filtered = items.filter((t) => {
1870
+ if (t.title.startsWith("$:/")) return false;
1871
+ if (sinceTime !== void 0) {
1872
+ const modified = typeof t.modified === "string" ? new Date(t.modified).getTime() : NaN;
1873
+ if (Number.isNaN(modified) || modified < sinceTime) return false;
1701
1874
  }
1702
1875
  return true;
1703
- }).slice(0, limit);
1876
+ });
1877
+ filtered.sort((a, b) => {
1878
+ const am = typeof a.modified === "string" ? new Date(a.modified).getTime() : 0;
1879
+ return (typeof b.modified === "string" ? new Date(b.modified).getTime() : 0) - am;
1880
+ });
1881
+ return filtered.slice(0, Math.max(1, Math.min(limit, 200)));
1882
+ }
1883
+ /**
1884
+ * Distinct non-system tags with their tiddler counts, most-used first then
1885
+ * zh-locale. One skinny listing request (no text payloads).
1886
+ */
1887
+ async listTags() {
1888
+ const items = await this.list(void 0, false);
1889
+ const map = /* @__PURE__ */ new Map();
1890
+ for (const t of items) {
1891
+ if (t.title.startsWith("$:/")) continue;
1892
+ for (const tag of t.tags ?? []) {
1893
+ if (tag.startsWith("$:/")) continue;
1894
+ map.set(tag, (map.get(tag) ?? 0) + 1);
1895
+ }
1896
+ }
1897
+ return [...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh")).map(([tag, count]) => ({
1898
+ tag,
1899
+ count
1900
+ }));
1704
1901
  }
1705
1902
  };
1903
+ /** Parse a `since` value into an epoch ms, or undefined when absent/invalid. */
1904
+ function parseSince(since) {
1905
+ if (typeof since !== "string" || since.trim().length === 0) return void 0;
1906
+ const ms = new Date(since.trim()).getTime();
1907
+ return Number.isNaN(ms) ? void 0 : ms;
1908
+ }
1706
1909
  //#endregion
1707
1910
  //#region src/sdk.ts
1708
1911
  /**
@@ -1738,7 +1941,11 @@ function compileValue(spec) {
1738
1941
  const objectSpec = spec;
1739
1942
  node.type = "object";
1740
1943
  node.additionalProperties = objectSpec.additionalProperties;
1741
- if (objectSpec.properties !== void 0) node.properties = compilePropertyMap(objectSpec.properties).properties;
1944
+ if (objectSpec.properties !== void 0) {
1945
+ const compiled = compilePropertyMap(objectSpec.properties);
1946
+ node.properties = compiled.properties;
1947
+ if (compiled.required !== void 0) node.required = compiled.required;
1948
+ }
1742
1949
  return node;
1743
1950
  }
1744
1951
  if (type === "array") {
@@ -1842,10 +2049,14 @@ function defineTool(options) {
1842
2049
  //#endregion
1843
2050
  //#region src/host/tools.ts
1844
2051
  /**
1845
- * The five `tiddlywiki_*` agent tools (design doc §11, D8) plus the extension
2052
+ * The `tiddlywiki_*` agent tools (design doc §11, D8) plus the extension
1846
2053
  * point: `registerTiddlywikiTools(ctx, deps)` registers tools list-style, so a
1847
2054
  * new tool is just one more `defineTool` in the array — index.ts never changes.
1848
2055
  *
2056
+ * Toolset (v0.5):
2057
+ * search / get / put / batch_put / rename / delete / recent / list_tags /
2058
+ * git_sync / git_resolve
2059
+ *
1849
2060
  * RENDER CONTRACT (design doc §4.3): the registry feeds `output.render(args,
1850
2061
  * value)` into the loop — the model sees ONLY the rendered text, never the raw
1851
2062
  * JSON `value`. Every render must carry the complete facts an agent needs to
@@ -1866,6 +2077,42 @@ function pickFields(t) {
1866
2077
  }
1867
2078
  return out;
1868
2079
  }
2080
+ /** A put-ready copy of a tiddler (no created/modified, tags as array). */
2081
+ function cleanTiddler(t) {
2082
+ const out = {
2083
+ title: t.title,
2084
+ text: t.text ?? "",
2085
+ tags: t.tags ?? []
2086
+ };
2087
+ if (typeof t.type === "string") out.type = t.type;
2088
+ for (const [k, v] of Object.entries(t)) {
2089
+ if (k === "title" || k === "text" || k === "tags" || k === "type" || k === "created" || k === "modified" || k === "fields") continue;
2090
+ out[k] = v;
2091
+ }
2092
+ return out;
2093
+ }
2094
+ /**
2095
+ * Rewrite TiddlyWiki references to a title inside wiki text: `[[Title]]`,
2096
+ * `[[display|Title]]`, `{{Title}}` → the new title. Best-effort link/text
2097
+ * migration for tiddlywiki_rename; returns the rewritten text + hit count.
2098
+ */
2099
+ function rewriteRefs(text, oldTitle, newTitle) {
2100
+ if (oldTitle.length === 0) return {
2101
+ text,
2102
+ count: 0
2103
+ };
2104
+ const escaped = oldTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2105
+ const re = new RegExp(`(\\[\\[[^\\]|]*\\|)${escaped}(\\]\\])|(\\[\\[)${escaped}(\\]\\])|(\\{\\{)${escaped}(\\}\\})`, "g");
2106
+ let count = 0;
2107
+ return {
2108
+ text: text.replace(re, (...args) => {
2109
+ const p = args;
2110
+ count++;
2111
+ return `${p[1] ?? p[3] ?? p[5] ?? ""}${newTitle}${p[2] ?? p[4] ?? p[6] ?? ""}`;
2112
+ }),
2113
+ count
2114
+ };
2115
+ }
1869
2116
  function registerTiddlywikiTools(ctx, deps) {
1870
2117
  const disposers = [];
1871
2118
  const register = (tool) => {
@@ -1873,29 +2120,51 @@ function registerTiddlywikiTools(ctx, deps) {
1873
2120
  };
1874
2121
  register(defineTool({
1875
2122
  name: "tiddlywiki_search",
1876
- description: "检索 TiddlyWiki 持久知识库:按关键词(可选 tag 精确匹配)搜索非系统 tiddler,返回标题、标签与摘要片段。",
2123
+ description: "检索 TiddlyWiki 持久知识库:按关键词(可选 tags 数组 / since 修改时间 / type / limit)搜索非系统 tiddler,返回标题、标签、修改时间与摘要片段。",
1877
2124
  parameters: {
1878
2125
  query: {
1879
2126
  type: "string",
1880
2127
  description: "搜索关键词(大小写不敏感,子串匹配)",
1881
2128
  required: true
1882
2129
  },
2130
+ tags: {
2131
+ type: "array",
2132
+ items: { type: "string" },
2133
+ description: "可选:要求同时包含的标签(AND)"
2134
+ },
1883
2135
  tag: {
1884
2136
  type: "string",
1885
- description: "可选:只返回带该 tag 的 tiddler"
2137
+ description: "可选:单个精确标签(与 tags 同为 AND)"
2138
+ },
2139
+ since: {
2140
+ type: "string",
2141
+ description: "可选:ISO 时间(如 2026-09-01 或 2026-09-01T00:00:00Z),只返回修改时间不早于它的 tiddler"
2142
+ },
2143
+ type: {
2144
+ type: "string",
2145
+ description: "可选:精确 tiddler 类型(默认 text/vnd.tiddlywiki)"
2146
+ },
2147
+ limit: {
2148
+ type: "integer",
2149
+ description: "可选:返回条数上限(默认 30,最大 200)"
1886
2150
  }
1887
2151
  },
1888
2152
  output: {
1889
2153
  schema: { type: "json" },
1890
2154
  render: (_args, value) => {
1891
- const lines = [`TiddlyWiki 搜索「${value.query}」${value.tag !== null ? ` (tag=${value.tag})` : ""}:命中 ${value.count} 条。`];
2155
+ const filters = [];
2156
+ if (value.tags.length > 0) filters.push(`tags=${value.tags.join(",")}`);
2157
+ if (value.since !== null) filters.push(`since=${value.since}`);
2158
+ if (value.type !== null) filters.push(`type=${value.type}`);
2159
+ const lines = [`TiddlyWiki 搜索「${value.query}」${filters.length > 0 ? ` (${filters.join(" · ")})` : ""}:命中 ${value.total} 条。`];
1892
2160
  if (value.results.length === 0) lines.push("没有匹配的 tiddler。");
1893
2161
  for (const r of value.results) {
1894
2162
  const tags = r.tags.length > 0 ? ` [${r.tags.join(", ")}]` : "";
1895
- lines.push(`- ${r.title}${tags}`);
2163
+ const modified = r.modified !== null ? ` (${r.modified})` : "";
2164
+ lines.push(`- ${r.title}${tags}${modified}`);
1896
2165
  if (r.snippet.length > 0) lines.push(` ${r.snippet}`);
1897
2166
  }
1898
- if (value.count > value.results.length) lines.push(`(另有 ${value.count - value.results.length} 条未展开,可用 tiddlywiki_get 读取具体标题)`);
2167
+ if (value.total > value.results.length) lines.push(`(另有 ${value.total - value.results.length} 条未展开,可用 tiddlywiki_get 读取具体标题,或提高 limit)`);
1899
2168
  return [{
1900
2169
  type: "text",
1901
2170
  text: lines.join("\n")
@@ -1905,19 +2174,101 @@ function registerTiddlywikiTools(ctx, deps) {
1905
2174
  execute: async (args) => {
1906
2175
  const wiki = deps.wiki();
1907
2176
  if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
1908
- const results = await wiki.search(args.query, args.tag);
2177
+ const { items, total } = await wiki.search(args.query, {
2178
+ tags: args.tags,
2179
+ tag: args.tag,
2180
+ since: args.since,
2181
+ type: args.type,
2182
+ limit: args.limit
2183
+ });
1909
2184
  return {
1910
2185
  query: args.query,
1911
- tag: args.tag ?? null,
1912
- count: results.length,
1913
- results: results.map((t) => ({
2186
+ tags: args.tags ?? [],
2187
+ since: args.since ?? null,
2188
+ type: args.type ?? null,
2189
+ total,
2190
+ results: items.map((t) => ({
2191
+ title: t.title,
2192
+ tags: t.tags ?? [],
2193
+ modified: typeof t.modified === "string" ? t.modified : null,
2194
+ snippet: snippetOf(t.text ?? "")
2195
+ }))
2196
+ };
2197
+ }
2198
+ }));
2199
+ register(defineTool({
2200
+ name: "tiddlywiki_recent",
2201
+ description: "查看 TiddlyWiki 知识库最近修改的笔记(按修改时间倒序,排除系统 tiddler),返回标题、标签、修改时间与摘要。适合开工时快速了解近期动态。",
2202
+ parameters: {
2203
+ limit: {
2204
+ type: "integer",
2205
+ description: "可选:返回条数(默认 15,最大 200)"
2206
+ },
2207
+ since: {
2208
+ type: "string",
2209
+ description: "可选:只返回修改时间不早于该 ISO 时间的 tiddler"
2210
+ }
2211
+ },
2212
+ output: {
2213
+ schema: { type: "json" },
2214
+ render: (_args, value) => {
2215
+ const lines = [`TiddlyWiki 最近修改(最近 ${value.results.length} 条${value.since !== null ? `,since=${value.since}` : ""}):`];
2216
+ if (value.results.length === 0) lines.push("暂无笔记。");
2217
+ for (const r of value.results) {
2218
+ const tags = r.tags.length > 0 ? ` [${r.tags.join(", ")}]` : "";
2219
+ lines.push(`- ${r.title}${tags} (${r.modified ?? "?"})`);
2220
+ if (r.snippet.length > 0) lines.push(` ${r.snippet}`);
2221
+ }
2222
+ return [{
2223
+ type: "text",
2224
+ text: lines.join("\n")
2225
+ }];
2226
+ }
2227
+ },
2228
+ execute: async (args) => {
2229
+ const wiki = deps.wiki();
2230
+ if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
2231
+ const items = await wiki.recent(args.limit ?? 15, args.since);
2232
+ return {
2233
+ since: args.since ?? null,
2234
+ results: items.map((t) => ({
1914
2235
  title: t.title,
1915
2236
  tags: t.tags ?? [],
2237
+ modified: typeof t.modified === "string" ? t.modified : null,
1916
2238
  snippet: snippetOf(t.text ?? "")
1917
2239
  }))
1918
2240
  };
1919
2241
  }
1920
2242
  }));
2243
+ register(defineTool({
2244
+ name: "tiddlywiki_list_tags",
2245
+ description: "列出 TiddlyWiki 知识库现有的非系统标签及各自计数(按使用次数降序),方便决定给笔记打什么 tag。",
2246
+ parameters: {},
2247
+ output: {
2248
+ schema: { type: "json" },
2249
+ render: (_args, value) => {
2250
+ if (value.tags.length === 0) return [{
2251
+ type: "text",
2252
+ text: "知识库暂无标签。"
2253
+ }];
2254
+ const lines = [`现有标签(${value.tags.length} 个,按使用次数降序):`];
2255
+ for (const t of value.tags) lines.push(`- ${t.tag} × ${t.count}`);
2256
+ return [{
2257
+ type: "text",
2258
+ text: lines.join("\n")
2259
+ }];
2260
+ }
2261
+ },
2262
+ execute: async () => {
2263
+ const wiki = deps.wiki();
2264
+ if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
2265
+ const tags = await wiki.listTags();
2266
+ return {
2267
+ count: tags.length,
2268
+ tags
2269
+ };
2270
+ }
2271
+ }));
1921
2272
  register(defineTool({
1922
2273
  name: "tiddlywiki_get",
1923
2274
  description: "读取一个 TiddlyWiki tiddler 的完整内容(标题、全文、标签、自定义字段)。",
@@ -1935,6 +2286,7 @@ function registerTiddlywikiTools(ctx, deps) {
1935
2286
  }];
1936
2287
  const lines = [`tiddler「${value.title}」`];
1937
2288
  if (value.tags.length > 0) lines.push(`标签: ${value.tags.join(", ")}`);
2289
+ if (value.modified !== null) lines.push(`修改: ${value.modified}`);
1938
2290
  const fields = Object.entries(value.fields);
1939
2291
  if (fields.length > 0) lines.push(`字段: ${fields.map(([k, v]) => `${k}=${String(v)}`).join(", ")}`);
1940
2292
  lines.push("--- 全文 ---");
@@ -1954,14 +2306,16 @@ function registerTiddlywikiTools(ctx, deps) {
1954
2306
  title: args.title,
1955
2307
  text: "",
1956
2308
  tags: [],
1957
- fields: {}
2309
+ fields: {},
2310
+ modified: null
1958
2311
  };
1959
2312
  return {
1960
2313
  notFound: false,
1961
2314
  title: t.title,
1962
2315
  text: t.text ?? "",
1963
2316
  tags: t.tags ?? [],
1964
- fields: pickFields(t)
2317
+ fields: pickFields(t),
2318
+ modified: typeof t.modified === "string" ? t.modified : null
1965
2319
  };
1966
2320
  }
1967
2321
  }));
@@ -2023,6 +2377,186 @@ function registerTiddlywikiTools(ctx, deps) {
2023
2377
  };
2024
2378
  }
2025
2379
  }));
2380
+ register(defineTool({
2381
+ name: "tiddlywiki_batch_put",
2382
+ description: "批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。",
2383
+ parameters: {
2384
+ items: {
2385
+ type: "array",
2386
+ items: {
2387
+ type: "object",
2388
+ additionalProperties: true,
2389
+ description: "要写入的 tiddler 数组",
2390
+ properties: {
2391
+ title: {
2392
+ type: "string",
2393
+ description: "标题(精确匹配,覆盖同名)",
2394
+ required: true
2395
+ },
2396
+ text: {
2397
+ type: "string",
2398
+ description: "全文(wiki 文本)",
2399
+ required: true
2400
+ },
2401
+ tags: {
2402
+ type: "array",
2403
+ items: { type: "string" },
2404
+ description: "标签数组(可选)"
2405
+ },
2406
+ fields: {
2407
+ type: "json",
2408
+ description: "附加自定义字段(可选)"
2409
+ }
2410
+ }
2411
+ }
2412
+ },
2413
+ overwrite: {
2414
+ type: "boolean",
2415
+ description: "可选:true=覆盖同名(默认),false=跳过已存在的标题"
2416
+ }
2417
+ },
2418
+ output: {
2419
+ schema: { type: "json" },
2420
+ render: (_args, value) => {
2421
+ const lines = [`批量写入完成:成功 ${value.written},跳过 ${value.skipped},共 ${value.items.length} 条。`];
2422
+ for (const r of value.items) lines.push(`- ${r.title}:${r.written ? "已写入" : "已跳过(存在)"}`);
2423
+ return [{
2424
+ type: "text",
2425
+ text: lines.join("\n")
2426
+ }];
2427
+ }
2428
+ },
2429
+ execute: async (args) => {
2430
+ const wiki = deps.wiki();
2431
+ if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
2432
+ const list = Array.isArray(args.items) ? args.items : [];
2433
+ if (list.length === 0) return {
2434
+ ok: true,
2435
+ written: 0,
2436
+ skipped: 0,
2437
+ items: []
2438
+ };
2439
+ const overwrite = args.overwrite !== false;
2440
+ const results = [];
2441
+ let written = 0;
2442
+ let skipped = 0;
2443
+ for (const item of list) {
2444
+ if (typeof item.title !== "string" || item.title.length === 0) throw new Error("batch_put: 每条 items 都需要非空 title");
2445
+ if (typeof item.text !== "string") throw new Error(`batch_put: items「${item.title}」缺少 text`);
2446
+ if (!overwrite) {
2447
+ if (await wiki.get(item.title).catch(() => void 0) !== void 0) {
2448
+ skipped++;
2449
+ results.push({
2450
+ title: item.title,
2451
+ written: false,
2452
+ skipped: true
2453
+ });
2454
+ continue;
2455
+ }
2456
+ }
2457
+ const tiddler = {
2458
+ title: item.title,
2459
+ text: item.text
2460
+ };
2461
+ if (Array.isArray(item.tags) && item.tags.length > 0) tiddler.tags = item.tags;
2462
+ if (item.fields !== void 0 && typeof item.fields === "object" && item.fields !== null) Object.assign(tiddler, item.fields);
2463
+ await wiki.put(tiddler);
2464
+ written++;
2465
+ results.push({
2466
+ title: item.title,
2467
+ written: true,
2468
+ skipped: false
2469
+ });
2470
+ }
2471
+ deps.autoCommit();
2472
+ return {
2473
+ ok: true,
2474
+ written,
2475
+ skipped,
2476
+ items: results
2477
+ };
2478
+ }
2479
+ }));
2480
+ register(defineTool({
2481
+ name: "tiddlywiki_rename",
2482
+ description: "重命名一个 TiddlyWiki tiddler:把旧标题的内容复制到新标题、删除旧标题,并可选地更新其他 tiddler 里的 [[旧标题]] / {{旧标题}} 引用(最佳努力)。",
2483
+ parameters: {
2484
+ oldTitle: {
2485
+ type: "string",
2486
+ description: "当前标题",
2487
+ required: true
2488
+ },
2489
+ newTitle: {
2490
+ type: "string",
2491
+ description: "新标题",
2492
+ required: true
2493
+ },
2494
+ updateRefs: {
2495
+ type: "boolean",
2496
+ description: "可选:是否同步更新其他 tiddler 里的引用(默认 true)"
2497
+ }
2498
+ },
2499
+ output: {
2500
+ schema: { type: "json" },
2501
+ render: (_args, value) => {
2502
+ const lines = [`已重命名「${value.from}」→「${value.to}」`];
2503
+ lines.push(`更新了 ${value.refsUpdated} 处引用(${value.refsTiddlers} 个 tiddler)`);
2504
+ if (value.warning !== void 0) lines.push(`注意: ${value.warning}`);
2505
+ return [{
2506
+ type: "text",
2507
+ text: lines.join("\n")
2508
+ }];
2509
+ }
2510
+ },
2511
+ execute: async (args) => {
2512
+ const wiki = deps.wiki();
2513
+ if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
2514
+ const { oldTitle, newTitle } = args;
2515
+ if (oldTitle === newTitle) return {
2516
+ ok: true,
2517
+ from: oldTitle,
2518
+ to: newTitle,
2519
+ refsUpdated: 0,
2520
+ refsTiddlers: 0
2521
+ };
2522
+ const existing = await wiki.get(oldTitle);
2523
+ if (existing === void 0) throw new Error(`tiddler「${oldTitle}」不存在`);
2524
+ if (await wiki.get(newTitle) !== void 0) throw new Error(`新标题「${newTitle}」已存在(可先用 tiddlywiki_delete 删除)`);
2525
+ let refsUpdated = 0;
2526
+ let refsTiddlers = 0;
2527
+ let warning;
2528
+ if (args.updateRefs !== false) {
2529
+ const all = await wiki.list(void 0, true);
2530
+ for (const t of all) {
2531
+ if (t.title === oldTitle || t.title === newTitle) continue;
2532
+ if (t.title.startsWith("$:/")) continue;
2533
+ const text = t.text ?? "";
2534
+ if (text.length === 0) continue;
2535
+ const rewritten = rewriteRefs(text, oldTitle, newTitle);
2536
+ if (rewritten.count > 0) {
2537
+ await wiki.put({
2538
+ ...cleanTiddler(t),
2539
+ text: rewritten.text
2540
+ });
2541
+ refsUpdated += rewritten.count;
2542
+ refsTiddlers++;
2543
+ }
2544
+ }
2545
+ }
2546
+ await wiki.put(cleanTiddler(existing));
2547
+ await wiki.delete(oldTitle);
2548
+ if (refsTiddlers === 0) warning = "未找到任何其他 tiddler 引用旧标题;如确实需要,可手动补充链接。";
2549
+ deps.autoCommit();
2550
+ return {
2551
+ ok: true,
2552
+ from: oldTitle,
2553
+ to: newTitle,
2554
+ refsUpdated,
2555
+ refsTiddlers,
2556
+ ...warning !== void 0 ? { warning } : {}
2557
+ };
2558
+ }
2559
+ }));
2026
2560
  register(defineTool({
2027
2561
  name: "tiddlywiki_delete",
2028
2562
  description: "删除一个 TiddlyWiki tiddler(不存在时是幂等空操作)。删除后触发自动 commit。",
@@ -2132,6 +2666,98 @@ function registerTiddlywikiTools(ctx, deps) {
2132
2666
  }
2133
2667
  }
2134
2668
  }));
2669
+ register(defineTool({
2670
+ name: "tiddlywiki_git_resolve",
2671
+ description: "在 tiddlywiki_git_sync action=pull 冲突(已 abort)后,按 tiddler 二选一解决:keep-local 保留本地版本;keep-remote 用 git fetch 拉取远端并检出远端版本(需已配置 git.remote);list 仅报告当前状态。解决后建议重新 pull/sync 整合其余改动。",
2672
+ parameters: {
2673
+ strategy: {
2674
+ type: "string",
2675
+ enum: [
2676
+ "keep-local",
2677
+ "keep-remote",
2678
+ "list"
2679
+ ],
2680
+ description: "keep-local=保留本地;keep-remote=改用远端版本;list=仅报告当前 git 状态",
2681
+ required: true
2682
+ },
2683
+ files: {
2684
+ type: "array",
2685
+ items: { type: "string" },
2686
+ description: "冲突文件名数组(来自 pull 返回的 conflictFiles;list 时忽略)"
2687
+ }
2688
+ },
2689
+ output: {
2690
+ schema: { type: "json" },
2691
+ render: (_args, value) => {
2692
+ const lines = [`git resolve ${value.action}: ${value.ok ? "成功" : "失败"}`];
2693
+ lines.push(` ${value.message}`);
2694
+ if (value.files !== void 0 && value.files.length > 0) lines.push(`涉及文件: ${value.files.join(", ")}`);
2695
+ if (value.commit !== void 0) lines.push(`本地 commit: ${value.commit}`);
2696
+ if (value.status !== void 0) {
2697
+ const s = value.status;
2698
+ const bits = [`分支 ${s.branch}`];
2699
+ if (s.ahead !== void 0) bits.push(`领先 ${s.ahead}`);
2700
+ if (s.behind !== void 0) bits.push(`落后 ${s.behind}`);
2701
+ if (s.dirty) bits.push(`工作区有 ${s.dirtyFiles.length} 个未提交改动`);
2702
+ if (s.lastCommit !== void 0) bits.push(`最近提交 ${s.lastCommit}`);
2703
+ lines.push(`状态: ${bits.join(" · ")}`);
2704
+ }
2705
+ return [{
2706
+ type: "text",
2707
+ text: lines.join("\n")
2708
+ }];
2709
+ }
2710
+ },
2711
+ execute: async (args) => {
2712
+ const dir = deps.wikiPath();
2713
+ if (args.strategy === "list") return {
2714
+ ok: true,
2715
+ action: "list",
2716
+ message: "当前仓库状态(pull 冲突已 abort,工作区即本地版本,不会残留未合并状态)",
2717
+ status: await deps.git.status(dir)
2718
+ };
2719
+ const files = (args.files ?? []).filter((f) => typeof f === "string" && f.length > 0);
2720
+ if (files.length === 0) return {
2721
+ ok: false,
2722
+ action: args.strategy,
2723
+ message: "请提供 conflictFiles(来自 pull 返回)",
2724
+ hint: "可先执行 tiddlywiki_git_sync action=pull 查看冲突文件"
2725
+ };
2726
+ if (args.strategy === "keep-local") {
2727
+ const status = await deps.git.status(dir);
2728
+ return {
2729
+ ok: true,
2730
+ action: "keep-local",
2731
+ message: `已保留本地版本(${files.length} 个文件;abort 后本地即为工作区内容)。建议重新 tiddlywiki_git_sync action=sync 整合远端其余改动。`,
2732
+ files,
2733
+ status
2734
+ };
2735
+ }
2736
+ const fetched = await deps.git.fetch(dir);
2737
+ if (!fetched.ok) return {
2738
+ ok: false,
2739
+ action: "keep-remote",
2740
+ message: `fetch 失败(可能未配置 git.remote):${fetched.message}`
2741
+ };
2742
+ const checked = await deps.git.checkoutFetchHead(dir, files);
2743
+ if (!checked.ok) return {
2744
+ ok: false,
2745
+ action: "keep-remote",
2746
+ message: `从远端检出失败:${checked.message}`
2747
+ };
2748
+ const committed = await deps.git.commit(dir, `resolve conflict (keep remote) ${(/* @__PURE__ */ new Date()).toISOString()}`);
2749
+ deps.autoCommit();
2750
+ const status = await deps.git.status(dir);
2751
+ return {
2752
+ ok: true,
2753
+ action: "keep-remote",
2754
+ message: `已把 ${files.length} 个冲突文件改为远端版本并提交。建议继续 tiddlywiki_git_sync action=sync 完成整合与推送。`,
2755
+ files,
2756
+ commit: committed.message,
2757
+ status
2758
+ };
2759
+ }
2760
+ }));
2135
2761
  return disposers;
2136
2762
  }
2137
2763
  function renderSync(value) {
@@ -2140,7 +2766,7 @@ function renderSync(value) {
2140
2766
  if (value.conflictFiles !== void 0 && value.conflictFiles.length > 0) {
2141
2767
  lines.push(`冲突文件(rebase 已 abort,勿自动覆盖):`);
2142
2768
  for (const f of value.conflictFiles) lines.push(` - ${f}`);
2143
- lines.push("处理方式:git checkout --ours <file> 保留本地,或人工编辑后 git add + git rebase --continue;也可以直接让用户处理。");
2769
+ lines.push("处理方式:用 tiddlywiki_git_resolve files=[以上文件] strategy=keep-local|keep-remote 按 tiddler 二选一解决,再重新 sync;也可以直接让用户处理。");
2144
2770
  }
2145
2771
  if (value.commit !== void 0) lines.push(`本地 commit: ${value.commit}`);
2146
2772
  if (value.push !== void 0) lines.push(`远端 push: ${value.push}`);
@@ -2248,14 +2874,17 @@ const PROMPT_TEXT = `## TiddlyWiki 持久知识库
2248
2874
 
2249
2875
  本机有一个 TiddlyWiki 5 持久知识库(wiki 文件夹即 git 仓库)。你可以用工具读写 tiddler:
2250
2876
 
2251
- - \`tiddlywiki_search\`(query, tag?)检索;\`tiddlywiki_get\`(title)读全文;\`tiddlywiki_put\`(title, text, tags?, fields?)写/覆盖;\`tiddlywiki_delete\`(title)删除。
2252
- - \`tiddlywiki_git_sync\`(pull|push|sync)做 git 同步。
2877
+ - \`tiddlywiki_search\`(query 必填;可选 tags[]/tag、since 修改时间、type、limit)检索;\`tiddlywiki_get\`(title)读全文;\`tiddlywiki_put\`(title, text, tags?, fields?)写/覆盖;\`tiddlywiki_batch_put\`(items[])批量写;\`tiddlywiki_rename\`(oldTitle, newTitle, updateRefs?)重命名并尽量同步引用;\`tiddlywiki_delete\`(title)删除。
2878
+ - \`tiddlywiki_recent\`(limit?, since?)看最近修改的笔记;\`tiddlywiki_list_tags\` 看现有 tag 及计数。
2879
+ - \`tiddlywiki_git_sync\`(pull|push|sync)做 git 同步;\`tiddlywiki_git_resolve\`(files, strategy=keep-local|keep-remote|list)在 pull 冲突后按 tiddler 二选一解决。
2253
2880
 
2254
2881
  知识库同步纪律(三条):
2255
2882
  1. 开工先 pull:\`tiddlywiki_git_sync action=pull\`(rebase + autostash;真冲突会自动 abort 并报冲突文件)。
2256
2883
  2. 收工 commit + push:\`tiddlywiki_git_sync action=sync\`(pull → commit → push)。
2257
2884
  3. 插件会自动防抖 commit(默认 60s),手动同步用上面的工具。
2258
2885
 
2886
+ pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep-local\`(保留本地)或 \`strategy=keep-remote\`(改用远端版本),再重新 pull/sync 整合其余改动。
2887
+
2259
2888
  把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
2260
2889
 
2261
2890
  用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。`;
@@ -2427,8 +3056,13 @@ function apply(ctx, rawConfig = {}) {
2427
3056
  };
2428
3057
  });
2429
3058
  ctx.effect(() => () => {
2430
- disposeAll();
2431
- server.stop();
3059
+ (async () => {
3060
+ try {
3061
+ await committer?.flush();
3062
+ } catch {}
3063
+ disposeAll();
3064
+ await server.stop();
3065
+ })();
2432
3066
  }, "dsh-tiddlywiki: host teardown");
2433
3067
  }
2434
3068
  //#endregion