@jhytabest/plashboard 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -35,7 +35,8 @@ Add to `openclaw.json`:
35
35
  "default_retry_count": 1,
36
36
  "retry_backoff_seconds": 20,
37
37
  "session_timeout_seconds": 90,
38
- "fill_provider": "mock",
38
+ "fill_provider": "openclaw",
39
+ "openclaw_fill_agent_id": "main",
39
40
  "display_profile": {
40
41
  "width_px": 1920,
41
42
  "height_px": 1080,
@@ -51,12 +52,13 @@ Add to `openclaw.json`:
51
52
  }
52
53
  ```
53
54
 
54
- For real model runs, switch `fill_provider` to `command` and provide `fill_command`.
55
+ `fill_provider: "openclaw"` is the default real mode and calls `openclaw agent` directly.
56
+ Use `fill_provider: "command"` only if you need a custom external runner.
55
57
 
56
58
  ## Runtime Command
57
59
 
58
60
  ```text
59
- /plashboard setup [mock|command <fill_command>]
61
+ /plashboard setup [openclaw [agent_id]|mock|command <fill_command>]
60
62
  /plashboard expose-guide [local_url] [https_port]
61
63
  /plashboard expose-check [local_url] [https_port]
62
64
  /plashboard init
@@ -72,7 +74,7 @@ For real model runs, switch `fill_provider` to `command` and provide `fill_comma
72
74
  Recommended first run:
73
75
 
74
76
  ```text
75
- /plashboard setup mock
77
+ /plashboard setup openclaw
76
78
  /plashboard init
77
79
  ```
78
80
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "plashboard",
3
3
  "name": "Plashboard",
4
- "version": "0.1.0",
4
+ "version": "0.1.6",
5
5
  "description": "Template-driven dashboard runtime with scheduled OpenClaw fills and safe publish.",
6
6
  "entry": "./src/index.ts",
7
7
  "skills": ["./skills/plashboard-admin"],
@@ -15,8 +15,9 @@
15
15
  "default_retry_count": { "type": "integer", "minimum": 0, "maximum": 5, "default": 1 },
16
16
  "retry_backoff_seconds": { "type": "integer", "minimum": 1, "maximum": 300, "default": 20 },
17
17
  "session_timeout_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 90 },
18
- "fill_provider": { "type": "string", "enum": ["command", "mock"], "default": "mock" },
18
+ "fill_provider": { "type": "string", "enum": ["command", "mock", "openclaw"], "default": "openclaw" },
19
19
  "fill_command": { "type": "string" },
20
+ "openclaw_fill_agent_id": { "type": "string", "default": "main" },
20
21
  "python_bin": { "type": "string", "default": "python3" },
21
22
  "writer_script_path": { "type": "string" },
22
23
  "dashboard_output_path": { "type": "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhytabest/plashboard",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "description": "Plashboard OpenClaw plugin runtime",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@ Always use plugin tools:
39
39
  - Never ask the model to generate full dashboard structure when filling values.
40
40
 
41
41
  ## Command Shortcuts
42
- - `/plashboard setup [mock|command <fill_command>]`
42
+ - `/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]`
43
43
  - `/plashboard expose-guide [local_url] [https_port]`
44
44
  - `/plashboard expose-check [local_url] [https_port]`
45
45
  - `/plashboard init`
package/src/config.ts CHANGED
@@ -50,8 +50,12 @@ export function resolveConfig(api: unknown): PlashboardConfig {
50
50
 
51
51
  const dataDir = asString(raw.data_dir, '/var/lib/openclaw/plash-data');
52
52
  const outputPath = asString(raw.dashboard_output_path, join(dataDir, 'dashboard.json'));
53
- const fillProviderRaw = asString(raw.fill_provider, 'mock');
54
- const fillProvider = fillProviderRaw === 'command' ? 'command' : 'mock';
53
+ const fillProviderRaw = asString(raw.fill_provider, 'openclaw');
54
+ const fillProvider = fillProviderRaw === 'command'
55
+ ? 'command'
56
+ : fillProviderRaw === 'mock'
57
+ ? 'mock'
58
+ : 'openclaw';
55
59
 
56
60
  return {
57
61
  data_dir: dataDir,
@@ -63,6 +67,7 @@ export function resolveConfig(api: unknown): PlashboardConfig {
63
67
  session_timeout_seconds: Math.max(10, Math.floor(asNumber(raw.session_timeout_seconds, 90))),
64
68
  fill_provider: fillProvider,
65
69
  fill_command: typeof raw.fill_command === 'string' ? raw.fill_command : undefined,
70
+ openclaw_fill_agent_id: asString(raw.openclaw_fill_agent_id, 'main'),
66
71
  python_bin: asString(raw.python_bin, 'python3'),
67
72
  writer_script_path: asString(raw.writer_script_path, DEFAULT_WRITER_PATH),
68
73
  dashboard_output_path: outputPath,
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createFillRunner } from './fill-runner.js';
3
+ import type { DashboardTemplate, FillRunContext, PlashboardConfig } from './types.js';
4
+
5
+ function template(): DashboardTemplate {
6
+ return {
7
+ id: 'ops',
8
+ name: 'Ops',
9
+ enabled: true,
10
+ schedule: {
11
+ mode: 'interval',
12
+ every_minutes: 5,
13
+ timezone: 'UTC'
14
+ },
15
+ base_dashboard: {
16
+ title: 'Ops',
17
+ summary: 'old summary'
18
+ },
19
+ fields: [
20
+ {
21
+ id: 'summary',
22
+ pointer: '/summary',
23
+ type: 'string',
24
+ prompt: 'Summarize current status',
25
+ required: true
26
+ }
27
+ ]
28
+ };
29
+ }
30
+
31
+ function config(overrides: Partial<PlashboardConfig>): PlashboardConfig {
32
+ return {
33
+ data_dir: '/tmp/plash-test',
34
+ timezone: 'UTC',
35
+ scheduler_tick_seconds: 30,
36
+ max_parallel_runs: 1,
37
+ default_retry_count: 0,
38
+ retry_backoff_seconds: 1,
39
+ session_timeout_seconds: 30,
40
+ fill_provider: 'mock',
41
+ fill_command: undefined,
42
+ openclaw_fill_agent_id: 'main',
43
+ python_bin: 'python3',
44
+ writer_script_path: '/tmp/writer.py',
45
+ dashboard_output_path: '/tmp/dashboard.json',
46
+ layout_overflow_tolerance_px: 40,
47
+ display_profile: {
48
+ width_px: 1920,
49
+ height_px: 1080,
50
+ safe_top_px: 96,
51
+ safe_bottom_px: 106,
52
+ safe_side_px: 28,
53
+ layout_safety_margin_px: 24
54
+ },
55
+ model_defaults: {},
56
+ ...overrides
57
+ };
58
+ }
59
+
60
+ function context(): FillRunContext {
61
+ return {
62
+ template: template(),
63
+ currentValues: { summary: 'old summary' },
64
+ attempt: 1
65
+ };
66
+ }
67
+
68
+ describe('createFillRunner', () => {
69
+ it('parses openclaw json envelope output', async () => {
70
+ const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
71
+ stdout: JSON.stringify({
72
+ result: {
73
+ payloads: [
74
+ {
75
+ text: '{"values":{"summary":"new summary"}}'
76
+ }
77
+ ]
78
+ }
79
+ }),
80
+ stderr: '',
81
+ code: 0
82
+ }));
83
+
84
+ const runner = createFillRunner(
85
+ config({ fill_provider: 'openclaw', openclaw_fill_agent_id: 'ops' }),
86
+ { commandRunner }
87
+ );
88
+ const response = await runner.run(context());
89
+
90
+ expect(response.values.summary).toBe('new summary');
91
+ expect(commandRunner).toHaveBeenCalledTimes(1);
92
+ const firstCall = commandRunner.mock.calls[0];
93
+ const argv = firstCall[0];
94
+ expect(argv.slice(0, 2)).toEqual(['openclaw', 'agent']);
95
+ expect(argv).toContain('--agent');
96
+ expect(argv).toContain('ops');
97
+ });
98
+
99
+ it('parses command runner fenced json output', async () => {
100
+ const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
101
+ stdout: '```json\n{"values":{"summary":"from command"}}\n```',
102
+ stderr: '',
103
+ code: 0
104
+ }));
105
+
106
+ const runner = createFillRunner(
107
+ config({ fill_provider: 'command', fill_command: 'echo "$PLASHBOARD_PROMPT_JSON"' }),
108
+ { commandRunner }
109
+ );
110
+ const response = await runner.run(context());
111
+
112
+ expect(response.values.summary).toBe('from command');
113
+ expect(commandRunner).toHaveBeenCalledTimes(1);
114
+ const firstCall = commandRunner.mock.calls[0];
115
+ const argv = firstCall[0];
116
+ expect(argv.slice(0, 2)).toEqual(['sh', '-lc']);
117
+ const options = firstCall[1] as { env?: Record<string, string> };
118
+ expect(options.env?.PLASHBOARD_PROMPT_JSON).toContain('"template"');
119
+ });
120
+ });
@@ -1,6 +1,30 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import type { FillResponse, FillRunContext, FillRunner, PlashboardConfig } from './types.js';
3
3
 
4
+ type CommandOptions = {
5
+ timeoutMs: number;
6
+ env?: NodeJS.ProcessEnv;
7
+ input?: string;
8
+ };
9
+
10
+ export type CommandRunResult = {
11
+ stdout: string;
12
+ stderr: string;
13
+ code: number | null;
14
+ signal?: NodeJS.Signals | null;
15
+ killed?: boolean;
16
+ termination?: 'exit' | 'timeout' | 'signal' | string;
17
+ };
18
+
19
+ export type CommandRunner = (
20
+ argv: string[],
21
+ optionsOrTimeout: number | CommandOptions
22
+ ) => Promise<CommandRunResult>;
23
+
24
+ export interface FillRunnerDeps {
25
+ commandRunner?: CommandRunner;
26
+ }
27
+
4
28
  function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
5
29
  return {
6
30
  instructions: {
@@ -32,6 +56,16 @@ function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
32
56
  };
33
57
  }
34
58
 
59
+ function buildOpenClawMessage(context: FillRunContext): string {
60
+ const payload = buildPromptPayload(context);
61
+ return [
62
+ 'Fill dashboard fields from the provided template context.',
63
+ 'Return exactly one JSON object with this shape: {"values": {...}}.',
64
+ 'Do not include markdown, comments, explanations, or extra keys.',
65
+ JSON.stringify(payload)
66
+ ].join('\n\n');
67
+ }
68
+
35
69
  function mockValue(type: string, currentValue: unknown, fieldId: string): unknown {
36
70
  if (type === 'number') return typeof currentValue === 'number' ? currentValue : 0;
37
71
  if (type === 'boolean') return typeof currentValue === 'boolean' ? currentValue : false;
@@ -40,30 +74,35 @@ function mockValue(type: string, currentValue: unknown, fieldId: string): unknow
40
74
  return `updated ${fieldId} at ${now}`;
41
75
  }
42
76
 
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 };
77
+ function normalizeCommandOptions(optionsOrTimeout: number | CommandOptions): CommandOptions {
78
+ if (typeof optionsOrTimeout === 'number') {
79
+ return { timeoutMs: optionsOrTimeout };
50
80
  }
81
+ return { timeoutMs: optionsOrTimeout.timeoutMs, env: optionsOrTimeout.env, input: optionsOrTimeout.input };
51
82
  }
52
83
 
53
- function runCommand(command: string, promptPayload: Record<string, unknown>, timeoutSeconds: number): Promise<string> {
84
+ function defaultCommandRunner(argv: string[], optionsOrTimeout: number | CommandOptions): Promise<CommandRunResult> {
85
+ const options = normalizeCommandOptions(optionsOrTimeout);
54
86
  return new Promise((resolve, reject) => {
55
- const child = spawn(command, {
56
- shell: true,
87
+ if (!Array.isArray(argv) || argv.length === 0 || !argv[0]) {
88
+ reject(new Error('command argv must include a binary name'));
89
+ return;
90
+ }
91
+
92
+ const child = spawn(argv[0], argv.slice(1), {
57
93
  env: {
58
94
  ...process.env,
59
- PLASHBOARD_PROMPT_JSON: JSON.stringify(promptPayload)
60
- }
95
+ ...(options.env || {})
96
+ },
97
+ stdio: 'pipe'
61
98
  });
62
99
 
100
+ const timeoutMs = Math.max(1000, Math.floor(options.timeoutMs));
101
+ let terminatedByTimeout = false;
63
102
  const timer = setTimeout(() => {
103
+ terminatedByTimeout = true;
64
104
  child.kill('SIGKILL');
65
- reject(new Error(`fill command timed out after ${timeoutSeconds}s`));
66
- }, timeoutSeconds * 1000);
105
+ }, timeoutMs);
67
106
 
68
107
  let stdout = '';
69
108
  let stderr = '';
@@ -71,7 +110,6 @@ function runCommand(command: string, promptPayload: Record<string, unknown>, tim
71
110
  child.stdout.on('data', (chunk) => {
72
111
  stdout += String(chunk);
73
112
  });
74
-
75
113
  child.stderr.on('data', (chunk) => {
76
114
  stderr += String(chunk);
77
115
  });
@@ -81,42 +119,195 @@ function runCommand(command: string, promptPayload: Record<string, unknown>, tim
81
119
  reject(error);
82
120
  });
83
121
 
84
- child.on('close', (code) => {
122
+ child.on('close', (code, signal) => {
85
123
  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());
124
+ resolve({
125
+ stdout: stdout.trim(),
126
+ stderr: stderr.trim(),
127
+ code,
128
+ signal,
129
+ killed: terminatedByTimeout || code === null,
130
+ termination: terminatedByTimeout ? 'timeout' : signal ? 'signal' : 'exit'
131
+ });
91
132
  });
133
+
134
+ if (typeof options.input === 'string') {
135
+ child.stdin.write(options.input);
136
+ }
137
+ child.stdin.end();
92
138
  });
93
139
  }
94
140
 
141
+ function tryParseJson(input: string): unknown | undefined {
142
+ try {
143
+ return JSON.parse(input);
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ function stripCodeFence(input: string): string {
150
+ const trimmed = input.trim();
151
+ if (!trimmed.startsWith('```') || !trimmed.endsWith('```')) return trimmed;
152
+ const lines = trimmed.split('\n');
153
+ if (lines.length < 3) return trimmed;
154
+ return lines.slice(1, -1).join('\n').trim();
155
+ }
156
+
157
+ function parseJsonCandidate(input: string): unknown | undefined {
158
+ const trimmed = input.trim();
159
+ if (!trimmed) return undefined;
160
+
161
+ const direct = tryParseJson(trimmed);
162
+ if (direct !== undefined) return direct;
163
+
164
+ const unfenced = stripCodeFence(trimmed);
165
+ if (unfenced !== trimmed) {
166
+ const parsed = tryParseJson(unfenced);
167
+ if (parsed !== undefined) return parsed;
168
+ }
169
+
170
+ const start = trimmed.indexOf('{');
171
+ const end = trimmed.lastIndexOf('}');
172
+ if (start >= 0 && end > start) {
173
+ const maybeObject = trimmed.slice(start, end + 1);
174
+ const parsed = tryParseJson(maybeObject);
175
+ if (parsed !== undefined) return parsed;
176
+ }
177
+
178
+ return undefined;
179
+ }
180
+
181
+ function asObject(value: unknown): Record<string, unknown> | null {
182
+ return value && typeof value === 'object' && !Array.isArray(value)
183
+ ? (value as Record<string, unknown>)
184
+ : null;
185
+ }
186
+
187
+ function extractFillResponse(value: unknown, depth = 0): FillResponse | null {
188
+ if (depth > 10) return null;
189
+
190
+ if (typeof value === 'string') {
191
+ const parsed = parseJsonCandidate(value);
192
+ if (parsed !== undefined) {
193
+ return extractFillResponse(parsed, depth + 1);
194
+ }
195
+ return null;
196
+ }
197
+
198
+ if (Array.isArray(value)) {
199
+ for (const item of value) {
200
+ const found = extractFillResponse(item, depth + 1);
201
+ if (found) return found;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ const objectValue = asObject(value);
207
+ if (!objectValue) return null;
208
+
209
+ const valuesRecord = asObject(objectValue.values);
210
+ if (valuesRecord) {
211
+ return { values: valuesRecord };
212
+ }
213
+
214
+ for (const nested of Object.values(objectValue)) {
215
+ const found = extractFillResponse(nested, depth + 1);
216
+ if (found) return found;
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function parseFillResponse(output: string, source: string): FillResponse {
222
+ const extracted = extractFillResponse(output);
223
+ if (!extracted) {
224
+ throw new Error(`${source} output did not include a valid {"values": ...} JSON object`);
225
+ }
226
+ return extracted;
227
+ }
228
+
229
+ async function runAndReadStdout(
230
+ commandRunner: CommandRunner,
231
+ argv: string[],
232
+ optionsOrTimeout: number | CommandOptions,
233
+ label: string
234
+ ): Promise<string> {
235
+ const result = await commandRunner(argv, optionsOrTimeout);
236
+ if (result.code !== 0) {
237
+ const reason = result.stderr || result.stdout || result.termination || `exit=${String(result.code)}`;
238
+ throw new Error(`${label} failed: ${reason}`);
239
+ }
240
+ return result.stdout.trim();
241
+ }
242
+
243
+ class MockFillRunner implements FillRunner {
244
+ async run(context: FillRunContext): Promise<FillResponse> {
245
+ const values: Record<string, unknown> = {};
246
+ for (const field of context.template.fields) {
247
+ values[field.id] = mockValue(field.type, context.currentValues[field.id], field.id);
248
+ }
249
+ return { values };
250
+ }
251
+ }
252
+
95
253
  class CommandFillRunner implements FillRunner {
96
- constructor(private readonly config: PlashboardConfig) {}
254
+ constructor(
255
+ private readonly config: PlashboardConfig,
256
+ private readonly commandRunner: CommandRunner
257
+ ) {}
97
258
 
98
259
  async run(context: FillRunContext): Promise<FillResponse> {
99
260
  if (!this.config.fill_command) {
100
261
  throw new Error('fill_provider=command but fill_command is not configured');
101
262
  }
102
263
 
103
- const promptPayload = buildPromptPayload(context);
104
- const output = await runCommand(this.config.fill_command, promptPayload, this.config.session_timeout_seconds);
264
+ const output = await runAndReadStdout(
265
+ this.commandRunner,
266
+ ['sh', '-lc', this.config.fill_command],
267
+ {
268
+ timeoutMs: this.config.session_timeout_seconds * 1000,
269
+ env: {
270
+ PLASHBOARD_PROMPT_JSON: JSON.stringify(buildPromptPayload(context))
271
+ }
272
+ },
273
+ 'fill command'
274
+ );
105
275
 
106
- let parsed: unknown;
107
- try {
108
- parsed = JSON.parse(output);
109
- } catch {
110
- throw new Error('fill command returned non-JSON output');
111
- }
276
+ return parseFillResponse(output, 'fill command');
277
+ }
278
+ }
279
+
280
+ class OpenClawFillRunner implements FillRunner {
281
+ constructor(
282
+ private readonly config: PlashboardConfig,
283
+ private readonly commandRunner: CommandRunner
284
+ ) {}
112
285
 
113
- return parsed as FillResponse;
286
+ async run(context: FillRunContext): Promise<FillResponse> {
287
+ const agentId = (this.config.openclaw_fill_agent_id || 'main').trim() || 'main';
288
+ const timeoutSeconds = Math.max(10, Math.floor(this.config.session_timeout_seconds));
289
+ const message = buildOpenClawMessage(context);
290
+
291
+ const output = await runAndReadStdout(
292
+ this.commandRunner,
293
+ ['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)],
294
+ {
295
+ timeoutMs: (timeoutSeconds + 30) * 1000
296
+ },
297
+ 'openclaw fill'
298
+ );
299
+
300
+ return parseFillResponse(output, 'openclaw fill');
114
301
  }
115
302
  }
116
303
 
117
- export function createFillRunner(config: PlashboardConfig): FillRunner {
304
+ export function createFillRunner(config: PlashboardConfig, deps: FillRunnerDeps = {}): FillRunner {
305
+ const commandRunner = deps.commandRunner || defaultCommandRunner;
118
306
  if (config.fill_provider === 'command') {
119
- return new CommandFillRunner(config);
307
+ return new CommandFillRunner(config, commandRunner);
308
+ }
309
+ if (config.fill_provider === 'openclaw') {
310
+ return new OpenClawFillRunner(config, commandRunner);
120
311
  }
121
312
  return new MockFillRunner();
122
313
  }
package/src/plugin.ts CHANGED
@@ -19,6 +19,26 @@ type UnknownApi = {
19
19
  loadConfig?: () => unknown;
20
20
  writeConfigFile?: (nextConfig: unknown) => Promise<void>;
21
21
  };
22
+ system?: {
23
+ runCommandWithTimeout?: (
24
+ argv: string[],
25
+ optionsOrTimeout: number | {
26
+ timeoutMs: number;
27
+ cwd?: string;
28
+ input?: string;
29
+ env?: NodeJS.ProcessEnv;
30
+ windowsVerbatimArguments?: boolean;
31
+ noOutputTimeoutMs?: number;
32
+ }
33
+ ) => Promise<{
34
+ stdout: string;
35
+ stderr: string;
36
+ code: number | null;
37
+ signal?: NodeJS.Signals | null;
38
+ killed?: boolean;
39
+ termination?: string;
40
+ }>;
41
+ };
22
42
  };
23
43
  config?: unknown;
24
44
  pluginConfig?: unknown;
@@ -67,8 +87,9 @@ function asErrorMessage(error: unknown): string {
67
87
  }
68
88
 
69
89
  type SetupParams = {
70
- fill_provider?: 'mock' | 'command';
90
+ fill_provider?: 'mock' | 'command' | 'openclaw';
71
91
  fill_command?: string;
92
+ openclaw_fill_agent_id?: string;
72
93
  data_dir?: string;
73
94
  scheduler_tick_seconds?: number;
74
95
  session_timeout_seconds?: number;
@@ -325,14 +346,23 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
325
346
  )
326
347
  };
327
348
 
349
+ const currentProvider = asString(currentPluginConfig.fill_provider);
328
350
  const selectedProvider =
329
351
  params.fill_provider
330
- || (currentPluginConfig.fill_provider === 'command' ? 'command' : currentPluginConfig.fill_provider === 'mock' ? 'mock' : resolvedConfig.fill_provider);
352
+ || (currentProvider === 'command' || currentProvider === 'mock' || currentProvider === 'openclaw'
353
+ ? currentProvider
354
+ : resolvedConfig.fill_provider);
331
355
  const selectedCommand = (
332
356
  params.fill_command
333
357
  || asString(currentPluginConfig.fill_command)
334
358
  || asString(resolvedConfig.fill_command)
335
359
  ).trim();
360
+ const selectedAgentId = (
361
+ params.openclaw_fill_agent_id
362
+ || asString(currentPluginConfig.openclaw_fill_agent_id)
363
+ || asString(resolvedConfig.openclaw_fill_agent_id)
364
+ || 'main'
365
+ ).trim();
336
366
 
337
367
  if (selectedProvider === 'command' && !selectedCommand) {
338
368
  return {
@@ -340,6 +370,12 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
340
370
  errors: ['fill_provider=command requires fill_command']
341
371
  } satisfies ToolResponse<Record<string, unknown>>;
342
372
  }
373
+ if (selectedProvider === 'openclaw' && !selectedAgentId) {
374
+ return {
375
+ ok: false,
376
+ errors: ['fill_provider=openclaw requires openclaw_fill_agent_id']
377
+ } satisfies ToolResponse<Record<string, unknown>>;
378
+ }
343
379
 
344
380
  const nextPluginConfig: Record<string, unknown> = {
345
381
  ...currentPluginConfig,
@@ -366,6 +402,13 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
366
402
 
367
403
  if (selectedCommand) {
368
404
  nextPluginConfig.fill_command = selectedCommand;
405
+ } else {
406
+ delete nextPluginConfig.fill_command;
407
+ }
408
+ if (selectedProvider === 'openclaw') {
409
+ nextPluginConfig.openclaw_fill_agent_id = selectedAgentId;
410
+ } else {
411
+ delete nextPluginConfig.openclaw_fill_agent_id;
369
412
  }
370
413
 
371
414
  const nextRootConfig = {
@@ -394,6 +437,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
394
437
  plugin_id: 'plashboard',
395
438
  fill_provider: selectedProvider,
396
439
  fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
440
+ openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
397
441
  data_dir: nextPluginConfig.data_dir,
398
442
  scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
399
443
  session_timeout_seconds: nextPluginConfig.session_timeout_seconds,
@@ -408,10 +452,36 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
408
452
 
409
453
  export function registerPlashboardPlugin(api: UnknownApi): void {
410
454
  const config = resolveConfig(api);
455
+ const runtimeCommand = api.runtime?.system?.runCommandWithTimeout;
456
+ const fillCommandRunner = runtimeCommand
457
+ ? async (
458
+ argv: string[],
459
+ optionsOrTimeout: number | {
460
+ timeoutMs: number;
461
+ cwd?: string;
462
+ input?: string;
463
+ env?: NodeJS.ProcessEnv;
464
+ windowsVerbatimArguments?: boolean;
465
+ noOutputTimeoutMs?: number;
466
+ }
467
+ ) => {
468
+ const result = await runtimeCommand(argv, optionsOrTimeout);
469
+ return {
470
+ stdout: result.stdout,
471
+ stderr: result.stderr,
472
+ code: result.code,
473
+ signal: result.signal,
474
+ killed: result.killed,
475
+ termination: result.termination
476
+ };
477
+ }
478
+ : undefined;
411
479
  const runtime = new PlashboardRuntime(config, {
412
480
  info: (...args) => api.logger?.info?.(...args),
413
481
  warn: (...args) => api.logger?.warn?.(...args),
414
482
  error: (...args) => api.logger?.error?.(...args)
483
+ }, {
484
+ commandRunner: fillCommandRunner
415
485
  });
416
486
 
417
487
  api.registerService?.({
@@ -465,8 +535,9 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
465
535
  parameters: {
466
536
  type: 'object',
467
537
  properties: {
468
- fill_provider: { type: 'string', enum: ['mock', 'command'] },
538
+ fill_provider: { type: 'string', enum: ['mock', 'command', 'openclaw'] },
469
539
  fill_command: { type: 'string' },
540
+ openclaw_fill_agent_id: { type: 'string' },
470
541
  data_dir: { type: 'string' },
471
542
  scheduler_tick_seconds: { type: 'number' },
472
543
  session_timeout_seconds: { type: 'number' },
@@ -685,9 +756,16 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
685
756
  }
686
757
  if (cmd === 'setup') {
687
758
  const mode = asString(rest[0]).toLowerCase();
688
- const fillProvider = mode === 'command' || mode === 'mock' ? mode : undefined;
759
+ const fillProvider = mode === 'command' || mode === 'mock' || mode === 'openclaw' ? mode : undefined;
689
760
  const fillCommand = fillProvider === 'command' ? rest.slice(1).join(' ').trim() || undefined : undefined;
690
- return toCommandResult(await runSetup(api, config, { fill_provider: fillProvider, fill_command: fillCommand }));
761
+ const fillAgentId = fillProvider === 'openclaw' ? (rest[1] || '').trim() || undefined : undefined;
762
+ return toCommandResult(
763
+ await runSetup(api, config, {
764
+ fill_provider: fillProvider,
765
+ fill_command: fillCommand,
766
+ openclaw_fill_agent_id: fillAgentId
767
+ })
768
+ );
691
769
  }
692
770
  if (cmd === 'init') return toCommandResult(await runtime.init());
693
771
  if (cmd === 'status') return toCommandResult(await runtime.status());
@@ -713,7 +791,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
713
791
  return toCommandResult({
714
792
  ok: false,
715
793
  errors: [
716
- 'unknown command. supported: setup [mock|command <fill_command>], expose-guide [local_url] [https_port], expose-check [local_url] [https_port], init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
794
+ 'unknown command. supported: setup [openclaw [agent_id]|mock|command <fill_command>], expose-guide [local_url] [https_port], expose-check [local_url] [https_port], init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
717
795
  ]
718
796
  });
719
797
  }
package/src/runtime.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  DashboardTemplate,
6
6
  DisplayProfile,
7
7
  FillResponse,
8
+ FillRunner,
8
9
  PlashboardConfig,
9
10
  PlashboardState,
10
11
  RunArtifact,
@@ -16,7 +17,7 @@ import { asErrorMessage, atomicWriteJson, deepClone, ensureDir, nowIso, sleep }
16
17
  import { collectCurrentValues, mergeTemplateValues, validateFieldPointers } from './merge.js';
17
18
  import { validateFillShape, validateTemplateShape } from './schema-validation.js';
18
19
  import { DashboardValidatorPublisher } from './publisher.js';
19
- import { createFillRunner } from './fill-runner.js';
20
+ import { createFillRunner, type FillRunnerDeps } from './fill-runner.js';
20
21
 
21
22
  interface Logger {
22
23
  info(message: string, ...args: unknown[]): void;
@@ -55,7 +56,7 @@ export class PlashboardRuntime {
55
56
  private readonly templateStore: TemplateStore;
56
57
  private readonly runStore: RunStore;
57
58
  private readonly publisher: DashboardValidatorPublisher;
58
- private readonly fillRunner;
59
+ private readonly fillRunner: FillRunner;
59
60
 
60
61
  private schedulerTimer: NodeJS.Timeout | null = null;
61
62
  private tickInProgress = false;
@@ -64,14 +65,15 @@ export class PlashboardRuntime {
64
65
 
65
66
  constructor(
66
67
  private readonly config: PlashboardConfig,
67
- private readonly logger: Logger = NOOP_LOGGER
68
+ private readonly logger: Logger = NOOP_LOGGER,
69
+ fillRunnerDeps: FillRunnerDeps = {}
68
70
  ) {
69
71
  this.paths = new Paths(config);
70
72
  this.stateStore = new StateStore(this.paths);
71
73
  this.templateStore = new TemplateStore(this.paths);
72
74
  this.runStore = new RunStore(this.paths);
73
75
  this.publisher = new DashboardValidatorPublisher(config);
74
- this.fillRunner = createFillRunner(config);
76
+ this.fillRunner = createFillRunner(config, fillRunnerDeps);
75
77
  }
76
78
 
77
79
  async start(): Promise<void> {
package/src/types.ts CHANGED
@@ -23,8 +23,9 @@ export interface PlashboardConfig {
23
23
  default_retry_count: number;
24
24
  retry_backoff_seconds: number;
25
25
  session_timeout_seconds: number;
26
- fill_provider: 'command' | 'mock';
26
+ fill_provider: 'command' | 'mock' | 'openclaw';
27
27
  fill_command?: string;
28
+ openclaw_fill_agent_id?: string;
28
29
  python_bin: string;
29
30
  writer_script_path: string;
30
31
  dashboard_output_path: string;