@jhytabest/plashboard 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/openclaw.plugin.json +52 -0
- package/package.json +39 -0
- package/schema/fill-response.schema.json +14 -0
- package/schema/template.schema.json +124 -0
- package/scripts/dashboard_write.py +543 -0
- package/skills/plashboard-admin/SKILL.md +46 -0
- package/src/config.ts +73 -0
- package/src/fill-runner.ts +122 -0
- package/src/index.ts +7 -0
- package/src/json-pointer.ts +102 -0
- package/src/merge.test.ts +65 -0
- package/src/merge.ts +108 -0
- package/src/plugin.ts +272 -0
- package/src/publisher.ts +98 -0
- package/src/runtime.test.ts +163 -0
- package/src/runtime.ts +622 -0
- package/src/schema-validation.ts +35 -0
- package/src/stores.ts +127 -0
- package/src/types.ts +139 -0
- package/src/utils.ts +46 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { FillResponse, FillRunContext, FillRunner, PlashboardConfig } from './types.js';
|
|
3
|
+
|
|
4
|
+
function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
|
|
5
|
+
return {
|
|
6
|
+
instructions: {
|
|
7
|
+
system: [
|
|
8
|
+
'Return JSON only.',
|
|
9
|
+
'Return exactly one object: {"values": {...}}.',
|
|
10
|
+
'Do not include markdown, explanations, or extra keys.'
|
|
11
|
+
],
|
|
12
|
+
error_hint: context.errorHint || ''
|
|
13
|
+
},
|
|
14
|
+
template: {
|
|
15
|
+
id: context.template.id,
|
|
16
|
+
name: context.template.name,
|
|
17
|
+
context: context.template.context || {}
|
|
18
|
+
},
|
|
19
|
+
fields: context.template.fields.map((field) => ({
|
|
20
|
+
id: field.id,
|
|
21
|
+
type: field.type,
|
|
22
|
+
prompt: field.prompt,
|
|
23
|
+
required: field.required !== false,
|
|
24
|
+
constraints: field.constraints || {},
|
|
25
|
+
current_value: context.currentValues[field.id]
|
|
26
|
+
})),
|
|
27
|
+
expected_response_schema: {
|
|
28
|
+
values: {
|
|
29
|
+
'<field_id>': '<typed value>'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mockValue(type: string, currentValue: unknown, fieldId: string): unknown {
|
|
36
|
+
if (type === 'number') return typeof currentValue === 'number' ? currentValue : 0;
|
|
37
|
+
if (type === 'boolean') return typeof currentValue === 'boolean' ? currentValue : false;
|
|
38
|
+
if (type === 'array') return Array.isArray(currentValue) ? currentValue : [];
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
return `updated ${fieldId} at ${now}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class MockFillRunner implements FillRunner {
|
|
44
|
+
async run(context: FillRunContext): Promise<FillResponse> {
|
|
45
|
+
const values: Record<string, unknown> = {};
|
|
46
|
+
for (const field of context.template.fields) {
|
|
47
|
+
values[field.id] = mockValue(field.type, context.currentValues[field.id], field.id);
|
|
48
|
+
}
|
|
49
|
+
return { values };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runCommand(command: string, promptPayload: Record<string, unknown>, timeoutSeconds: number): Promise<string> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(command, {
|
|
56
|
+
shell: true,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
PLASHBOARD_PROMPT_JSON: JSON.stringify(promptPayload)
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
child.kill('SIGKILL');
|
|
65
|
+
reject(new Error(`fill command timed out after ${timeoutSeconds}s`));
|
|
66
|
+
}, timeoutSeconds * 1000);
|
|
67
|
+
|
|
68
|
+
let stdout = '';
|
|
69
|
+
let stderr = '';
|
|
70
|
+
|
|
71
|
+
child.stdout.on('data', (chunk) => {
|
|
72
|
+
stdout += String(chunk);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.stderr.on('data', (chunk) => {
|
|
76
|
+
stderr += String(chunk);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
child.on('error', (error) => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
reject(error);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
child.on('close', (code) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
if (code !== 0) {
|
|
87
|
+
reject(new Error(`fill command failed (code=${code}): ${stderr.trim() || 'no stderr'}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
resolve(stdout.trim());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class CommandFillRunner implements FillRunner {
|
|
96
|
+
constructor(private readonly config: PlashboardConfig) {}
|
|
97
|
+
|
|
98
|
+
async run(context: FillRunContext): Promise<FillResponse> {
|
|
99
|
+
if (!this.config.fill_command) {
|
|
100
|
+
throw new Error('fill_provider=command but fill_command is not configured');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const promptPayload = buildPromptPayload(context);
|
|
104
|
+
const output = await runCommand(this.config.fill_command, promptPayload, this.config.session_timeout_seconds);
|
|
105
|
+
|
|
106
|
+
let parsed: unknown;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(output);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error('fill command returned non-JSON output');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parsed as FillResponse;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createFillRunner(config: PlashboardConfig): FillRunner {
|
|
118
|
+
if (config.fill_provider === 'command') {
|
|
119
|
+
return new CommandFillRunner(config);
|
|
120
|
+
}
|
|
121
|
+
return new MockFillRunner();
|
|
122
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export function decodeToken(token: string): string {
|
|
2
|
+
return token.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function parsePointer(pointer: string): string[] {
|
|
6
|
+
if (!pointer.startsWith('/')) {
|
|
7
|
+
throw new Error(`invalid pointer: ${pointer}`);
|
|
8
|
+
}
|
|
9
|
+
if (pointer === '/') return [''];
|
|
10
|
+
return pointer.slice(1).split('/').map(decodeToken);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isArrayIndex(token: string): boolean {
|
|
14
|
+
return /^\d+$/.test(token);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readPointer(root: unknown, pointer: string): unknown {
|
|
18
|
+
const tokens = parsePointer(pointer);
|
|
19
|
+
let cursor: unknown = root;
|
|
20
|
+
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
if (Array.isArray(cursor)) {
|
|
23
|
+
if (!isArrayIndex(token)) {
|
|
24
|
+
throw new Error(`pointer token must be numeric for array: ${pointer}`);
|
|
25
|
+
}
|
|
26
|
+
const index = Number(token);
|
|
27
|
+
if (!Number.isInteger(index) || index < 0 || index >= cursor.length) {
|
|
28
|
+
throw new Error(`array index out of range for pointer: ${pointer}`);
|
|
29
|
+
}
|
|
30
|
+
cursor = cursor[index];
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cursor && typeof cursor === 'object') {
|
|
35
|
+
const record = cursor as Record<string, unknown>;
|
|
36
|
+
if (!(token in record)) {
|
|
37
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
38
|
+
}
|
|
39
|
+
cursor = record[token];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cursor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writePointer(root: unknown, pointer: string, value: unknown): void {
|
|
50
|
+
const tokens = parsePointer(pointer);
|
|
51
|
+
if (!tokens.length) throw new Error(`invalid pointer: ${pointer}`);
|
|
52
|
+
|
|
53
|
+
let cursor: unknown = root;
|
|
54
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
55
|
+
const token = tokens[index];
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(cursor)) {
|
|
58
|
+
if (!isArrayIndex(token)) {
|
|
59
|
+
throw new Error(`pointer token must be numeric for array: ${pointer}`);
|
|
60
|
+
}
|
|
61
|
+
const arrayIndex = Number(token);
|
|
62
|
+
if (!Number.isInteger(arrayIndex) || arrayIndex < 0 || arrayIndex >= cursor.length) {
|
|
63
|
+
throw new Error(`array index out of range for pointer: ${pointer}`);
|
|
64
|
+
}
|
|
65
|
+
cursor = cursor[arrayIndex];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!cursor || typeof cursor !== 'object') {
|
|
70
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const record = cursor as Record<string, unknown>;
|
|
74
|
+
if (!(token in record)) {
|
|
75
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
76
|
+
}
|
|
77
|
+
cursor = record[token];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const last = tokens[tokens.length - 1];
|
|
81
|
+
if (Array.isArray(cursor)) {
|
|
82
|
+
if (!isArrayIndex(last)) {
|
|
83
|
+
throw new Error(`pointer token must be numeric for array: ${pointer}`);
|
|
84
|
+
}
|
|
85
|
+
const index = Number(last);
|
|
86
|
+
if (!Number.isInteger(index) || index < 0 || index >= cursor.length) {
|
|
87
|
+
throw new Error(`array index out of range for pointer: ${pointer}`);
|
|
88
|
+
}
|
|
89
|
+
cursor[index] = value;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!cursor || typeof cursor !== 'object') {
|
|
94
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const record = cursor as Record<string, unknown>;
|
|
98
|
+
if (!(last in record)) {
|
|
99
|
+
throw new Error(`pointer path not found: ${pointer}`);
|
|
100
|
+
}
|
|
101
|
+
record[last] = value;
|
|
102
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mergeTemplateValues, validateFieldPointers } from './merge.js';
|
|
3
|
+
import type { DashboardTemplate } from './types.js';
|
|
4
|
+
|
|
5
|
+
function baseTemplate(): DashboardTemplate {
|
|
6
|
+
return {
|
|
7
|
+
id: 'ops',
|
|
8
|
+
name: 'Ops',
|
|
9
|
+
enabled: true,
|
|
10
|
+
schedule: {
|
|
11
|
+
mode: 'interval',
|
|
12
|
+
every_minutes: 10,
|
|
13
|
+
timezone: 'Europe/Berlin'
|
|
14
|
+
},
|
|
15
|
+
base_dashboard: {
|
|
16
|
+
title: 'Dashboard',
|
|
17
|
+
summary: 'Old',
|
|
18
|
+
ui: { timezone: 'Europe/Berlin' },
|
|
19
|
+
sections: [
|
|
20
|
+
{
|
|
21
|
+
id: 'sec',
|
|
22
|
+
label: 'Section',
|
|
23
|
+
cards: [
|
|
24
|
+
{
|
|
25
|
+
id: 'card',
|
|
26
|
+
title: 'Card',
|
|
27
|
+
description: 'desc'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
alerts: []
|
|
33
|
+
},
|
|
34
|
+
fields: [
|
|
35
|
+
{
|
|
36
|
+
id: 'summary',
|
|
37
|
+
pointer: '/summary',
|
|
38
|
+
type: 'string',
|
|
39
|
+
prompt: 'Summary',
|
|
40
|
+
required: true,
|
|
41
|
+
constraints: { max_len: 80 }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('mergeTemplateValues', () => {
|
|
48
|
+
it('merges valid values into base dashboard', () => {
|
|
49
|
+
const template = baseTemplate();
|
|
50
|
+
const merged = mergeTemplateValues(template, { summary: 'New summary' });
|
|
51
|
+
expect(merged.summary).toBe('New summary');
|
|
52
|
+
expect(template.base_dashboard.summary).toBe('Old');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects unknown field ids', () => {
|
|
56
|
+
const template = baseTemplate();
|
|
57
|
+
expect(() => mergeTemplateValues(template, { nope: 'x' })).toThrow(/unknown field id/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('validates pointers exist in base dashboard', () => {
|
|
61
|
+
const template = baseTemplate();
|
|
62
|
+
template.fields[0].pointer = '/missing';
|
|
63
|
+
expect(() => validateFieldPointers(template)).toThrow(/pointer path not found/i);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/merge.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { DashboardTemplate, FieldSpec } from './types.js';
|
|
2
|
+
import { deepClone } from './utils.js';
|
|
3
|
+
import { readPointer, writePointer } from './json-pointer.js';
|
|
4
|
+
|
|
5
|
+
function assertFieldValue(field: FieldSpec, value: unknown): void {
|
|
6
|
+
const required = field.required !== false;
|
|
7
|
+
if (value === undefined || value === null) {
|
|
8
|
+
if (required) {
|
|
9
|
+
throw new Error(`missing required value for field ${field.id}`);
|
|
10
|
+
}
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (field.type === 'string') {
|
|
15
|
+
if (typeof value !== 'string') {
|
|
16
|
+
throw new Error(`field ${field.id} expects string`);
|
|
17
|
+
}
|
|
18
|
+
if (field.constraints?.max_len && value.length > field.constraints.max_len) {
|
|
19
|
+
throw new Error(`field ${field.id} exceeds max_len=${field.constraints.max_len}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (field.type === 'number') {
|
|
24
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
25
|
+
throw new Error(`field ${field.id} expects number`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof field.constraints?.min === 'number' && value < field.constraints.min) {
|
|
28
|
+
throw new Error(`field ${field.id} below min=${field.constraints.min}`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof field.constraints?.max === 'number' && value > field.constraints.max) {
|
|
31
|
+
throw new Error(`field ${field.id} above max=${field.constraints.max}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (field.type === 'boolean') {
|
|
36
|
+
if (typeof value !== 'boolean') {
|
|
37
|
+
throw new Error(`field ${field.id} expects boolean`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (field.type === 'array') {
|
|
42
|
+
if (!Array.isArray(value)) {
|
|
43
|
+
throw new Error(`field ${field.id} expects array`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof field.constraints?.min_items === 'number' && value.length < field.constraints.min_items) {
|
|
46
|
+
throw new Error(`field ${field.id} requires at least ${field.constraints.min_items} items`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof field.constraints?.max_items === 'number' && value.length > field.constraints.max_items) {
|
|
49
|
+
throw new Error(`field ${field.id} allows at most ${field.constraints.max_items} items`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (field.constraints?.enum && !field.constraints.enum.includes(value as never)) {
|
|
54
|
+
throw new Error(`field ${field.id} value not in enum`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateFieldPointers(template: DashboardTemplate): void {
|
|
59
|
+
const seenPointers = new Set<string>();
|
|
60
|
+
const seenIds = new Set<string>();
|
|
61
|
+
|
|
62
|
+
for (const field of template.fields) {
|
|
63
|
+
if (seenIds.has(field.id)) {
|
|
64
|
+
throw new Error(`duplicate field id: ${field.id}`);
|
|
65
|
+
}
|
|
66
|
+
seenIds.add(field.id);
|
|
67
|
+
|
|
68
|
+
if (seenPointers.has(field.pointer)) {
|
|
69
|
+
throw new Error(`duplicate field pointer: ${field.pointer}`);
|
|
70
|
+
}
|
|
71
|
+
seenPointers.add(field.pointer);
|
|
72
|
+
|
|
73
|
+
readPointer(template.base_dashboard, field.pointer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function mergeTemplateValues(
|
|
78
|
+
template: DashboardTemplate,
|
|
79
|
+
values: Record<string, unknown>
|
|
80
|
+
): Record<string, unknown> {
|
|
81
|
+
const next = deepClone(template.base_dashboard);
|
|
82
|
+
const knownIds = new Set(template.fields.map((field) => field.id));
|
|
83
|
+
|
|
84
|
+
for (const fieldId of Object.keys(values)) {
|
|
85
|
+
if (!knownIds.has(fieldId)) {
|
|
86
|
+
throw new Error(`unknown field id in fill response: ${fieldId}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const field of template.fields) {
|
|
91
|
+
const value = values[field.id];
|
|
92
|
+
assertFieldValue(field, value);
|
|
93
|
+
|
|
94
|
+
if (value !== undefined && value !== null) {
|
|
95
|
+
writePointer(next, field.pointer, value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function collectCurrentValues(template: DashboardTemplate): Record<string, unknown> {
|
|
103
|
+
const values: Record<string, unknown> = {};
|
|
104
|
+
for (const field of template.fields) {
|
|
105
|
+
values[field.id] = readPointer(template.base_dashboard, field.pointer);
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { DisplayProfile, ToolResponse } from './types.js';
|
|
2
|
+
import { resolveConfig } from './config.js';
|
|
3
|
+
import { PlashboardRuntime } from './runtime.js';
|
|
4
|
+
|
|
5
|
+
type UnknownApi = {
|
|
6
|
+
registerTool?: (definition: unknown) => void;
|
|
7
|
+
registerCommand?: (definition: unknown) => void;
|
|
8
|
+
registerService?: (definition: unknown) => void;
|
|
9
|
+
logger?: {
|
|
10
|
+
info?: (...args: unknown[]) => void;
|
|
11
|
+
warn?: (...args: unknown[]) => void;
|
|
12
|
+
error?: (...args: unknown[]) => void;
|
|
13
|
+
};
|
|
14
|
+
config?: unknown;
|
|
15
|
+
pluginConfig?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function toToolResult<T>(payload: ToolResponse<T>) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify(payload)
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toCommandResult<T>(payload: ToolResponse<T>) {
|
|
30
|
+
return {
|
|
31
|
+
text: JSON.stringify(payload)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function asObject(value: unknown): Record<string, unknown> {
|
|
36
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
37
|
+
? (value as Record<string, unknown>)
|
|
38
|
+
: {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function asString(value: unknown): string {
|
|
42
|
+
return typeof value === 'string' ? value : '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
46
|
+
const config = resolveConfig(api);
|
|
47
|
+
const runtime = new PlashboardRuntime(config, {
|
|
48
|
+
info: (...args) => api.logger?.info?.(...args),
|
|
49
|
+
warn: (...args) => api.logger?.warn?.(...args),
|
|
50
|
+
error: (...args) => api.logger?.error?.(...args)
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
api.registerService?.({
|
|
54
|
+
id: 'plashboard-scheduler',
|
|
55
|
+
async start() {
|
|
56
|
+
await runtime.start();
|
|
57
|
+
},
|
|
58
|
+
async stop() {
|
|
59
|
+
await runtime.stop();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
api.registerTool?.({
|
|
64
|
+
name: 'plashboard_init',
|
|
65
|
+
description: 'Initialize plashboard state directories and optional default template.',
|
|
66
|
+
optional: true,
|
|
67
|
+
execute: async () => toToolResult(await runtime.init())
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
api.registerTool?.({
|
|
71
|
+
name: 'plashboard_template_create',
|
|
72
|
+
description: 'Create a new dashboard template.',
|
|
73
|
+
optional: true,
|
|
74
|
+
parameters: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
required: ['template'],
|
|
77
|
+
properties: {
|
|
78
|
+
template: { type: 'object' }
|
|
79
|
+
},
|
|
80
|
+
additionalProperties: false
|
|
81
|
+
},
|
|
82
|
+
execute: async (_toolCallId: unknown, params: { template?: unknown } = {}) =>
|
|
83
|
+
toToolResult(await runtime.templateCreate(params.template))
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
api.registerTool?.({
|
|
87
|
+
name: 'plashboard_template_update',
|
|
88
|
+
description: 'Update an existing dashboard template.',
|
|
89
|
+
optional: true,
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
required: ['template_id', 'template'],
|
|
93
|
+
properties: {
|
|
94
|
+
template_id: { type: 'string' },
|
|
95
|
+
template: { type: 'object' }
|
|
96
|
+
},
|
|
97
|
+
additionalProperties: false
|
|
98
|
+
},
|
|
99
|
+
execute: async (_toolCallId: unknown, params: { template_id?: string; template?: unknown } = {}) =>
|
|
100
|
+
toToolResult(await runtime.templateUpdate(params.template_id || '', params.template))
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
api.registerTool?.({
|
|
104
|
+
name: 'plashboard_template_list',
|
|
105
|
+
description: 'List available dashboard templates with schedule and run state.',
|
|
106
|
+
optional: true,
|
|
107
|
+
execute: async () => toToolResult(await runtime.templateList())
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
api.registerTool?.({
|
|
111
|
+
name: 'plashboard_template_activate',
|
|
112
|
+
description: 'Set active dashboard template.',
|
|
113
|
+
optional: true,
|
|
114
|
+
parameters: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
required: ['template_id'],
|
|
117
|
+
properties: {
|
|
118
|
+
template_id: { type: 'string' }
|
|
119
|
+
},
|
|
120
|
+
additionalProperties: false
|
|
121
|
+
},
|
|
122
|
+
execute: async (_toolCallId: unknown, params: { template_id?: string } = {}) =>
|
|
123
|
+
toToolResult(await runtime.templateActivate(params.template_id || ''))
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
api.registerTool?.({
|
|
127
|
+
name: 'plashboard_template_delete',
|
|
128
|
+
description: 'Delete a dashboard template by id.',
|
|
129
|
+
optional: true,
|
|
130
|
+
parameters: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
required: ['template_id'],
|
|
133
|
+
properties: {
|
|
134
|
+
template_id: { type: 'string' }
|
|
135
|
+
},
|
|
136
|
+
additionalProperties: false
|
|
137
|
+
},
|
|
138
|
+
execute: async (_toolCallId: unknown, params: { template_id?: string } = {}) =>
|
|
139
|
+
toToolResult(await runtime.templateDelete(params.template_id || ''))
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
api.registerTool?.({
|
|
143
|
+
name: 'plashboard_template_copy',
|
|
144
|
+
description: 'Copy a dashboard template into a new template id.',
|
|
145
|
+
optional: true,
|
|
146
|
+
parameters: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
required: ['source_template_id', 'new_template_id'],
|
|
149
|
+
properties: {
|
|
150
|
+
source_template_id: { type: 'string' },
|
|
151
|
+
new_template_id: { type: 'string' },
|
|
152
|
+
new_name: { type: 'string' },
|
|
153
|
+
activate: { type: 'boolean' }
|
|
154
|
+
},
|
|
155
|
+
additionalProperties: false
|
|
156
|
+
},
|
|
157
|
+
execute: async (
|
|
158
|
+
_toolCallId: unknown,
|
|
159
|
+
params: {
|
|
160
|
+
source_template_id?: string;
|
|
161
|
+
new_template_id?: string;
|
|
162
|
+
new_name?: string;
|
|
163
|
+
activate?: boolean;
|
|
164
|
+
} = {}
|
|
165
|
+
) =>
|
|
166
|
+
toToolResult(
|
|
167
|
+
await runtime.templateCopy(
|
|
168
|
+
params.source_template_id || '',
|
|
169
|
+
params.new_template_id || '',
|
|
170
|
+
params.new_name,
|
|
171
|
+
Boolean(params.activate)
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
api.registerTool?.({
|
|
177
|
+
name: 'plashboard_template_validate',
|
|
178
|
+
description: 'Validate a dashboard template payload without saving.',
|
|
179
|
+
optional: true,
|
|
180
|
+
parameters: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
required: ['template'],
|
|
183
|
+
properties: {
|
|
184
|
+
template: { type: 'object' }
|
|
185
|
+
},
|
|
186
|
+
additionalProperties: false
|
|
187
|
+
},
|
|
188
|
+
execute: async (_toolCallId: unknown, params: { template?: unknown } = {}) =>
|
|
189
|
+
toToolResult(await runtime.templateValidate(params.template))
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
api.registerTool?.({
|
|
193
|
+
name: 'plashboard_run_now',
|
|
194
|
+
description: 'Run fill pipeline for a template immediately.',
|
|
195
|
+
optional: true,
|
|
196
|
+
parameters: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
required: ['template_id'],
|
|
199
|
+
properties: {
|
|
200
|
+
template_id: { type: 'string' }
|
|
201
|
+
},
|
|
202
|
+
additionalProperties: false
|
|
203
|
+
},
|
|
204
|
+
execute: async (_toolCallId: unknown, params: { template_id?: string } = {}) =>
|
|
205
|
+
toToolResult(await runtime.runNow(params.template_id || ''))
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
api.registerTool?.({
|
|
209
|
+
name: 'plashboard_status',
|
|
210
|
+
description: 'Read current plashboard runtime status.',
|
|
211
|
+
optional: true,
|
|
212
|
+
execute: async () => toToolResult(await runtime.status())
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
api.registerTool?.({
|
|
216
|
+
name: 'plashboard_display_profile_set',
|
|
217
|
+
description: 'Update display profile for layout budget enforcement.',
|
|
218
|
+
optional: true,
|
|
219
|
+
parameters: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
width_px: { type: 'number' },
|
|
223
|
+
height_px: { type: 'number' },
|
|
224
|
+
safe_top_px: { type: 'number' },
|
|
225
|
+
safe_bottom_px: { type: 'number' },
|
|
226
|
+
safe_side_px: { type: 'number' },
|
|
227
|
+
layout_safety_margin_px: { type: 'number' }
|
|
228
|
+
},
|
|
229
|
+
additionalProperties: false
|
|
230
|
+
},
|
|
231
|
+
execute: async (_toolCallId: unknown, params: Partial<DisplayProfile> = {}) =>
|
|
232
|
+
toToolResult(await runtime.displayProfileSet(params))
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
api.registerCommand?.({
|
|
236
|
+
name: 'plashboard',
|
|
237
|
+
description: 'Plashboard admin command wrapper for common runtime operations.',
|
|
238
|
+
acceptsArgs: true,
|
|
239
|
+
handler: async (ctx: { args?: string }) => {
|
|
240
|
+
const args = asString(ctx.args).split(/\s+/).filter(Boolean);
|
|
241
|
+
const [cmd, ...rest] = args;
|
|
242
|
+
|
|
243
|
+
if (cmd === 'init') return toCommandResult(await runtime.init());
|
|
244
|
+
if (cmd === 'status') return toCommandResult(await runtime.status());
|
|
245
|
+
if (cmd === 'list') return toCommandResult(await runtime.templateList());
|
|
246
|
+
if (cmd === 'activate') return toCommandResult(await runtime.templateActivate(rest[0] || ''));
|
|
247
|
+
if (cmd === 'delete') return toCommandResult(await runtime.templateDelete(rest[0] || ''));
|
|
248
|
+
if (cmd === 'copy') {
|
|
249
|
+
return toCommandResult(
|
|
250
|
+
await runtime.templateCopy(rest[0] || '', rest[1] || '', rest[2] || undefined, rest[3] === 'activate')
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (cmd === 'run') return toCommandResult(await runtime.runNow(rest[0] || ''));
|
|
254
|
+
if (cmd === 'set-display') {
|
|
255
|
+
const input = asObject({
|
|
256
|
+
width_px: rest[0] ? Number(rest[0]) : undefined,
|
|
257
|
+
height_px: rest[1] ? Number(rest[1]) : undefined,
|
|
258
|
+
safe_top_px: rest[2] ? Number(rest[2]) : undefined,
|
|
259
|
+
safe_bottom_px: rest[3] ? Number(rest[3]) : undefined
|
|
260
|
+
});
|
|
261
|
+
return toCommandResult(await runtime.displayProfileSet(input as Partial<DisplayProfile>));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return toCommandResult({
|
|
265
|
+
ok: false,
|
|
266
|
+
errors: [
|
|
267
|
+
'unknown command. supported: init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
|
|
268
|
+
]
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|