dsh-tiddlywiki 0.7.2 → 0.7.4

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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tiddlywiki",
3
3
  "description": "TiddlyWiki 5 as the DSH persistent knowledge base: tiddlywiki_* agent tools (search with filters/recent/tags/batch/rename + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a quick-note card with draft auto-save & recent-notes picker behind a single knowledge FAB, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.7.2",
4
+ "version": "0.7.4",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -48,7 +48,7 @@
48
48
  "homepage": "https://github.com/bbqisbbq/dsh-tiddlywiki#readme",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "https://github.com/bbqisbbq/dsh-tiddlywiki.git"
51
+ "url": "git+https://github.com/bbqisbbq/dsh-tiddlywiki.git"
52
52
  },
53
53
  "bugs": {
54
54
  "url": "https://github.com/bbqisbbq/dsh-tiddlywiki/issues"
@@ -73,6 +73,16 @@ export interface SessionControllerFace {
73
73
  create(request: { cwd?: string; workspaceId?: string }): Promise<{ sessionId: string }>
74
74
  }
75
75
 
76
+ /**
77
+ * Structural face over the DSH `workspaceRegistry` service (a subset of
78
+ * dsh-workspace). Only what the agent-create route needs is declared; the
79
+ * runtime instance is a real Service, never inspected data.
80
+ */
81
+ export interface WorkspaceRegistryFace {
82
+ /** Resolve or create the workspace owning `path` — idempotent by canonical path. */
83
+ create(path: string, title?: string): Promise<{ id: string; path: string }>
84
+ }
85
+
76
86
  /** Effective UI visibility flags returned by /status (mirror index.ts). */
77
87
  export interface UiDefaultsPublic {
78
88
  showQuickNote: boolean
@@ -91,6 +101,10 @@ export interface RouteDeps {
91
101
  /** Optional DSH sessionController service (agent-send routes only); resolved
92
102
  * lazily per request because it may register after webServer appears. */
93
103
  getSessionController: () => SessionControllerFace | undefined
104
+ /** Optional DSH workspaceRegistry service (agent-create route); resolves a
105
+ * cwd to a real Workspace so new sessions land inside it instead of the
106
+ * ungrouped bucket. */
107
+ getWorkspaceRegistry: () => WorkspaceRegistryFace | undefined
94
108
  /** Whether the one-click send-to-agent feature is enabled (config switch). */
95
109
  sendToAgentEnabled: () => boolean
96
110
  /** Optional shared token that must match `x-send-to-agent-token` when set. */
@@ -334,10 +348,14 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
334
348
  }
335
349
 
336
350
  /**
337
- * POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session,
338
- * optionally inside a workspace path. The picker uses it for "new workspace /
339
- * new session"; the created session's cwd becomes its workspace. The directory
340
- * is materialised so a brand-new workspace actually exists on disk.
351
+ * POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
352
+ * inside a real DSH workspace resolved from the requested path. The picker
353
+ * uses it for "new workspace / new session": the directory is materialised so
354
+ * a brand-new workspace actually exists on disk, the path is resolved to its
355
+ * (idempotent) Workspace, and the session is created with `workspaceId` so it
356
+ * lands under that workspace in the sidebar. Creating with bare `cwd` instead
357
+ * would leave the session in the ungrouped bucket even when its working
358
+ * directory matches an existing workspace path.
341
359
  */
342
360
  const handleAgentCreate = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
343
361
  try {
@@ -361,10 +379,18 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
361
379
  json(res, { ok: false, error: 'session service unavailable' }, 503)
362
380
  return
363
381
  }
382
+ const ws = deps.getWorkspaceRegistry()
364
383
  if (cwd.length > 0) {
365
384
  await mkdir(cwd, { recursive: true })
366
385
  }
367
- const created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
386
+ let created: { sessionId: string }
387
+ if (cwd.length > 0 && ws !== undefined) {
388
+ const workspace = await ws.create(cwd)
389
+ created = await sc.create({ workspaceId: workspace.id })
390
+ json(res, { ok: true, sessionId: created.sessionId, cwd, workspaceId: workspace.id })
391
+ return
392
+ }
393
+ created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
368
394
  json(res, { ok: true, sessionId: created.sessionId, cwd: cwd || null })
369
395
  } catch (err) {
370
396
  json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
package/src/host/tools.ts CHANGED
@@ -18,6 +18,19 @@ import { defineTool } from '../sdk.ts'
18
18
  import type { TiddlyWebClient, Tiddler } from './tw-api.ts'
19
19
  import type { GitFace } from './git.ts'
20
20
 
21
+ /**
22
+ * 约定标签:标记「由 Agent 撰写」的笔记。
23
+ * 新建(title 不存在)时由 tiddlywiki_put / tiddlywiki_batch_put 自动补打;
24
+ * 首页据此把这类笔记单独列在「Agent 区块」并从主标签列表排除。
25
+ */
26
+ export const AGENT_WRITTEN_TAG = 'agent-written'
27
+
28
+ /**
29
+ * 约定标签:标记「Agent 撰写后又经人类编辑」的笔记(双标签分级第二档)。
30
+ * 人类在编辑某篇 Agent 笔记时手动补打,首页把这类笔记归入「Agent + 人工」档。
31
+ */
32
+ export const HUMAN_EDITED_TAG = 'human-edited'
33
+
21
34
  /** Structural tool-registry face (subset of the dsh tools service). */
22
35
  export interface ToolsCtx {
23
36
  tools: { register(tool: unknown): () => void }
@@ -83,6 +96,21 @@ function rewriteRefs(text: string, oldTitle: string, newTitle: string): { text:
83
96
  return { text: out, count }
84
97
  }
85
98
 
99
+ /**
100
+ * Compute the final tags for a write:
101
+ * - NEW tiddler (created by this tool) → auto-append the agent-written tag,
102
+ * unless it is a `$:/` system tiddler (config/language/theme internals).
103
+ * - Existing tiddler → keep the caller's tags as-is (an agent maintaining a
104
+ * human note must NOT silently claim authorship; an already agent-written
105
+ * note keeps its tag because the caller provides the full tag list).
106
+ */
107
+ function finalTagsForWrite(title: string, existing: Tiddler | undefined, tags: string[]): string[] {
108
+ if (existing !== undefined) return tags
109
+ if (title.startsWith('$:/')) return tags
110
+ if (tags.includes(AGENT_WRITTEN_TAG)) return tags
111
+ return [...tags, AGENT_WRITTEN_TAG]
112
+ }
113
+
86
114
  export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<() => void> {
87
115
  const disposers: Array<() => void> = []
88
116
  const register = (tool: unknown): void => { disposers.push(ctx.tools.register(tool)) }
@@ -227,7 +255,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
227
255
  // ── tiddlywiki_put ───────────────────────────────────────────────────────
228
256
  register(defineTool({
229
257
  name: 'tiddlywiki_put',
230
- description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。',
258
+ description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。新建(title 不存在)时自动补打 agent-written 标签标记「由 Agent 撰写」,无需手动添加。',
231
259
  parameters: {
232
260
  title: { type: 'string', description: 'tiddler 标题(精确匹配,覆盖同名)', required: true },
233
261
  text: { type: 'string', description: 'tiddler 全文(wiki 文本)', required: true },
@@ -249,19 +277,21 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
249
277
  execute: async (args: { title: string; text: string; tags?: string[]; fields?: Record<string, unknown> }): Promise<PutResult> => {
250
278
  const wiki = deps.wiki()
251
279
  if (wiki === undefined) throw new Error('TiddlyWiki 服务未运行(tiddlywiki_status 可查)')
280
+ const existing = await wiki.get(args.title).catch(() => undefined)
281
+ const tags = finalTagsForWrite(args.title, existing, Array.isArray(args.tags) ? args.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
252
282
  const tiddler: Tiddler = { title: args.title, text: args.text }
253
- if (Array.isArray(args.tags) && args.tags.length > 0) tiddler.tags = args.tags
283
+ if (tags.length > 0) tiddler.tags = tags
254
284
  if (args.fields !== undefined && typeof args.fields === 'object' && args.fields !== null) Object.assign(tiddler, args.fields)
255
285
  await wiki.put(tiddler)
256
286
  deps.autoCommit()
257
- return { ok: true, title: args.title, tags: args.tags ?? [], fields: args.fields ?? null }
287
+ return { ok: true, title: args.title, tags, fields: args.fields ?? null }
258
288
  },
259
289
  }))
260
290
 
261
291
  // ── tiddlywiki_batch_put ─────────────────────────────────────────────────
262
292
  register(defineTool({
263
293
  name: 'tiddlywiki_batch_put',
264
- description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。',
294
+ description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。新建(title 不存在)的条目会自动补打 agent-written 标签,无需手动添加。',
265
295
  parameters: {
266
296
  items: {
267
297
  type: 'array',
@@ -301,16 +331,15 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
301
331
  for (const item of list) {
302
332
  if (typeof item.title !== 'string' || item.title.length === 0) throw new Error('batch_put: 每条 items 都需要非空 title')
303
333
  if (typeof item.text !== 'string') throw new Error(`batch_put: items「${item.title}」缺少 text`)
304
- if (!overwrite) {
305
- const existing = await wiki.get(item.title).catch(() => undefined)
306
- if (existing !== undefined) {
307
- skipped++
308
- results.push({ title: item.title, written: false, skipped: true })
309
- continue
310
- }
334
+ const existing = await wiki.get(item.title).catch(() => undefined)
335
+ if (!overwrite && existing !== undefined) {
336
+ skipped++
337
+ results.push({ title: item.title, written: false, skipped: true })
338
+ continue
311
339
  }
340
+ const tags = finalTagsForWrite(item.title, existing, Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
312
341
  const tiddler: Tiddler = { title: item.title, text: item.text }
313
- if (Array.isArray(item.tags) && item.tags.length > 0) tiddler.tags = item.tags
342
+ if (tags.length > 0) tiddler.tags = tags
314
343
  if (item.fields !== undefined && typeof item.fields === 'object' && item.fields !== null) Object.assign(tiddler, item.fields)
315
344
  await wiki.put(tiddler)
316
345
  written++
@@ -366,7 +395,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
366
395
  }
367
396
  }
368
397
  }
369
- await wiki.put(cleanTiddler(existing))
398
+ await wiki.put({ ...cleanTiddler(existing), title: newTitle })
370
399
  await wiki.delete(oldTitle)
371
400
  if (refsTiddlers === 0) {
372
401
  warning = '未找到任何其他 tiddler 引用旧标题;如确实需要,可手动补充链接。'
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ import { watch, type FSWatcher } from 'node:fs'
22
22
  import { writeFile } from 'node:fs/promises'
23
23
  import { join } from 'node:path'
24
24
  import { AutoCommitter, GitFace } from './host/git.ts'
25
- import { registerRoutes, type SessionControllerFace, type WebServerFace } from './host/routes.ts'
25
+ import { registerRoutes, type SessionControllerFace, type WebServerFace, type WorkspaceRegistryFace } from './host/routes.ts'
26
26
  import { ConfigStore, deepMerge, type PluginConfigShape } from './host/config.ts'
27
27
  import { registerAdminRoutes, ensureLanguage, resolveTwRoot, type AdminDeps } from './host/admin.ts'
28
28
  import { seedDocNote, DOC_NOTE_TITLE } from './host/seed-notes.ts'
@@ -43,6 +43,7 @@ export { ConfigStore, deepMerge } from './host/config.ts'
43
43
  export { openInTwEditor, registerRoutes } from './host/routes.ts'
44
44
  export { registerAdminRoutes, resolveTwRoot, readWikiInfo, writeWikiInfo, bundledCatalog, ensureLanguage, normalizeThemes } from './host/admin.ts'
45
45
  export { seedDocNote, DOC_NOTE_TITLE, DOC_NOTE_TAG, DOC_NOTE_TEXT } from './host/seed-notes.ts'
46
+ export { registerTiddlywikiTools } from './host/tools.ts'
46
47
  export type { PluginConfigShape } from './host/config.ts'
47
48
  export type { GitStatusView } from './host/git.ts'
48
49
  export type { Tiddler } from './host/tw-api.ts'
@@ -182,7 +183,9 @@ pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep
182
183
 
183
184
  把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
184
185
 
185
- 用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。`
186
+ 用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。
187
+
188
+ **Agent 笔记标签约定**:\`tiddlywiki_put\` / \`tiddlywiki_batch_put\` 新建笔记时,插件会自动补打 \`agent-written\` 标签(标记「由 Agent 撰写」),无需手动添加,也不要手动移除它(除非用户明确要求)。首页会把 Agent 笔记单独列在「Agent 区块」,主标签列表只统计人类笔记。若某篇 Agent 笔记后续被人类编辑过,请在该笔记上补打 \`human-edited\` 标签,首页会把它归入「Agent + 人工」档。覆盖写入已有的(人类)笔记时不会自动加 agent-written,请保持笔记原本的归属。`
186
189
 
187
190
  /**
188
191
  * Mount the host half.
@@ -358,6 +361,8 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
358
361
  // agent routes must work whenever a request actually arrives.
359
362
  const getSessionController = (): SessionControllerFace | undefined =>
360
363
  ctx.get('sessionController') as SessionControllerFace | undefined
364
+ const getWorkspaceRegistry = (): WorkspaceRegistryFace | undefined =>
365
+ ctx.get('workspaceRegistry') as WorkspaceRegistryFace | undefined
361
366
  const disposeRoutes = registerRoutes({ webServer: ws }, {
362
367
  server,
363
368
  getClient: client,
@@ -367,6 +372,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
367
372
  uiDefaults: () => effectiveUi(),
368
373
  getWikiPath: () => wikiPath,
369
374
  getSessionController,
375
+ getWorkspaceRegistry,
370
376
  sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
371
377
  sendToAgentToken: () => {
372
378
  const token = eff().ui?.sendToAgent?.token