dsh-tiddlywiki 0.16.19 → 0.16.21
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/LICENSE +21 -21
- package/README.md +8 -1
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +53 -26
- package/lib/client.js +53 -26
- package/lib/index.js +136 -25
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/endpoints.ts +10 -10
- package/src/client/index.ts +36 -0
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/panel.ts +4 -0
- package/src/client/rightbar-tab.ts +440 -0
- package/src/client/session-summary.ts +279 -279
- package/src/client/settings-page.ts +1 -0
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +27 -0
- package/src/client/theme-sync.ts +312 -312
- package/src/client/toast.ts +22 -22
- package/src/client/tool-views.ts +604 -604
- package/src/client/ui-config.ts +64 -61
- package/src/host/config.ts +7 -0
- package/src/host/http.ts +57 -57
- package/src/host/routes.ts +2 -0
- package/src/host/seed-home.ts +1 -1
- package/src/host/seed-render.ts +66 -66
- package/src/host/seed-send-to-agent.ts +63 -63
- package/src/host/tools.ts +40 -5
- package/src/host/tw-api.ts +108 -16
- package/src/host/wiki.ts +299 -299
- package/src/index.ts +9 -8
- package/src/sdk.ts +204 -204
package/src/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { registerRoutes, type AgentPresetsFace, type PermissionPresetsFace, type
|
|
|
26
26
|
import { ConfigStore, deepMerge, DARK_PALETTE_DEFAULT, TW_WEB_HOST_TIDDLER, TW_WEB_HOST_DEFAULT, type PluginConfigShape } from './host/config.ts'
|
|
27
27
|
import { registerAdminRoutes, ensureLanguage, resolveTwRoot, type AdminDeps } from './host/admin.ts'
|
|
28
28
|
import { runAllSeeds, checkAllSeeds, runSeedById, removeSeedById, SEED_DEFS, type SeedStatus, type SeedRunResult } from './host/seeds.ts'
|
|
29
|
-
import { TiddlyWebClient } from './host/tw-api.ts'
|
|
29
|
+
import { TiddlyWebClient, isBinaryType, TEXT_LIST_FILTER } from './host/tw-api.ts'
|
|
30
30
|
import { registerTiddlywikiTools, type ToolsDeps } from './host/tools.ts'
|
|
31
31
|
import { PATH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, WikiServer, type WikiServerOptions } from './host/wiki.ts'
|
|
32
32
|
import { dshHomePath, defineTool } from './sdk.ts'
|
|
@@ -38,7 +38,7 @@ export const name = 'dsh-tiddlywiki'
|
|
|
38
38
|
export const inject = ['tools', 'systemPrompt']
|
|
39
39
|
|
|
40
40
|
/** Re-exports for the headless selftest and future consumers. */
|
|
41
|
-
export { AutoCommitter, GitFace, PATH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TiddlyWebClient, WikiServer, dshHomePath, defineTool }
|
|
41
|
+
export { AutoCommitter, GitFace, PATH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TiddlyWebClient, isBinaryType, TEXT_LIST_FILTER, WikiServer, dshHomePath, defineTool }
|
|
42
42
|
export { ConfigStore, deepMerge } from './host/config.ts'
|
|
43
43
|
export { openInTwEditor, registerRoutes } from './host/routes.ts'
|
|
44
44
|
export { writeSessionSummary, SESSION_SUMMARY_PREFIX } from './host/routes.ts'
|
|
@@ -66,7 +66,7 @@ export interface TiddlywikiConfig {
|
|
|
66
66
|
port?: number
|
|
67
67
|
git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
|
|
68
68
|
note?: { tag?: string }
|
|
69
|
-
ui?: { showQuickNote?: boolean; showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string; tabLabel?: string; showSessionTab?: boolean; sendToAgent?: { enabled?: boolean; endpoint?: string; token?: string }; allArticles?: { pageSize?: number } }
|
|
69
|
+
ui?: { showQuickNote?: boolean; showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean; sendToAgent?: { enabled?: boolean; endpoint?: string; token?: string }; allArticles?: { pageSize?: number } }
|
|
70
70
|
/** 启动时自动启用的 TW 语言代码(如 "zh-Hans"),也受配置 tiddler 覆盖。 */
|
|
71
71
|
uiLanguage?: string
|
|
72
72
|
auth?: { username?: string; password?: string }
|
|
@@ -89,7 +89,7 @@ interface ResolvedConfig {
|
|
|
89
89
|
port: number
|
|
90
90
|
git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
|
|
91
91
|
note: { tag: string }
|
|
92
|
-
ui: { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; sendToAgent: { enabled: boolean; endpoint?: string; token?: string }; allArticles: { pageSize: number } }
|
|
92
|
+
ui: { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; showRightbarTab: boolean; sendToAgent: { enabled: boolean; endpoint?: string; token?: string }; allArticles: { pageSize: number } }
|
|
93
93
|
uiLanguage: string
|
|
94
94
|
auth: { username?: string; password?: string }
|
|
95
95
|
}
|
|
@@ -100,7 +100,7 @@ const DEFAULTS: ResolvedConfig = {
|
|
|
100
100
|
port: 0,
|
|
101
101
|
git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
|
|
102
102
|
note: { tag: 'inbox' },
|
|
103
|
-
ui: { showQuickNote: true, showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, tabLabel: '知识库', showSessionTab: true, sendToAgent: { enabled: true }, allArticles: { pageSize: 10 } },
|
|
103
|
+
ui: { showQuickNote: true, showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, tabLabel: '知识库', showSessionTab: true, showRightbarTab: true, sendToAgent: { enabled: true }, allArticles: { pageSize: 10 } },
|
|
104
104
|
uiLanguage: '',
|
|
105
105
|
auth: { username: '', password: '' },
|
|
106
106
|
}
|
|
@@ -201,8 +201,8 @@ const PROMPT_TEXT = `## TiddlyWiki 持久知识库
|
|
|
201
201
|
|
|
202
202
|
本机有一个 TiddlyWiki 5 持久知识库(wiki 文件夹即 git 仓库)。你可以用工具读写 tiddler:
|
|
203
203
|
|
|
204
|
-
- \`tiddlywiki_search\`(query 必填;可选 tags[]/tag、since 修改时间、type、limit
|
|
205
|
-
- \`tiddlywiki_recent\`(limit?, since
|
|
204
|
+
- \`tiddlywiki_search\`(query 必填;可选 tags[]/tag、since 修改时间、type、limit)检索(图片等二进制附件不参与检索);\`tiddlywiki_get\`(title)读全文(二进制附件只返回元数据,不含 base64 正文);\`tiddlywiki_put\`(title, text, tags?, fields?)写/覆盖;\`tiddlywiki_batch_put\`(items[])批量写;\`tiddlywiki_rename\`(oldTitle, newTitle, updateRefs?)重命名并尽量同步引用;\`tiddlywiki_delete\`(title)删除。
|
|
205
|
+
- \`tiddlywiki_recent\`(limit?, since?)看最近修改的笔记(不含图片等二进制附件);\`tiddlywiki_list_tags\` 看现有 tag 及计数。
|
|
206
206
|
- \`tiddlywiki_git_sync\`(pull|push|sync)做 git 同步;\`tiddlywiki_git_resolve\`(files, strategy=keep-local|keep-remote|list)在 pull 冲突后按 tiddler 二选一解决。
|
|
207
207
|
|
|
208
208
|
知识库同步纪律(三条):
|
|
@@ -259,7 +259,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
|
|
|
259
259
|
const tag = eff().note?.tag
|
|
260
260
|
return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
|
|
261
261
|
}
|
|
262
|
-
const effectiveUi = (): { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean } => {
|
|
262
|
+
const effectiveUi = (): { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; showRightbarTab: boolean } => {
|
|
263
263
|
const ui = eff().ui ?? {}
|
|
264
264
|
const palette = typeof ui.darkPalette === 'string' && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : DARK_PALETTE_DEFAULT
|
|
265
265
|
const label = typeof ui.sidebarLabel === 'string' && ui.sidebarLabel.trim().length > 0 ? ui.sidebarLabel.trim() : config.ui.sidebarLabel
|
|
@@ -276,6 +276,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
|
|
|
276
276
|
darkPalette: palette,
|
|
277
277
|
tabLabel,
|
|
278
278
|
showSessionTab: ui.showSessionTab !== false,
|
|
279
|
+
showRightbarTab: ui.showRightbarTab !== false,
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
|
package/src/sdk.ts
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self-contained replacements for the @deepseek-ai runtime imports the host
|
|
3
|
-
* half must NEVER take from npm-mirror SDK packages (dsh-home-paths,
|
|
4
|
-
* dsh-tools' defineTool).
|
|
5
|
-
*
|
|
6
|
-
* Why (design doc §4.4, taskboard lesson): a published copy must not resolve
|
|
7
|
-
* `@deepseek-ai/dsh-tools` from the profile's node_modules — an npm-mirror
|
|
8
|
-
* dsh-tools there shadows the CLI-internal build for the WHOLE base layer and
|
|
9
|
-
* breaks the agent loop. Everything here is a pure, structure-compatible
|
|
10
|
-
* reimplementation of the exact behavior the registry relies on:
|
|
11
|
-
*
|
|
12
|
-
* - `dshHomePath` mirrors `join(resolve(env.DSH_HOME ?? ~/.dsh), ...segments)`;
|
|
13
|
-
* - `defineTool` compiles author-facing parameter specs into the same raw
|
|
14
|
-
* JSON-Schema subset the registry expects and pre-validates model arguments.
|
|
15
|
-
*
|
|
16
|
-
* @module dsh-tiddlywiki/sdk
|
|
17
|
-
*/
|
|
18
|
-
import { homedir } from 'node:os'
|
|
19
|
-
import { join, resolve } from 'node:path'
|
|
20
|
-
|
|
21
|
-
/** The DSH user home (DSH_HOME overrides). */
|
|
22
|
-
export function dshHomePath(...segments: string[]): string {
|
|
23
|
-
const override = process.env.DSH_HOME
|
|
24
|
-
const home = resolve(override !== undefined && override.length > 0 ? override : join(homedir(), '.dsh'))
|
|
25
|
-
return join(home, ...segments)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Author-facing scalar spec. */
|
|
29
|
-
interface ScalarSpec {
|
|
30
|
-
readonly type: 'string' | 'number' | 'integer' | 'boolean' | 'null'
|
|
31
|
-
readonly description?: string
|
|
32
|
-
readonly enum?: readonly unknown[]
|
|
33
|
-
readonly const?: unknown
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Author-facing object spec (additionalProperties is mandatory). */
|
|
37
|
-
interface ObjectSpec {
|
|
38
|
-
readonly type: 'object'
|
|
39
|
-
readonly additionalProperties: boolean
|
|
40
|
-
readonly description?: string
|
|
41
|
-
/** Nested properties may also declare `required` (array items etc.). */
|
|
42
|
-
readonly properties?: Readonly<Record<string, ParameterSpec>>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Author-facing value spec. */
|
|
46
|
-
type ValueSpec = ScalarSpec | ObjectSpec | { readonly type: 'json'; readonly description?: string } | { readonly type: 'array'; readonly items?: ValueSpec; readonly description?: string }
|
|
47
|
-
|
|
48
|
-
/** Author-facing parameter entry (a value spec plus top-level required). */
|
|
49
|
-
type ParameterSpec = ValueSpec & { readonly required?: boolean }
|
|
50
|
-
|
|
51
|
-
/** Raw JSON-Schema subset node. */
|
|
52
|
-
type RawSchema = Record<string, unknown>
|
|
53
|
-
|
|
54
|
-
/** Compile one value spec to the raw subset (json → annotation-only). */
|
|
55
|
-
function compileValue(spec: ValueSpec): RawSchema {
|
|
56
|
-
const node: RawSchema = {}
|
|
57
|
-
const description = (spec as { description?: string }).description
|
|
58
|
-
if (typeof description === 'string' && description.length > 0) node.description = description
|
|
59
|
-
const type = (spec as { type?: string }).type
|
|
60
|
-
if (type === undefined || type === 'json') return node
|
|
61
|
-
if (type === 'object') {
|
|
62
|
-
const objectSpec = spec as ObjectSpec
|
|
63
|
-
node.type = 'object'
|
|
64
|
-
node.additionalProperties = objectSpec.additionalProperties
|
|
65
|
-
if (objectSpec.properties !== undefined) {
|
|
66
|
-
const compiled = compilePropertyMap(objectSpec.properties)
|
|
67
|
-
node.properties = compiled.properties
|
|
68
|
-
// Nested objects also carry their own `required` list (array items, etc.).
|
|
69
|
-
if (compiled.required !== undefined) node.required = compiled.required
|
|
70
|
-
}
|
|
71
|
-
return node
|
|
72
|
-
}
|
|
73
|
-
if (type === 'array') {
|
|
74
|
-
node.type = 'array'
|
|
75
|
-
const items = (spec as { items?: ValueSpec }).items
|
|
76
|
-
if (items !== undefined) node.items = compileValue(items)
|
|
77
|
-
return node
|
|
78
|
-
}
|
|
79
|
-
node.type = type
|
|
80
|
-
const enumValues = (spec as ScalarSpec).enum
|
|
81
|
-
if (enumValues !== undefined) node.enum = [...enumValues]
|
|
82
|
-
const constValue = (spec as ScalarSpec).const
|
|
83
|
-
if (constValue !== undefined) node.const = constValue
|
|
84
|
-
return node
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Compile a property map: properties + collected required list. */
|
|
88
|
-
function compilePropertyMap(spec: Readonly<Record<string, ParameterSpec>>): { properties: Record<string, RawSchema>; required?: string[] } {
|
|
89
|
-
const properties: Record<string, RawSchema> = {}
|
|
90
|
-
const required: string[] = []
|
|
91
|
-
for (const [name, entry] of Object.entries(spec)) {
|
|
92
|
-
const { required: isRequired, ...valueSpec } = entry as ParameterSpec & Record<string, unknown>
|
|
93
|
-
properties[name] = compileValue(valueSpec as ValueSpec)
|
|
94
|
-
if (isRequired === true) required.push(name)
|
|
95
|
-
}
|
|
96
|
-
return required.length > 0 ? { properties, required } : { properties }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Does a JS value match a raw-subset scalar type? */
|
|
100
|
-
function matchesScalarType(value: unknown, type: string): boolean {
|
|
101
|
-
switch (type) {
|
|
102
|
-
case 'string': return typeof value === 'string'
|
|
103
|
-
case 'number': return typeof value === 'number'
|
|
104
|
-
case 'integer': return typeof value === 'number' && Number.isInteger(value)
|
|
105
|
-
case 'boolean': return typeof value === 'boolean'
|
|
106
|
-
case 'null': return value === null
|
|
107
|
-
default: return true
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Validate a value against the compiled subset; returns path-qualified violations. */
|
|
112
|
-
function validateValue(schema: RawSchema, value: unknown, path: string): string[] {
|
|
113
|
-
if (typeof schema.type !== 'string' || schema.type.length === 0) return []
|
|
114
|
-
if (schema.type === 'object') {
|
|
115
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) return [`${path} must be an object`]
|
|
116
|
-
const violations: string[] = []
|
|
117
|
-
const present = value as Record<string, unknown>
|
|
118
|
-
for (const key of (schema.required as string[] | undefined) ?? []) {
|
|
119
|
-
if (!(key in present)) violations.push(`${path}.${key} is required`)
|
|
120
|
-
}
|
|
121
|
-
if (schema.additionalProperties === false) {
|
|
122
|
-
const known = new Set(Object.keys((schema.properties as Record<string, RawSchema> | undefined) ?? {}))
|
|
123
|
-
for (const key of Object.keys(present)) {
|
|
124
|
-
if (!known.has(key)) violations.push(`${path}.${key} is not a declared property`)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
for (const [key, child] of Object.entries((schema.properties as Record<string, RawSchema> | undefined) ?? {})) {
|
|
128
|
-
if (key in present) violations.push(...validateValue(child, present[key], `${path}.${key}`))
|
|
129
|
-
}
|
|
130
|
-
return violations
|
|
131
|
-
}
|
|
132
|
-
if (schema.type === 'array') {
|
|
133
|
-
if (!Array.isArray(value)) return [`${path} must be an array`]
|
|
134
|
-
const violations: string[] = []
|
|
135
|
-
const items = schema.items as RawSchema | undefined
|
|
136
|
-
if (items !== undefined) {
|
|
137
|
-
value.forEach((item, index) => { violations.push(...validateValue(items, item, `${path}[${index}]`)) })
|
|
138
|
-
}
|
|
139
|
-
return violations
|
|
140
|
-
}
|
|
141
|
-
if (!matchesScalarType(value, schema.type)) return [`${path} must be ${schema.type}`]
|
|
142
|
-
const enumValues = schema.enum as unknown[] | undefined
|
|
143
|
-
if (enumValues !== undefined && !enumValues.some(v => v === value)) {
|
|
144
|
-
return [`${path} must be one of ${enumValues.map(String).join(', ')}`]
|
|
145
|
-
}
|
|
146
|
-
const constValue = (schema as { const?: unknown }).const
|
|
147
|
-
if (constValue !== undefined && constValue !== value) {
|
|
148
|
-
return [`${path} must be ${String(constValue)}`]
|
|
149
|
-
}
|
|
150
|
-
return []
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Options shape we consume (a structural subset of the SDK's defineTool). */
|
|
154
|
-
export interface DefineToolOptions<A, V> {
|
|
155
|
-
readonly name: string
|
|
156
|
-
readonly description: string
|
|
157
|
-
readonly parameters: Readonly<Record<string, ParameterSpec>>
|
|
158
|
-
readonly output: {
|
|
159
|
-
readonly schema: { readonly type: 'json' }
|
|
160
|
-
render(args: A, value: V): Array<{ type: 'text'; text: string }>
|
|
161
|
-
}
|
|
162
|
-
execute(args: A, exec: unknown): Promise<V>
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** A registry-ready tool definition (structure-compatible with the SDK's). */
|
|
166
|
-
export interface ToolDefinition<A = unknown, V = unknown> {
|
|
167
|
-
readonly name: string
|
|
168
|
-
readonly description: string
|
|
169
|
-
readonly parameters: RawSchema
|
|
170
|
-
readonly output: {
|
|
171
|
-
readonly schema: RawSchema
|
|
172
|
-
render(args: A, value: V): Array<{ type: 'text'; text: string }>
|
|
173
|
-
}
|
|
174
|
-
execute(args: A, exec: unknown): Promise<V>
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Define a first-party tool: compile the parameter spec, pre-validate
|
|
179
|
-
* arguments, and pass through the execution.
|
|
180
|
-
*/
|
|
181
|
-
export function defineTool<A extends Record<string, unknown>, V>(options: DefineToolOptions<A, V>): ToolDefinition<A, V> {
|
|
182
|
-
const compiled = compilePropertyMap(options.parameters as Readonly<Record<string, ParameterSpec>>)
|
|
183
|
-
const parameters: RawSchema = { type: 'object', properties: compiled.properties }
|
|
184
|
-
if (compiled.required !== undefined) parameters.required = compiled.required
|
|
185
|
-
const userExecute = options.execute
|
|
186
|
-
return {
|
|
187
|
-
name: options.name,
|
|
188
|
-
description: options.description,
|
|
189
|
-
parameters,
|
|
190
|
-
output: {
|
|
191
|
-
schema: {},
|
|
192
|
-
render(args, value) {
|
|
193
|
-
return options.output.render(args, value)
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
async execute(args, exec) {
|
|
197
|
-
const violations = validateValue(parameters, args, 'arguments')
|
|
198
|
-
if (violations.length > 0) {
|
|
199
|
-
throw new Error(`Error: invalid arguments: ${violations.join('; ')}`)
|
|
200
|
-
}
|
|
201
|
-
return userExecute(args, exec)
|
|
202
|
-
},
|
|
203
|
-
}
|
|
204
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained replacements for the @deepseek-ai runtime imports the host
|
|
3
|
+
* half must NEVER take from npm-mirror SDK packages (dsh-home-paths,
|
|
4
|
+
* dsh-tools' defineTool).
|
|
5
|
+
*
|
|
6
|
+
* Why (design doc §4.4, taskboard lesson): a published copy must not resolve
|
|
7
|
+
* `@deepseek-ai/dsh-tools` from the profile's node_modules — an npm-mirror
|
|
8
|
+
* dsh-tools there shadows the CLI-internal build for the WHOLE base layer and
|
|
9
|
+
* breaks the agent loop. Everything here is a pure, structure-compatible
|
|
10
|
+
* reimplementation of the exact behavior the registry relies on:
|
|
11
|
+
*
|
|
12
|
+
* - `dshHomePath` mirrors `join(resolve(env.DSH_HOME ?? ~/.dsh), ...segments)`;
|
|
13
|
+
* - `defineTool` compiles author-facing parameter specs into the same raw
|
|
14
|
+
* JSON-Schema subset the registry expects and pre-validates model arguments.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-tiddlywiki/sdk
|
|
17
|
+
*/
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
/** The DSH user home (DSH_HOME overrides). */
|
|
22
|
+
export function dshHomePath(...segments: string[]): string {
|
|
23
|
+
const override = process.env.DSH_HOME
|
|
24
|
+
const home = resolve(override !== undefined && override.length > 0 ? override : join(homedir(), '.dsh'))
|
|
25
|
+
return join(home, ...segments)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Author-facing scalar spec. */
|
|
29
|
+
interface ScalarSpec {
|
|
30
|
+
readonly type: 'string' | 'number' | 'integer' | 'boolean' | 'null'
|
|
31
|
+
readonly description?: string
|
|
32
|
+
readonly enum?: readonly unknown[]
|
|
33
|
+
readonly const?: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Author-facing object spec (additionalProperties is mandatory). */
|
|
37
|
+
interface ObjectSpec {
|
|
38
|
+
readonly type: 'object'
|
|
39
|
+
readonly additionalProperties: boolean
|
|
40
|
+
readonly description?: string
|
|
41
|
+
/** Nested properties may also declare `required` (array items etc.). */
|
|
42
|
+
readonly properties?: Readonly<Record<string, ParameterSpec>>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Author-facing value spec. */
|
|
46
|
+
type ValueSpec = ScalarSpec | ObjectSpec | { readonly type: 'json'; readonly description?: string } | { readonly type: 'array'; readonly items?: ValueSpec; readonly description?: string }
|
|
47
|
+
|
|
48
|
+
/** Author-facing parameter entry (a value spec plus top-level required). */
|
|
49
|
+
type ParameterSpec = ValueSpec & { readonly required?: boolean }
|
|
50
|
+
|
|
51
|
+
/** Raw JSON-Schema subset node. */
|
|
52
|
+
type RawSchema = Record<string, unknown>
|
|
53
|
+
|
|
54
|
+
/** Compile one value spec to the raw subset (json → annotation-only). */
|
|
55
|
+
function compileValue(spec: ValueSpec): RawSchema {
|
|
56
|
+
const node: RawSchema = {}
|
|
57
|
+
const description = (spec as { description?: string }).description
|
|
58
|
+
if (typeof description === 'string' && description.length > 0) node.description = description
|
|
59
|
+
const type = (spec as { type?: string }).type
|
|
60
|
+
if (type === undefined || type === 'json') return node
|
|
61
|
+
if (type === 'object') {
|
|
62
|
+
const objectSpec = spec as ObjectSpec
|
|
63
|
+
node.type = 'object'
|
|
64
|
+
node.additionalProperties = objectSpec.additionalProperties
|
|
65
|
+
if (objectSpec.properties !== undefined) {
|
|
66
|
+
const compiled = compilePropertyMap(objectSpec.properties)
|
|
67
|
+
node.properties = compiled.properties
|
|
68
|
+
// Nested objects also carry their own `required` list (array items, etc.).
|
|
69
|
+
if (compiled.required !== undefined) node.required = compiled.required
|
|
70
|
+
}
|
|
71
|
+
return node
|
|
72
|
+
}
|
|
73
|
+
if (type === 'array') {
|
|
74
|
+
node.type = 'array'
|
|
75
|
+
const items = (spec as { items?: ValueSpec }).items
|
|
76
|
+
if (items !== undefined) node.items = compileValue(items)
|
|
77
|
+
return node
|
|
78
|
+
}
|
|
79
|
+
node.type = type
|
|
80
|
+
const enumValues = (spec as ScalarSpec).enum
|
|
81
|
+
if (enumValues !== undefined) node.enum = [...enumValues]
|
|
82
|
+
const constValue = (spec as ScalarSpec).const
|
|
83
|
+
if (constValue !== undefined) node.const = constValue
|
|
84
|
+
return node
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Compile a property map: properties + collected required list. */
|
|
88
|
+
function compilePropertyMap(spec: Readonly<Record<string, ParameterSpec>>): { properties: Record<string, RawSchema>; required?: string[] } {
|
|
89
|
+
const properties: Record<string, RawSchema> = {}
|
|
90
|
+
const required: string[] = []
|
|
91
|
+
for (const [name, entry] of Object.entries(spec)) {
|
|
92
|
+
const { required: isRequired, ...valueSpec } = entry as ParameterSpec & Record<string, unknown>
|
|
93
|
+
properties[name] = compileValue(valueSpec as ValueSpec)
|
|
94
|
+
if (isRequired === true) required.push(name)
|
|
95
|
+
}
|
|
96
|
+
return required.length > 0 ? { properties, required } : { properties }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Does a JS value match a raw-subset scalar type? */
|
|
100
|
+
function matchesScalarType(value: unknown, type: string): boolean {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'string': return typeof value === 'string'
|
|
103
|
+
case 'number': return typeof value === 'number'
|
|
104
|
+
case 'integer': return typeof value === 'number' && Number.isInteger(value)
|
|
105
|
+
case 'boolean': return typeof value === 'boolean'
|
|
106
|
+
case 'null': return value === null
|
|
107
|
+
default: return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Validate a value against the compiled subset; returns path-qualified violations. */
|
|
112
|
+
function validateValue(schema: RawSchema, value: unknown, path: string): string[] {
|
|
113
|
+
if (typeof schema.type !== 'string' || schema.type.length === 0) return []
|
|
114
|
+
if (schema.type === 'object') {
|
|
115
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return [`${path} must be an object`]
|
|
116
|
+
const violations: string[] = []
|
|
117
|
+
const present = value as Record<string, unknown>
|
|
118
|
+
for (const key of (schema.required as string[] | undefined) ?? []) {
|
|
119
|
+
if (!(key in present)) violations.push(`${path}.${key} is required`)
|
|
120
|
+
}
|
|
121
|
+
if (schema.additionalProperties === false) {
|
|
122
|
+
const known = new Set(Object.keys((schema.properties as Record<string, RawSchema> | undefined) ?? {}))
|
|
123
|
+
for (const key of Object.keys(present)) {
|
|
124
|
+
if (!known.has(key)) violations.push(`${path}.${key} is not a declared property`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [key, child] of Object.entries((schema.properties as Record<string, RawSchema> | undefined) ?? {})) {
|
|
128
|
+
if (key in present) violations.push(...validateValue(child, present[key], `${path}.${key}`))
|
|
129
|
+
}
|
|
130
|
+
return violations
|
|
131
|
+
}
|
|
132
|
+
if (schema.type === 'array') {
|
|
133
|
+
if (!Array.isArray(value)) return [`${path} must be an array`]
|
|
134
|
+
const violations: string[] = []
|
|
135
|
+
const items = schema.items as RawSchema | undefined
|
|
136
|
+
if (items !== undefined) {
|
|
137
|
+
value.forEach((item, index) => { violations.push(...validateValue(items, item, `${path}[${index}]`)) })
|
|
138
|
+
}
|
|
139
|
+
return violations
|
|
140
|
+
}
|
|
141
|
+
if (!matchesScalarType(value, schema.type)) return [`${path} must be ${schema.type}`]
|
|
142
|
+
const enumValues = schema.enum as unknown[] | undefined
|
|
143
|
+
if (enumValues !== undefined && !enumValues.some(v => v === value)) {
|
|
144
|
+
return [`${path} must be one of ${enumValues.map(String).join(', ')}`]
|
|
145
|
+
}
|
|
146
|
+
const constValue = (schema as { const?: unknown }).const
|
|
147
|
+
if (constValue !== undefined && constValue !== value) {
|
|
148
|
+
return [`${path} must be ${String(constValue)}`]
|
|
149
|
+
}
|
|
150
|
+
return []
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Options shape we consume (a structural subset of the SDK's defineTool). */
|
|
154
|
+
export interface DefineToolOptions<A, V> {
|
|
155
|
+
readonly name: string
|
|
156
|
+
readonly description: string
|
|
157
|
+
readonly parameters: Readonly<Record<string, ParameterSpec>>
|
|
158
|
+
readonly output: {
|
|
159
|
+
readonly schema: { readonly type: 'json' }
|
|
160
|
+
render(args: A, value: V): Array<{ type: 'text'; text: string }>
|
|
161
|
+
}
|
|
162
|
+
execute(args: A, exec: unknown): Promise<V>
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A registry-ready tool definition (structure-compatible with the SDK's). */
|
|
166
|
+
export interface ToolDefinition<A = unknown, V = unknown> {
|
|
167
|
+
readonly name: string
|
|
168
|
+
readonly description: string
|
|
169
|
+
readonly parameters: RawSchema
|
|
170
|
+
readonly output: {
|
|
171
|
+
readonly schema: RawSchema
|
|
172
|
+
render(args: A, value: V): Array<{ type: 'text'; text: string }>
|
|
173
|
+
}
|
|
174
|
+
execute(args: A, exec: unknown): Promise<V>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Define a first-party tool: compile the parameter spec, pre-validate
|
|
179
|
+
* arguments, and pass through the execution.
|
|
180
|
+
*/
|
|
181
|
+
export function defineTool<A extends Record<string, unknown>, V>(options: DefineToolOptions<A, V>): ToolDefinition<A, V> {
|
|
182
|
+
const compiled = compilePropertyMap(options.parameters as Readonly<Record<string, ParameterSpec>>)
|
|
183
|
+
const parameters: RawSchema = { type: 'object', properties: compiled.properties }
|
|
184
|
+
if (compiled.required !== undefined) parameters.required = compiled.required
|
|
185
|
+
const userExecute = options.execute
|
|
186
|
+
return {
|
|
187
|
+
name: options.name,
|
|
188
|
+
description: options.description,
|
|
189
|
+
parameters,
|
|
190
|
+
output: {
|
|
191
|
+
schema: {},
|
|
192
|
+
render(args, value) {
|
|
193
|
+
return options.output.render(args, value)
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
async execute(args, exec) {
|
|
197
|
+
const violations = validateValue(parameters, args, 'arguments')
|
|
198
|
+
if (violations.length > 0) {
|
|
199
|
+
throw new Error(`Error: invalid arguments: ${violations.join('; ')}`)
|
|
200
|
+
}
|
|
201
|
+
return userExecute(args, exec)
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|