dsh-tiddlywiki 0.1.0

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/sdk.ts ADDED
@@ -0,0 +1,198 @@
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
+ readonly properties?: Readonly<Record<string, ValueSpec>>
42
+ }
43
+
44
+ /** Author-facing value spec. */
45
+ type ValueSpec = ScalarSpec | ObjectSpec | { readonly type: 'json'; readonly description?: string } | { readonly type: 'array'; readonly items?: ValueSpec; readonly description?: string }
46
+
47
+ /** Author-facing parameter entry (a value spec plus top-level required). */
48
+ type ParameterSpec = ValueSpec & { readonly required?: boolean }
49
+
50
+ /** Raw JSON-Schema subset node. */
51
+ type RawSchema = Record<string, unknown>
52
+
53
+ /** Compile one value spec to the raw subset (json → annotation-only). */
54
+ function compileValue(spec: ValueSpec): RawSchema {
55
+ const node: RawSchema = {}
56
+ const description = (spec as { description?: string }).description
57
+ if (typeof description === 'string' && description.length > 0) node.description = description
58
+ const type = (spec as { type?: string }).type
59
+ if (type === undefined || type === 'json') return node
60
+ if (type === 'object') {
61
+ const objectSpec = spec as ObjectSpec
62
+ node.type = 'object'
63
+ node.additionalProperties = objectSpec.additionalProperties
64
+ if (objectSpec.properties !== undefined) node.properties = compilePropertyMap(objectSpec.properties).properties
65
+ return node
66
+ }
67
+ if (type === 'array') {
68
+ node.type = 'array'
69
+ const items = (spec as { items?: ValueSpec }).items
70
+ if (items !== undefined) node.items = compileValue(items)
71
+ return node
72
+ }
73
+ node.type = type
74
+ const enumValues = (spec as ScalarSpec).enum
75
+ if (enumValues !== undefined) node.enum = [...enumValues]
76
+ const constValue = (spec as ScalarSpec).const
77
+ if (constValue !== undefined) node.const = constValue
78
+ return node
79
+ }
80
+
81
+ /** Compile a property map: properties + collected required list. */
82
+ function compilePropertyMap(spec: Readonly<Record<string, ParameterSpec>>): { properties: Record<string, RawSchema>; required?: string[] } {
83
+ const properties: Record<string, RawSchema> = {}
84
+ const required: string[] = []
85
+ for (const [name, entry] of Object.entries(spec)) {
86
+ const { required: isRequired, ...valueSpec } = entry as ParameterSpec & Record<string, unknown>
87
+ properties[name] = compileValue(valueSpec as ValueSpec)
88
+ if (isRequired === true) required.push(name)
89
+ }
90
+ return required.length > 0 ? { properties, required } : { properties }
91
+ }
92
+
93
+ /** Does a JS value match a raw-subset scalar type? */
94
+ function matchesScalarType(value: unknown, type: string): boolean {
95
+ switch (type) {
96
+ case 'string': return typeof value === 'string'
97
+ case 'number': return typeof value === 'number'
98
+ case 'integer': return typeof value === 'number' && Number.isInteger(value)
99
+ case 'boolean': return typeof value === 'boolean'
100
+ case 'null': return value === null
101
+ default: return true
102
+ }
103
+ }
104
+
105
+ /** Validate a value against the compiled subset; returns path-qualified violations. */
106
+ function validateValue(schema: RawSchema, value: unknown, path: string): string[] {
107
+ if (typeof schema.type !== 'string' || schema.type.length === 0) return []
108
+ if (schema.type === 'object') {
109
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return [`${path} must be an object`]
110
+ const violations: string[] = []
111
+ const present = value as Record<string, unknown>
112
+ for (const key of (schema.required as string[] | undefined) ?? []) {
113
+ if (!(key in present)) violations.push(`${path}.${key} is required`)
114
+ }
115
+ if (schema.additionalProperties === false) {
116
+ const known = new Set(Object.keys((schema.properties as Record<string, RawSchema> | undefined) ?? {}))
117
+ for (const key of Object.keys(present)) {
118
+ if (!known.has(key)) violations.push(`${path}.${key} is not a declared property`)
119
+ }
120
+ }
121
+ for (const [key, child] of Object.entries((schema.properties as Record<string, RawSchema> | undefined) ?? {})) {
122
+ if (key in present) violations.push(...validateValue(child, present[key], `${path}.${key}`))
123
+ }
124
+ return violations
125
+ }
126
+ if (schema.type === 'array') {
127
+ if (!Array.isArray(value)) return [`${path} must be an array`]
128
+ const violations: string[] = []
129
+ const items = schema.items as RawSchema | undefined
130
+ if (items !== undefined) {
131
+ value.forEach((item, index) => { violations.push(...validateValue(items, item, `${path}[${index}]`)) })
132
+ }
133
+ return violations
134
+ }
135
+ if (!matchesScalarType(value, schema.type)) return [`${path} must be ${schema.type}`]
136
+ const enumValues = schema.enum as unknown[] | undefined
137
+ if (enumValues !== undefined && !enumValues.some(v => v === value)) {
138
+ return [`${path} must be one of ${enumValues.map(String).join(', ')}`]
139
+ }
140
+ const constValue = (schema as { const?: unknown }).const
141
+ if (constValue !== undefined && constValue !== value) {
142
+ return [`${path} must be ${String(constValue)}`]
143
+ }
144
+ return []
145
+ }
146
+
147
+ /** Options shape we consume (a structural subset of the SDK's defineTool). */
148
+ export interface DefineToolOptions<A, V> {
149
+ readonly name: string
150
+ readonly description: string
151
+ readonly parameters: Readonly<Record<string, ParameterSpec>>
152
+ readonly output: {
153
+ readonly schema: { readonly type: 'json' }
154
+ render(args: A, value: V): Array<{ type: 'text'; text: string }>
155
+ }
156
+ execute(args: A, exec: unknown): Promise<V>
157
+ }
158
+
159
+ /** A registry-ready tool definition (structure-compatible with the SDK's). */
160
+ export interface ToolDefinition<A = unknown, V = unknown> {
161
+ readonly name: string
162
+ readonly description: string
163
+ readonly parameters: RawSchema
164
+ readonly output: {
165
+ readonly schema: RawSchema
166
+ render(args: A, value: V): Array<{ type: 'text'; text: string }>
167
+ }
168
+ execute(args: A, exec: unknown): Promise<V>
169
+ }
170
+
171
+ /**
172
+ * Define a first-party tool: compile the parameter spec, pre-validate
173
+ * arguments, and pass through the execution.
174
+ */
175
+ export function defineTool<A extends Record<string, unknown>, V>(options: DefineToolOptions<A, V>): ToolDefinition<A, V> {
176
+ const compiled = compilePropertyMap(options.parameters as Readonly<Record<string, ParameterSpec>>)
177
+ const parameters: RawSchema = { type: 'object', properties: compiled.properties }
178
+ if (compiled.required !== undefined) parameters.required = compiled.required
179
+ const userExecute = options.execute
180
+ return {
181
+ name: options.name,
182
+ description: options.description,
183
+ parameters,
184
+ output: {
185
+ schema: {},
186
+ render(args, value) {
187
+ return options.output.render(args, value)
188
+ },
189
+ },
190
+ async execute(args, exec) {
191
+ const violations = validateValue(parameters, args, 'arguments')
192
+ if (violations.length > 0) {
193
+ throw new Error(`Error: invalid arguments: ${violations.join('; ')}`)
194
+ }
195
+ return userExecute(args, exec)
196
+ },
197
+ }
198
+ }