dsh-tiddlywiki 0.6.0 → 0.7.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/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.6.0",
4
+ "version": "0.7.2",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -27,6 +27,13 @@ export interface PluginConfigShape {
27
27
  showPanelStatus?: boolean
28
28
  /** 是否在界面右下角显示「同步」悬浮按钮(默认 true)。 */
29
29
  showSyncButton?: boolean
30
+ /**
31
+ * 一键发送给 Agent(TW 笔记 → DSH 会话注入)。
32
+ * `enabled: false` 时路由 403、TW 端按钮点击提示未启用;`token` 非空时
33
+ * 路由要求请求头 `x-send-to-agent-token` 与之匹配;`endpoint` 可覆盖
34
+ * TW 端默认的 `location.origin + /dsh-tiddlywiki`。
35
+ */
36
+ sendToAgent?: { enabled?: boolean; endpoint?: string; token?: string }
30
37
  }
31
38
  uiLanguage?: string
32
39
  [key: string]: unknown
@@ -47,6 +47,32 @@ export interface WebServerFace {
47
47
  register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: IncomingMessage, res: ServerResponse) => void }): () => void
48
48
  }
49
49
 
50
+ /**
51
+ * Structural face over the DSH `sessionController` service (a subset of
52
+ * dsh-api-session-controller). Only the methods the agent-send routes need are
53
+ * declared; the runtime instance is a real Service, never inspected data.
54
+ */
55
+ export interface SessionControllerFace {
56
+ prompt(
57
+ request: { requestId: string; sessionId: string; mode: 'queue' | 'steer'; content: Array<{ type: 'text'; text: string }> },
58
+ signal: AbortSignal,
59
+ ): Promise<{ accepted: boolean }>
60
+ list(
61
+ request: { cursor?: string },
62
+ signal: AbortSignal,
63
+ ): Promise<{
64
+ items: Array<{
65
+ sessionId: string
66
+ updatedAt?: number
67
+ running?: boolean
68
+ blank?: boolean
69
+ parentSessionId?: string
70
+ cwd?: string
71
+ }>
72
+ }>
73
+ create(request: { cwd?: string; workspaceId?: string }): Promise<{ sessionId: string }>
74
+ }
75
+
50
76
  /** Effective UI visibility flags returned by /status (mirror index.ts). */
51
77
  export interface UiDefaultsPublic {
52
78
  showQuickNote: boolean
@@ -62,6 +88,13 @@ export interface RouteDeps {
62
88
  noteDefaults: () => { tag: string }
63
89
  uiDefaults: () => UiDefaultsPublic
64
90
  getWikiPath: () => string
91
+ /** Optional DSH sessionController service (agent-send routes only); resolved
92
+ * lazily per request because it may register after webServer appears. */
93
+ getSessionController: () => SessionControllerFace | undefined
94
+ /** Whether the one-click send-to-agent feature is enabled (config switch). */
95
+ sendToAgentEnabled: () => boolean
96
+ /** Optional shared token that must match `x-send-to-agent-token` when set. */
97
+ sendToAgentToken: () => string
65
98
  }
66
99
 
67
100
  function readBody(req: IncomingMessage, limit = MAX_BODY_BYTES): Promise<string> {
@@ -229,6 +262,115 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
229
262
  json(res, { ok: true, ...view, twProxy: TW_PROXY_PATH, git: gitSummary, note: { tag: deps.noteDefaults().tag }, ui: deps.uiDefaults() })
230
263
  }
231
264
 
265
+ /**
266
+ * GET /dsh-tiddlywiki/agent/sessions — visible ordinary sessions for the TW
267
+ * one-click picker (excludes subagent sessions, activity-descending).
268
+ */
269
+ const handleAgentSessions = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
270
+ try {
271
+ const sc = deps.getSessionController()
272
+ if (sc === undefined) {
273
+ json(res, { ok: false, error: 'session service unavailable' }, 503)
274
+ return
275
+ }
276
+ const list = await sc.list({}, AbortSignal.timeout(10_000))
277
+ const items = (list.items ?? [])
278
+ .filter((s) => s.parentSessionId === undefined)
279
+ .map((s) => ({
280
+ sessionId: s.sessionId,
281
+ cwd: s.cwd ?? null,
282
+ running: !!s.running,
283
+ blank: !!s.blank,
284
+ updatedAt: s.updatedAt ?? 0,
285
+ }))
286
+ .sort((a, b) => b.updatedAt - a.updatedAt)
287
+ json(res, { ok: true, items })
288
+ } catch (err) {
289
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
290
+ }
291
+ }
292
+
293
+ /**
294
+ * POST /dsh-tiddlywiki/agent/send — deliver a note to one agent session as a
295
+ * queued user message (sessionController.prompt, the same API the GUI chat
296
+ * input uses). Guards: feature switch, optional shared token, body shape.
297
+ */
298
+ const handleAgentSend = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
299
+ try {
300
+ if (!deps.sendToAgentEnabled()) {
301
+ json(res, { ok: false, error: 'send-to-agent is disabled' }, 403)
302
+ return
303
+ }
304
+ const token = deps.sendToAgentToken().trim()
305
+ if (token.length > 0) {
306
+ const got = req.headers['x-send-to-agent-token']
307
+ const value = typeof got === 'string' ? got : Array.isArray(got) ? got[0] ?? '' : ''
308
+ if (value !== token) {
309
+ json(res, { ok: false, error: 'unauthorized' }, 401)
310
+ return
311
+ }
312
+ }
313
+ const body = JSON.parse(await readBody(req)) as { sessionId?: unknown; text?: unknown }
314
+ const sessionId = typeof body.sessionId === 'string' && body.sessionId.trim().length > 0 ? body.sessionId.trim() : ''
315
+ const text = typeof body.text === 'string' && body.text.trim().length > 0 ? body.text.trim() : ''
316
+ if (sessionId.length === 0 || text.length === 0) {
317
+ json(res, { ok: false, error: 'sessionId and text are required' }, 400)
318
+ return
319
+ }
320
+ const sc = deps.getSessionController()
321
+ if (sc === undefined) {
322
+ json(res, { ok: false, error: 'session service unavailable' }, 503)
323
+ return
324
+ }
325
+ const requestId = `tw-send-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
326
+ await sc.prompt(
327
+ { requestId, sessionId, mode: 'queue', content: [{ type: 'text', text }] },
328
+ AbortSignal.timeout(20_000),
329
+ )
330
+ json(res, { ok: true, requestId, sessionId })
331
+ } catch (err) {
332
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
333
+ }
334
+ }
335
+
336
+ /**
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.
341
+ */
342
+ const handleAgentCreate = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
343
+ try {
344
+ if (!deps.sendToAgentEnabled()) {
345
+ json(res, { ok: false, error: 'send-to-agent is disabled' }, 403)
346
+ return
347
+ }
348
+ const token = deps.sendToAgentToken().trim()
349
+ if (token.length > 0) {
350
+ const got = req.headers['x-send-to-agent-token']
351
+ const value = typeof got === 'string' ? got : Array.isArray(got) ? got[0] ?? '' : ''
352
+ if (value !== token) {
353
+ json(res, { ok: false, error: 'unauthorized' }, 401)
354
+ return
355
+ }
356
+ }
357
+ const body = JSON.parse(await readBody(req)) as { cwd?: unknown }
358
+ const cwd = typeof body.cwd === 'string' ? body.cwd.trim() : ''
359
+ const sc = deps.getSessionController()
360
+ if (sc === undefined) {
361
+ json(res, { ok: false, error: 'session service unavailable' }, 503)
362
+ return
363
+ }
364
+ if (cwd.length > 0) {
365
+ await mkdir(cwd, { recursive: true })
366
+ }
367
+ const created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
368
+ json(res, { ok: true, sessionId: created.sessionId, cwd: cwd || null })
369
+ } catch (err) {
370
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
371
+ }
372
+ }
373
+
232
374
  const handleNote = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
233
375
  try {
234
376
  const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; tags?: unknown; text?: unknown }
@@ -546,6 +688,9 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
546
688
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/sync`, handler: (req, res) => { void handleSync(req, res) } }),
547
689
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/upload`, handler: (req, res) => { void handleUpload(req, res) } }),
548
690
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/restart`, handler: (req, res) => { void handleRestart(req, res) } }),
691
+ ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/agent/sessions`, handler: (req, res) => { void handleAgentSessions(req, res) } }),
692
+ ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/agent/send`, handler: (req, res) => { void handleAgentSend(req, res) } }),
693
+ ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/agent/create`, handler: (req, res) => { void handleAgentCreate(req, res) } }),
549
694
  ctx.webServer.register({ kind: 'prefix', path: `${ROUTE_PREFIX}/api`, handler: (req, res) => { void handleApiProxy(req, res) } }),
550
695
  ctx.webServer.register({ kind: 'prefix', path: `${TW_PROXY_PREFIX}`, handler: (req, res) => { void handleTwProxy(req, res) } }),
551
696
  ]
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 WebServerFace } from './host/routes.ts'
25
+ import { registerRoutes, type SessionControllerFace, type WebServerFace } 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'
@@ -76,7 +76,7 @@ interface ResolvedConfig {
76
76
  port: number
77
77
  git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
78
78
  note: { tag: string }
79
- ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean }
79
+ ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; sendToAgent: { enabled: boolean } }
80
80
  auth: { username?: string; password?: string }
81
81
  }
82
82
 
@@ -86,7 +86,7 @@ const DEFAULTS: ResolvedConfig = {
86
86
  port: 0,
87
87
  git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
88
88
  note: { tag: 'inbox' },
89
- ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true },
89
+ ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true, sendToAgent: { enabled: true } },
90
90
  auth: { username: '', password: '' },
91
91
  }
92
92
 
@@ -353,6 +353,11 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
353
353
  // Routes + settings-panel admin surface (lazy webServer).
354
354
  ctx.inject(['webServer'], (webCtx: HostCtx) => {
355
355
  const ws = (webCtx as unknown as { webServer: WebServerFace }).webServer
356
+ // sessionController is a core host service; read it LAZILY per request (via
357
+ // the plugin root ctx) because it may be registered after webServer, and the
358
+ // agent routes must work whenever a request actually arrives.
359
+ const getSessionController = (): SessionControllerFace | undefined =>
360
+ ctx.get('sessionController') as SessionControllerFace | undefined
356
361
  const disposeRoutes = registerRoutes({ webServer: ws }, {
357
362
  server,
358
363
  getClient: client,
@@ -361,6 +366,12 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
361
366
  noteDefaults: () => ({ tag: effectiveNoteTag() }),
362
367
  uiDefaults: () => effectiveUi(),
363
368
  getWikiPath: () => wikiPath,
369
+ getSessionController,
370
+ sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
371
+ sendToAgentToken: () => {
372
+ const token = eff().ui?.sendToAgent?.token
373
+ return typeof token === 'string' ? token : ''
374
+ },
364
375
  })
365
376
  const adminDeps: AdminDeps = {
366
377
  server,