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/lib/index.js +176 -2
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/host/config.ts +7 -0
- package/src/host/routes.ts +145 -0
- package/src/index.ts +14 -3
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.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
package/src/host/config.ts
CHANGED
|
@@ -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
|
package/src/host/routes.ts
CHANGED
|
@@ -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,
|