From d74a0871b4fd5cf458093a471ca338ee81432a8d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 5 May 2026 12:03:46 +0000 Subject: [PATCH] Add SDK package --- .gitignore | 13 ++++++ package.json | 40 +++++++++++++++++ readme.md | 3 ++ test/test.node.ts | 14 ++++++ ts/classes.agentproxy.ts | 36 ++++++++++++++++ ts/classes.automationcontext.ts | 76 +++++++++++++++++++++++++++++++++ ts/classes.deviceproxy.ts | 47 ++++++++++++++++++++ ts/index.ts | 20 +++++++++ ts/plugins.ts | 4 ++ tsconfig.json | 13 ++++++ 10 files changed, 266 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 readme.md create mode 100644 test/test.node.ts create mode 100644 ts/classes.agentproxy.ts create mode 100644 ts/classes.automationcontext.ts create mode 100644 ts/classes.deviceproxy.ts create mode 100644 ts/index.ts create mode 100644 ts/plugins.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4336e0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +dist_*/ +dist_ts/ +coverage/ +.nyc_output/ +.nogit/ +.playwright-mcp/ +*.log +.DS_Store +.env +.env.* +!.env.example diff --git a/package.json b/package.json new file mode 100644 index 0000000..5594bdd --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "@smarthome.exchange/sdk", + "version": "0.1.0", + "private": false, + "description": "Automation authoring SDK for smarthome.exchange.", + "exports": { + ".": "./dist_ts/index.js" + }, + "type": "module", + "author": "Task Venture Capital GmbH", + "license": "MIT", + "scripts": { + "test": "tstest test/ --verbose --logfile --timeout 60", + "build": "tsbuild tsfolders --allowimplicitany", + "buildDocs": "tsdoc" + }, + "dependencies": { + "@smarthome.exchange/interfaces": "workspace:*" + }, + "devDependencies": { + "@git.zone/tsbuild": "^4.4.0", + "@git.zone/tsdoc": "^2.0.3", + "@git.zone/tsrun": "^2.0.3", + "@git.zone/tstest": "^3.6.3", + "@types/node": "^25.6.0" + }, + "files": [ + "ts/**/*", + "dist/**/*", + "dist_*/**/*", + "dist_ts/**/*", + "readme.md", + "changelog.md", + "license" + ], + "browserslist": [ + "last 1 chrome versions" + ], + "packageManager": "pnpm@10.28.2" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..40e24a8 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# @smarthome.exchange/sdk + +TypeScript automation authoring SDK for smarthome.exchange. diff --git a/test/test.node.ts b/test/test.node.ts new file mode 100644 index 0000000..e41119c --- /dev/null +++ b/test/test.node.ts @@ -0,0 +1,14 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { ShxAutomationContext } from '../ts/index.js'; + +tap.test('registers and runs automation handlers', async () => { + const context = new ShxAutomationContext({ callerId: 'test' }); + let ran = false; + context.on({ id: 'manual:test', kind: 'manual', expression: 'test' }, async () => { + ran = true; + }); + await context.runTrigger({ triggerId: 'manual:test', kind: 'manual', payload: {} }); + expect(ran).toBeTrue(); +}); + +export default tap.start(); diff --git a/ts/classes.agentproxy.ts b/ts/classes.agentproxy.ts new file mode 100644 index 0000000..f98509e --- /dev/null +++ b/ts/classes.agentproxy.ts @@ -0,0 +1,36 @@ +import * as plugins from './plugins.js'; +import type { ShxAutomationContext } from './classes.automationcontext.js'; + +export class AgentProxy { + constructor(private context: ShxAutomationContext) {} + + public async decide(optionsArg: { + agentId: string; + goal: string; + scopes: plugins.shxInterfaces.data.TToolScope[]; + input?: Record; + confidence?: number; + }) { + const callId = `call:${Date.now()}:${Math.random().toString(36).slice(2)}`; + const plan: plugins.shxInterfaces.data.IToolPlan = { + id: `plan:${callId}`, + callerId: this.context.options.callerId, + title: `Ask ${optionsArg.agentId}`, + reason: optionsArg.goal, + confidence: optionsArg.confidence ?? 0.65, + calls: [ + { + id: callId, + toolId: `agent:${optionsArg.agentId}:decide`, + callerId: this.context.options.callerId, + input: { + goal: optionsArg.goal, + scopes: optionsArg.scopes, + ...(optionsArg.input ?? {}), + }, + }, + ], + }; + return this.context.executePlan(plan); + } +} diff --git a/ts/classes.automationcontext.ts b/ts/classes.automationcontext.ts new file mode 100644 index 0000000..d2a23d7 --- /dev/null +++ b/ts/classes.automationcontext.ts @@ -0,0 +1,76 @@ +import * as plugins from './plugins.js'; +import { AgentProxy } from './classes.agentproxy.js'; +import { DeviceProxy } from './classes.deviceproxy.js'; + +export type TAutomationEvent = { + triggerId: string; + kind: plugins.shxInterfaces.data.TAutomationTriggerKind; + payload: Record; +}; + +export type TAutomationHandler = ( + eventArg: TAutomationEvent, + contextArg: ShxAutomationContext +) => Promise | void; + +export type TToolPlanExecutor = ( + planArg: plugins.shxInterfaces.data.IToolPlan +) => Promise; + +export interface IShxAutomationContextOptions { + callerId: string; + executePlan?: TToolPlanExecutor; +} + +export interface IAutomationRegistration { + trigger: plugins.shxInterfaces.data.IAutomationTrigger; + handler: TAutomationHandler; +} + +export class ShxAutomationContext { + public devices = new DeviceProxy(this); + public agents = new AgentProxy(this); + private registrations: IAutomationRegistration[] = []; + + constructor(public options: IShxAutomationContextOptions) {} + + public on( + triggerArg: plugins.shxInterfaces.data.IAutomationTrigger, + handlerArg: TAutomationHandler + ) { + this.registrations.push({ + trigger: triggerArg, + handler: handlerArg, + }); + return triggerArg; + } + + public getRegistrations() { + return [...this.registrations]; + } + + public async runTrigger(eventArg: TAutomationEvent) { + const matchingRegistrations = this.registrations.filter((registrationArg) => { + return registrationArg.trigger.id === eventArg.triggerId || registrationArg.trigger.kind === eventArg.kind; + }); + for (const registration of matchingRegistrations) { + await registration.handler(eventArg, this); + } + } + + public async executePlan(planArg: plugins.shxInterfaces.data.IToolPlan) { + if (this.options.executePlan) { + return this.options.executePlan(planArg); + } + return { + planId: planArg.id, + results: planArg.calls.map((callArg: plugins.shxInterfaces.data.IToolCall) => ({ + callId: callArg.id, + status: 'suggested' as const, + output: { + message: 'No hub executor attached. Plan was recorded as a suggestion.', + }, + })), + }; + } +} diff --git a/ts/classes.deviceproxy.ts b/ts/classes.deviceproxy.ts new file mode 100644 index 0000000..84a975d --- /dev/null +++ b/ts/classes.deviceproxy.ts @@ -0,0 +1,47 @@ +import * as plugins from './plugins.js'; +import type { ShxAutomationContext } from './classes.automationcontext.js'; + +export class DeviceProxy { + constructor(private context: ShxAutomationContext) {} + + public async call(optionsArg: { + deviceId: string; + toolId: string; + title: string; + reason: string; + input?: Record; + confidence?: number; + }) { + const callId = `call:${Date.now()}:${Math.random().toString(36).slice(2)}`; + const plan: plugins.shxInterfaces.data.IToolPlan = { + id: `plan:${callId}`, + callerId: this.context.options.callerId, + title: optionsArg.title, + reason: optionsArg.reason, + confidence: optionsArg.confidence ?? 0.5, + calls: [ + { + id: callId, + toolId: optionsArg.toolId, + callerId: this.context.options.callerId, + input: { + deviceId: optionsArg.deviceId, + ...(optionsArg.input ?? {}), + }, + }, + ], + }; + return this.context.executePlan(plan); + } + + public async read(deviceIdArg: string) { + return this.call({ + deviceId: deviceIdArg, + toolId: `device:${deviceIdArg}:read`, + title: `Read ${deviceIdArg}`, + reason: 'Automation requested current device state.', + input: {}, + confidence: 1, + }); + } +} diff --git a/ts/index.ts b/ts/index.ts new file mode 100644 index 0000000..374c351 --- /dev/null +++ b/ts/index.ts @@ -0,0 +1,20 @@ +export * from './classes.automationcontext.js'; +export * from './classes.agentproxy.js'; +export * from './classes.deviceproxy.js'; + +import type * as shxInterfaces from '@smarthome.exchange/interfaces'; +import { ShxAutomationContext, type TAutomationHandler } from './classes.automationcontext.js'; +import { AgentProxy } from './classes.agentproxy.js'; +import { DeviceProxy } from './classes.deviceproxy.js'; + +export const defaultAutomationContext = new ShxAutomationContext({ + callerId: 'automation:default', +}); + +export const on: ( + triggerArg: shxInterfaces.data.IAutomationTrigger, + handlerArg: TAutomationHandler +) => shxInterfaces.data.IAutomationTrigger = defaultAutomationContext.on.bind(defaultAutomationContext); + +export const devices: DeviceProxy = defaultAutomationContext.devices; +export const agents: AgentProxy = defaultAutomationContext.agents; diff --git a/ts/plugins.ts b/ts/plugins.ts new file mode 100644 index 0000000..9c0b14f --- /dev/null +++ b/ts/plugins.ts @@ -0,0 +1,4 @@ +// Project scope +import * as shxInterfaces from '@smarthome.exchange/interfaces'; + +export { shxInterfaces }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7862634 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "types": ["node"] + }, + "exclude": [ + "dist_*/**/*.d.ts" + ] +}