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/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)检索;\`tiddlywiki_get\`(title)读全文;\`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 及计数。
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
+ }