@assistkick/create 1.19.0 → 1.22.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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/mcp_config.ts +87 -0
  3. package/templates/assistkick-product-system/packages/backend/src/server.ts +8 -1
  4. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +7 -0
  5. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +26 -1
  6. package/templates/assistkick-product-system/packages/backend/src/services/mcp_config_service.ts +157 -0
  7. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +37 -0
  8. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +14 -1
  9. package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx +307 -0
  10. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_superb_roxanne_simpson.sql +8 -0
  11. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_noisy_maelstrom.sql +1 -0
  12. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +1019 -23
  13. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +997 -22
  14. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
  15. package/templates/assistkick-product-system/packages/shared/db/schema.ts +11 -0
  16. package/templates/assistkick-product-system/packages/shared/lib/app_use_flow.ts +484 -0
  17. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  18. package/templates/assistkick-product-system/packages/shared/tools/agent_builder.ts +341 -0
  19. package/templates/assistkick-product-system/packages/shared/tools/app_use_record.ts +268 -0
  20. package/templates/assistkick-product-system/packages/shared/tools/app_use_run.ts +348 -0
  21. package/templates/assistkick-product-system/packages/shared/tools/app_use_validate.ts +67 -0
  22. package/templates/assistkick-product-system/packages/shared/tools/workflow_builder.ts +754 -0
  23. package/templates/skills/assistkick-agent-builder/SKILL.md +168 -0
  24. package/templates/skills/assistkick-app-use/SKILL.md +296 -0
  25. package/templates/skills/assistkick-app-use/references/agent-browser.md +1156 -0
  26. package/templates/skills/assistkick-workflow-builder/SKILL.md +234 -0
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * app_use_run — Execute a YAML flow file against a target platform.
5
+ *
6
+ * Parses the flow, translates each step to agent-browser commands (for web),
7
+ * executes them sequentially, reports pass/fail per step with timing.
8
+ *
9
+ * Usage:
10
+ * pnpm tsx packages/shared/tools/app_use_run.ts <flow.yaml> [--env KEY=VALUE] [--headed] [--platform web]
11
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type click --selector "@e1"
12
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type fill --selector "@e2" --value "hello"
13
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type snapshot --params '{"interactive":true}'
14
+ * pnpm tsx packages/shared/tools/app_use_run.ts assert-ai <screenshot-path> --prompt "The login form is visible"
15
+ */
16
+
17
+ import {program} from 'commander';
18
+ import chalk from 'chalk';
19
+ import {execSync, spawn} from 'node:child_process';
20
+ import {
21
+ type FlowResult,
22
+ type FlowStep,
23
+ interpolate,
24
+ readFlowFile,
25
+ type StepResult,
26
+ stepToAgentBrowserArgs,
27
+ validateFlow,
28
+ } from '../lib/app_use_flow.js';
29
+
30
+ // ── Helpers ───────────────────────────────────────────────────────────────────
31
+
32
+ function runAgentBrowser(args: string[], timeoutMs = 30_000): { stdout: string; exitCode: number } {
33
+ const cmd = `agent-browser ${args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
34
+ try {
35
+ const stdout = execSync(cmd, {
36
+ timeout: timeoutMs,
37
+ encoding: 'utf-8',
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ });
40
+ return { stdout: stdout.trim(), exitCode: 0 };
41
+ } catch (err: any) {
42
+ return {
43
+ stdout: err.stdout?.toString()?.trim() || '',
44
+ exitCode: err.status ?? 1,
45
+ };
46
+ }
47
+ }
48
+
49
+ function assertVisibleInSnapshot(snapshotOutput: string, text: string): boolean {
50
+ // Check if the text appears in the accessibility snapshot
51
+ return snapshotOutput.toLowerCase().includes(text.toLowerCase());
52
+ }
53
+
54
+ async function assertWithAI(screenshotPath: string, prompt: string): Promise<{ passes: boolean; details: string }> {
55
+ const systemPrompt = [
56
+ 'You are a UI test assertion engine. You analyze screenshots to verify assertions.',
57
+ 'Output ONLY valid JSON with these fields:',
58
+ '{"passes": boolean, "confidence": "high"|"medium"|"low", "details": "brief explanation"}',
59
+ 'No other text, no code fences.',
60
+ ].join(' ');
61
+
62
+ const fullPrompt = `Look at the screenshot and determine: ${prompt}`;
63
+
64
+ return new Promise((resolve) => {
65
+ const args = [
66
+ '-p', fullPrompt,
67
+ '--system-prompt', systemPrompt,
68
+ '--model', 'claude-haiku-4-5',
69
+ '--output-format', 'text',
70
+ '--max-turns', '1',
71
+ '--dangerously-skip-permissions',
72
+ screenshotPath,
73
+ ];
74
+
75
+ const child = spawn('claude', args, {
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ env: { ...process.env },
78
+ timeout: 30_000,
79
+ });
80
+
81
+ let stdout = '';
82
+ child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
83
+
84
+ child.on('close', (code) => {
85
+ if (code === 0 && stdout.trim()) {
86
+ try {
87
+ // Try parsing JSON directly
88
+ let jsonStr = stdout.trim();
89
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
90
+ if (fenceMatch) jsonStr = fenceMatch[1].trim();
91
+ const result = JSON.parse(jsonStr);
92
+ resolve({ passes: !!result.passes, details: result.details || '' });
93
+ } catch {
94
+ // If JSON parsing fails, check for positive keywords
95
+ const lower = stdout.toLowerCase();
96
+ const passes = lower.includes('"passes": true') || lower.includes('"passes":true');
97
+ resolve({ passes, details: stdout.trim() });
98
+ }
99
+ } else {
100
+ resolve({ passes: false, details: `AI assertion failed (exit code: ${code})` });
101
+ }
102
+ });
103
+
104
+ child.on('error', () => {
105
+ resolve({ passes: false, details: 'Failed to spawn Claude CLI for AI assertion' });
106
+ });
107
+ });
108
+ }
109
+
110
+ // ── Single step execution (for agent interactive use) ─────────────────────────
111
+
112
+ async function execSingleStep(opts: any): Promise<void> {
113
+ const step: FlowStep = {
114
+ type: opts.type,
115
+ selector: opts.selector,
116
+ value: opts.value,
117
+ params: opts.params ? JSON.parse(opts.params) : undefined,
118
+ };
119
+
120
+ const args = stepToAgentBrowserArgs(step);
121
+ const { stdout, exitCode } = runAgentBrowser(args);
122
+
123
+ if (exitCode === 0) {
124
+ console.log(stdout);
125
+ console.log('\n' + JSON.stringify({ status: 'ok', step: step.type, output: stdout }));
126
+ } else {
127
+ console.error(chalk.red(`Step failed: ${step.type}`));
128
+ if (stdout) console.error(stdout);
129
+ console.log('\n' + JSON.stringify({ status: 'error', step: step.type, output: stdout }));
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ // ── Flow execution ────────────────────────────────────────────────────────────
135
+
136
+ async function runFlow(flowPath: string, opts: any): Promise<void> {
137
+ const flow = readFlowFile(flowPath);
138
+
139
+ // Validate first
140
+ const issues = validateFlow(flow);
141
+ if (issues.length > 0) {
142
+ console.error(chalk.red('Flow validation failed:'));
143
+ for (const issue of issues) console.error(chalk.red(` - ${issue}`));
144
+ process.exit(1);
145
+ }
146
+
147
+ // Merge env from CLI and flow config
148
+ const env: Record<string, string> = { ...flow.config.env };
149
+ if (opts.env) {
150
+ for (const e of Array.isArray(opts.env) ? opts.env : [opts.env]) {
151
+ const [k, ...rest] = e.split('=');
152
+ if (k) env[k] = rest.join('=');
153
+ }
154
+ }
155
+
156
+ const platform = opts.platform || flow.config.platform || 'web';
157
+ if (platform !== 'web') {
158
+ console.error(chalk.red(`Platform "${platform}" is not yet supported. Only "web" is available.`));
159
+ process.exit(1);
160
+ }
161
+
162
+ const flowName = flow.config.name || flowPath;
163
+ console.log(chalk.cyan.bold(`\nRunning flow: ${flowName}`));
164
+ console.log(`Platform: ${platform} | Steps: ${flow.steps.length}`);
165
+ if (opts.headed) console.log('Mode: headed');
166
+ console.log();
167
+
168
+ // Auto-session: open browser if configured
169
+ if (flow.config.autoSession && flow.config.appId) {
170
+ const headedArgs = opts.headed ? ['--headed'] : [];
171
+ const openArgs = ['open', interpolate(flow.config.appId, env), ...headedArgs];
172
+ console.log(chalk.gray(` [setup] agent-browser ${openArgs.join(' ')}`));
173
+ runAgentBrowser(openArgs);
174
+ }
175
+
176
+ const results: StepResult[] = [];
177
+ const startTime = Date.now();
178
+ let aborted = false;
179
+
180
+ for (let i = 0; i < flow.steps.length; i++) {
181
+ if (aborted) {
182
+ results.push({
183
+ step: flow.steps[i], index: i, status: 'skipped', durationMs: 0,
184
+ });
185
+ continue;
186
+ }
187
+
188
+ const step = flow.steps[i];
189
+ const stepStart = Date.now();
190
+ const label = step.label || `${step.type}${step.value ? ` ${step.value}` : ''}${step.selector ? ` ${step.selector}` : ''}`;
191
+
192
+ try {
193
+ // Handle assertion steps specially
194
+ if (step.type === 'assertVisible' || step.type === 'assertNotVisible') {
195
+ const { stdout } = runAgentBrowser(['snapshot', '-i']);
196
+ const text = step.value || '';
197
+ const found = assertVisibleInSnapshot(stdout, text);
198
+ const shouldBeVisible = step.type === 'assertVisible';
199
+
200
+ if (found === shouldBeVisible) {
201
+ const ms = Date.now() - stepStart;
202
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
203
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
204
+ } else {
205
+ throw new Error(`Expected "${text}" to be ${shouldBeVisible ? 'visible' : 'not visible'}`);
206
+ }
207
+ continue;
208
+ }
209
+
210
+ if (step.type === 'assertUrl') {
211
+ const { stdout } = runAgentBrowser(['get', 'url']);
212
+ const expected = step.value ? interpolate(step.value, env) : '';
213
+ if (!stdout.includes(expected)) {
214
+ throw new Error(`URL assertion failed: expected "${expected}" in "${stdout}"`);
215
+ }
216
+ const ms = Date.now() - stepStart;
217
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
218
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
219
+ continue;
220
+ }
221
+
222
+ if (step.type === 'assertText') {
223
+ const sel = step.selector ? interpolate(step.selector, env) : '';
224
+ const { stdout } = runAgentBrowser(['get', 'text', sel]);
225
+ const expected = step.value ? interpolate(step.value, env) : '';
226
+ if (!stdout.includes(expected)) {
227
+ throw new Error(`Text assertion failed: expected "${expected}" in "${stdout}"`);
228
+ }
229
+ const ms = Date.now() - stepStart;
230
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
231
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
232
+ continue;
233
+ }
234
+
235
+ if (step.type === 'assertWithAI') {
236
+ const screenshotResult = runAgentBrowser(['screenshot']);
237
+ // Extract screenshot path from agent-browser output
238
+ const pathMatch = screenshotResult.stdout.match(/(?:saved to |Screenshot: ?)(.+\.png)/i);
239
+ const screenshotPath = pathMatch?.[1]?.trim();
240
+ if (!screenshotPath) {
241
+ throw new Error('Could not capture screenshot for AI assertion');
242
+ }
243
+ const aiResult = await assertWithAI(screenshotPath, step.value || '');
244
+ if (aiResult.passes) {
245
+ const ms = Date.now() - stepStart;
246
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms) — ${aiResult.details}`));
247
+ results.push({ step, index: i, status: 'passed', durationMs: ms, output: aiResult.details });
248
+ } else {
249
+ throw new Error(`AI assertion failed: ${aiResult.details}`);
250
+ }
251
+ continue;
252
+ }
253
+
254
+ // Normal step: translate to agent-browser command and run
255
+ const args = stepToAgentBrowserArgs(step, env);
256
+ const { stdout, exitCode } = runAgentBrowser(args);
257
+
258
+ if (exitCode !== 0) {
259
+ throw new Error(stdout || `agent-browser exited with code ${exitCode}`);
260
+ }
261
+
262
+ const ms = Date.now() - stepStart;
263
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
264
+ results.push({ step, index: i, status: 'passed', durationMs: ms, output: stdout });
265
+
266
+ } catch (err) {
267
+ const ms = Date.now() - stepStart;
268
+ const errMsg = (err as Error).message;
269
+
270
+ if (step.optional) {
271
+ console.log(chalk.yellow(` [${i + 1}/${flow.steps.length}] SKIP ${label} (optional, ${ms}ms) — ${errMsg}`));
272
+ results.push({ step, index: i, status: 'skipped', durationMs: ms, error: errMsg });
273
+ } else {
274
+ console.log(chalk.red(` [${i + 1}/${flow.steps.length}] FAIL ${label} (${ms}ms) — ${errMsg}`));
275
+ results.push({ step, index: i, status: 'failed', durationMs: ms, error: errMsg });
276
+ aborted = true;
277
+ }
278
+ }
279
+ }
280
+
281
+ // Auto-session: close browser
282
+ if (flow.config.autoSession) {
283
+ runAgentBrowser(['close']);
284
+ }
285
+
286
+ // Summary
287
+ const totalMs = Date.now() - startTime;
288
+ const passed = results.filter(r => r.status === 'passed').length;
289
+ const failed = results.filter(r => r.status === 'failed').length;
290
+ const skipped = results.filter(r => r.status === 'skipped').length;
291
+ const status = failed === 0 ? 'passed' : 'failed';
292
+
293
+ console.log();
294
+ const statusColor = status === 'passed' ? chalk.green : chalk.red;
295
+ console.log(statusColor.bold(`${status.toUpperCase()}: ${passed} passed, ${failed} failed, ${skipped} skipped (${totalMs}ms)`));
296
+
297
+ const result: FlowResult = {
298
+ flowName, platform, status, totalSteps: flow.steps.length,
299
+ passed, failed, skipped, durationMs: totalMs, steps: results,
300
+ };
301
+ console.log('\n' + JSON.stringify(result));
302
+
303
+ if (failed > 0) process.exit(1);
304
+ }
305
+
306
+ // ── CLI ───────────────────────────────────────────────────────────────────────
307
+
308
+ program
309
+ .argument('<action>', 'Flow file path, or "exec" for single step, or "assert-ai" for AI assertion')
310
+ .argument('[target]', 'Screenshot path (for assert-ai)')
311
+ .option('--env <pairs...>', 'Environment variables (KEY=VALUE)')
312
+ .option('--headed', 'Run browser in headed mode')
313
+ .option('--platform <platform>', 'Override platform (web, ios, android)')
314
+ .option('--type <type>', 'Step type (for exec action)')
315
+ .option('--selector <sel>', 'Element selector (for exec action)')
316
+ .option('--value <val>', 'Step value (for exec action)')
317
+ .option('--params <json>', 'Additional params as JSON (for exec action)')
318
+ .option('--prompt <text>', 'Assertion prompt (for assert-ai action)')
319
+ .parse();
320
+
321
+ const [action, target] = program.args;
322
+ const opts = program.opts();
323
+
324
+ (async () => {
325
+ try {
326
+ if (action === 'exec') {
327
+ if (!opts.type) throw new Error('--type is required for exec action');
328
+ await execSingleStep(opts);
329
+ } else if (action === 'assert-ai') {
330
+ if (!target) throw new Error('Screenshot path is required for assert-ai');
331
+ if (!opts.prompt) throw new Error('--prompt is required for assert-ai');
332
+ const result = await assertWithAI(target, opts.prompt);
333
+ if (result.passes) {
334
+ console.log(chalk.green(`PASS: ${result.details}`));
335
+ } else {
336
+ console.log(chalk.red(`FAIL: ${result.details}`));
337
+ }
338
+ console.log(JSON.stringify(result));
339
+ if (!result.passes) process.exit(1);
340
+ } else {
341
+ // Treat action as a flow file path
342
+ await runFlow(action, opts);
343
+ }
344
+ } catch (err) {
345
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
346
+ process.exit(1);
347
+ }
348
+ })();
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * app_use_validate — Validate a YAML flow file without executing it.
5
+ *
6
+ * Usage:
7
+ * pnpm tsx packages/shared/tools/app_use_validate.ts <flow.yaml>
8
+ * pnpm tsx packages/shared/tools/app_use_validate.ts <flow.yaml> --show-steps
9
+ */
10
+
11
+ import { program } from 'commander';
12
+ import chalk from 'chalk';
13
+ import { readFlowFile, validateFlow } from '../lib/app_use_flow.js';
14
+
15
+ program
16
+ .argument('<flow>', 'Path to flow YAML file')
17
+ .option('--show-steps', 'Show parsed steps for debugging')
18
+ .parse();
19
+
20
+ const [flowPath] = program.args;
21
+ const opts = program.opts();
22
+
23
+ (async () => {
24
+ try {
25
+ const flow = readFlowFile(flowPath);
26
+ const issues = validateFlow(flow);
27
+
28
+ console.log(chalk.cyan.bold(`\nFlow: ${flow.config.name || flowPath}\n`));
29
+ console.log(` Platform: ${flow.config.platform || 'web'}`);
30
+ if (flow.config.appId) console.log(` App: ${flow.config.appId}`);
31
+ console.log(` Steps: ${flow.steps.length}`);
32
+ console.log(` Auto-session: ${flow.config.autoSession !== false}`);
33
+ if (flow.config.env) {
34
+ console.log(` Env vars: ${Object.keys(flow.config.env).join(', ')}`);
35
+ }
36
+ console.log();
37
+
38
+ if (opts.showSteps) {
39
+ console.log(chalk.cyan(' Steps:'));
40
+ for (let i = 0; i < flow.steps.length; i++) {
41
+ const s = flow.steps[i];
42
+ const parts: string[] = [s.type];
43
+ if (s.selector) parts.push(s.selector);
44
+ if (s.value) parts.push(`"${s.value}"`);
45
+ if (s.optional) parts.push('(optional)');
46
+ if (s.label) parts.push(`— ${s.label}`);
47
+ console.log(` ${i + 1}. ${parts.join(' ')}`);
48
+ }
49
+ console.log();
50
+ }
51
+
52
+ if (issues.length === 0) {
53
+ console.log(chalk.green.bold('Valid flow — no issues found'));
54
+ console.log(JSON.stringify({ valid: true, steps: flow.steps.length, issues: [] }));
55
+ } else {
56
+ console.log(chalk.yellow.bold(`Found ${issues.length} issue(s):`));
57
+ for (const issue of issues) {
58
+ console.log(chalk.yellow(` - ${issue}`));
59
+ }
60
+ console.log('\n' + JSON.stringify({ valid: false, steps: flow.steps.length, issues }));
61
+ process.exit(1);
62
+ }
63
+ } catch (err) {
64
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
65
+ process.exit(1);
66
+ }
67
+ })();