@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
@@ -8,6 +8,20 @@
8
8
  "when": 1773826299804,
9
9
  "tag": "0000_outgoing_ultron",
10
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1773914724031,
16
+ "tag": "0001_superb_roxanne_simpson",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1773918822690,
23
+ "tag": "0002_noisy_maelstrom",
24
+ "breakpoints": true
11
25
  }
12
26
  ]
13
27
  }
@@ -247,6 +247,17 @@ export const workflowToolCalls = sqliteTable('workflow_tool_calls', {
247
247
  createdAt: text('created_at').notNull(),
248
248
  });
249
249
 
250
+ // --- user_mcp_configs table ---
251
+ export const userMcpConfigs = sqliteTable('user_mcp_configs', {
252
+ id: text('id').primaryKey(),
253
+ userId: text('user_id').notNull(),
254
+ projectId: text('project_id'), // null = global (user-level), non-null = project-scoped
255
+ configType: text('config_type').notNull(), // 'localhost' or 'remote'
256
+ mcpServersJson: text('mcp_servers_json').notNull().default('{}'),
257
+ createdAt: text('created_at').notNull(),
258
+ updatedAt: text('updated_at').notNull(),
259
+ });
260
+
250
261
  // --- chat_permission_rules table ---
251
262
  export const chatPermissionRules = sqliteTable('chat_permission_rules', {
252
263
  id: text('id').primaryKey(),
@@ -0,0 +1,484 @@
1
+ /**
2
+ * app_use_flow — Shared types and utilities for the app-use flow YAML format.
3
+ *
4
+ * A flow file is a YAML document with an optional header (config) separated
5
+ * from steps by `---`. Steps are a list of actions that map to agent-browser
6
+ * commands (for web) or platform-specific commands (for iOS/Android).
7
+ */
8
+
9
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+
13
+ // ── Types ─────────────────────────────────────────────────────────────────────
14
+
15
+ export type Platform = 'web' | 'ios' | 'android';
16
+
17
+ export interface FlowConfig {
18
+ /** App URL (web) or bundle ID (mobile) */
19
+ appId?: string;
20
+ /** Target platform */
21
+ platform?: Platform;
22
+ /** Display name for the flow */
23
+ name?: string;
24
+ /** Environment variables for interpolation */
25
+ env?: Record<string, string>;
26
+ /** Whether to manage agent-browser session automatically */
27
+ autoSession?: boolean;
28
+ /** Tags for filtering */
29
+ tags?: string[];
30
+ }
31
+
32
+ export type StepType =
33
+ | 'open' | 'click' | 'dblclick' | 'fill' | 'type' | 'press'
34
+ | 'hover' | 'select' | 'check' | 'uncheck'
35
+ | 'scroll' | 'scrollIntoView'
36
+ | 'wait' | 'snapshot' | 'screenshot'
37
+ | 'assertVisible' | 'assertNotVisible' | 'assertUrl' | 'assertText' | 'assertWithAI'
38
+ | 'back' | 'forward' | 'reload'
39
+ | 'eval' | 'setViewport' | 'setDevice'
40
+ | 'upload' | 'drag'
41
+ | 'tab' | 'frame'
42
+ | 'sessionStart' | 'sessionEnd';
43
+
44
+ export interface FlowStep {
45
+ /** Action type */
46
+ type: StepType;
47
+ /** Main argument (shorthand for the primary parameter) */
48
+ value?: string;
49
+ /** Element selector (ref @e1, CSS #id, text=..., etc.) */
50
+ selector?: string;
51
+ /** Additional parameters (type-specific) */
52
+ params?: Record<string, unknown>;
53
+ /** Human-readable label for this step */
54
+ label?: string;
55
+ /** If true, step failure doesn't abort the flow */
56
+ optional?: boolean;
57
+ }
58
+
59
+ export interface FlowFile {
60
+ config: FlowConfig;
61
+ steps: FlowStep[];
62
+ }
63
+
64
+ export interface StepResult {
65
+ step: FlowStep;
66
+ index: number;
67
+ status: 'passed' | 'failed' | 'skipped';
68
+ durationMs: number;
69
+ output?: string;
70
+ error?: string;
71
+ }
72
+
73
+ export interface FlowResult {
74
+ flowName: string;
75
+ platform: Platform;
76
+ status: 'passed' | 'failed';
77
+ totalSteps: number;
78
+ passed: number;
79
+ failed: number;
80
+ skipped: number;
81
+ durationMs: number;
82
+ steps: StepResult[];
83
+ }
84
+
85
+ // ── Parsing ───────────────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Parse a raw YAML step into a normalized FlowStep.
89
+ * Supports both shorthand (`- click: "@e1"`) and full object form.
90
+ */
91
+ function parseStep(raw: unknown): FlowStep | null {
92
+ if (typeof raw === 'string') {
93
+ // Simple command with no args: `- back`, `- reload`
94
+ return { type: raw as StepType };
95
+ }
96
+
97
+ if (typeof raw !== 'object' || raw === null) return null;
98
+ const obj = raw as Record<string, unknown>;
99
+
100
+ // Find the action key (first key that is a valid step type)
101
+ const keys = Object.keys(obj);
102
+ const typeKey = keys[0];
103
+ if (!typeKey) return null;
104
+
105
+ const step: FlowStep = { type: typeKey as StepType };
106
+
107
+ const val = obj[typeKey];
108
+
109
+ if (typeof val === 'string' || typeof val === 'number') {
110
+ // Shorthand: `- click: "@e1"` or `- wait: 2000`
111
+ step.value = String(val);
112
+ } else if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
113
+ // Full object form
114
+ const params = val as Record<string, unknown>;
115
+ if (params.selector !== undefined) step.selector = String(params.selector);
116
+ if (params.value !== undefined) step.value = String(params.value);
117
+ if (params.label !== undefined) step.label = String(params.label);
118
+ if (params.optional !== undefined) step.optional = Boolean(params.optional);
119
+
120
+ // Remaining keys go into params
121
+ const rest: Record<string, unknown> = {};
122
+ for (const [k, v] of Object.entries(params)) {
123
+ if (!['selector', 'value', 'label', 'optional'].includes(k)) {
124
+ rest[k] = v;
125
+ }
126
+ }
127
+ if (Object.keys(rest).length > 0) step.params = rest;
128
+ }
129
+
130
+ // Also pick up top-level label/optional
131
+ if (obj.label !== undefined) step.label = String(obj.label);
132
+ if (obj.optional !== undefined) step.optional = Boolean(obj.optional);
133
+
134
+ return step;
135
+ }
136
+
137
+ /**
138
+ * Parse a flow YAML string into a FlowFile.
139
+ * Handles the `---` separator between config header and steps.
140
+ */
141
+ export function parseFlowYaml(content: string): FlowFile {
142
+ // Split on document separator
143
+ const parts = content.split(/^---\s*$/m);
144
+
145
+ let config: FlowConfig = {};
146
+ let stepsRaw: unknown[] = [];
147
+
148
+ if (parts.length >= 2) {
149
+ // Header + steps
150
+ const headerPart = parts[0].trim();
151
+ const stepsPart = parts.slice(1).join('---').trim();
152
+
153
+ if (headerPart) {
154
+ config = (parseYaml(headerPart) as FlowConfig) || {};
155
+ }
156
+ if (stepsPart) {
157
+ stepsRaw = (parseYaml(stepsPart) as unknown[]) || [];
158
+ }
159
+ } else {
160
+ // No separator — treat as steps only, or check if it's an array
161
+ const parsed = parseYaml(content);
162
+ if (Array.isArray(parsed)) {
163
+ stepsRaw = parsed;
164
+ } else if (typeof parsed === 'object' && parsed !== null) {
165
+ // Could be a config-only document
166
+ config = parsed as FlowConfig;
167
+ }
168
+ }
169
+
170
+ const steps = (stepsRaw || [])
171
+ .map(parseStep)
172
+ .filter((s): s is FlowStep => s !== null);
173
+
174
+ // Defaults
175
+ if (!config.platform) config.platform = 'web';
176
+ if (config.autoSession === undefined) config.autoSession = true;
177
+
178
+ return { config, steps };
179
+ }
180
+
181
+ /**
182
+ * Read and parse a flow YAML file.
183
+ */
184
+ export function readFlowFile(path: string): FlowFile {
185
+ if (!existsSync(path)) {
186
+ throw new Error(`Flow file not found: ${path}`);
187
+ }
188
+ const content = readFileSync(path, 'utf-8');
189
+ return parseFlowYaml(content);
190
+ }
191
+
192
+ // ── Serialization ─────────────────────────────────────────────────────────────
193
+
194
+ /**
195
+ * Serialize a FlowStep back to a YAML-friendly plain object.
196
+ */
197
+ function stepToYamlObj(step: FlowStep): Record<string, unknown> {
198
+ const hasExtra = step.selector || step.params || step.label || step.optional;
199
+
200
+ if (!hasExtra && step.value) {
201
+ // Shorthand form
202
+ return { [step.type]: step.value };
203
+ }
204
+
205
+ if (!hasExtra && !step.value) {
206
+ // Bare command
207
+ return { [step.type]: null };
208
+ }
209
+
210
+ // Full object form
211
+ const inner: Record<string, unknown> = {};
212
+ if (step.selector) inner.selector = step.selector;
213
+ if (step.value) inner.value = step.value;
214
+ if (step.label) inner.label = step.label;
215
+ if (step.optional) inner.optional = step.optional;
216
+ if (step.params) Object.assign(inner, step.params);
217
+
218
+ return { [step.type]: inner };
219
+ }
220
+
221
+ /**
222
+ * Serialize a FlowFile to YAML string.
223
+ */
224
+ export function serializeFlowYaml(flow: FlowFile, comments?: string[]): string {
225
+ const parts: string[] = [];
226
+
227
+ // Config header
228
+ const configCopy = { ...flow.config };
229
+ if (configCopy.autoSession === true) delete configCopy.autoSession; // default
230
+ const hasConfig = Object.keys(configCopy).length > 0;
231
+
232
+ if (hasConfig) {
233
+ parts.push(stringifyYaml(configCopy).trim());
234
+ }
235
+
236
+ parts.push('---');
237
+
238
+ // Comments
239
+ if (comments && comments.length > 0) {
240
+ for (const c of comments) {
241
+ parts.push(`# ${c}`);
242
+ }
243
+ }
244
+
245
+ // Steps
246
+ const stepsObj = flow.steps.map(stepToYamlObj);
247
+ parts.push(stringifyYaml(stepsObj).trim());
248
+
249
+ return parts.join('\n') + '\n';
250
+ }
251
+
252
+ /**
253
+ * Write a flow file to disk.
254
+ */
255
+ export function writeFlowFile(path: string, flow: FlowFile, comments?: string[]): void {
256
+ const dir = dirname(path);
257
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
258
+ writeFileSync(path, serializeFlowYaml(flow, comments), 'utf-8');
259
+ }
260
+
261
+ // ── Interpolation ─────────────────────────────────────────────────────────────
262
+
263
+ /**
264
+ * Interpolate `${VAR}` references in a string using the env map.
265
+ */
266
+ export function interpolate(text: string, env: Record<string, string>): string {
267
+ return text.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? `\${${key}}`);
268
+ }
269
+
270
+ // ── Step → agent-browser command mapping ──────────────────────────────────────
271
+
272
+ /**
273
+ * Translate a FlowStep to an agent-browser CLI command (for web platform).
274
+ * Returns the command as a string array (args to pass to agent-browser).
275
+ */
276
+ export function stepToAgentBrowserArgs(step: FlowStep, env: Record<string, string> = {}): string[] {
277
+ const val = step.value ? interpolate(step.value, env) : '';
278
+ const sel = step.selector ? interpolate(step.selector, env) : '';
279
+
280
+ switch (step.type) {
281
+ case 'open':
282
+ return ['open', val];
283
+
284
+ case 'click':
285
+ return ['click', sel || val];
286
+
287
+ case 'dblclick':
288
+ return ['dblclick', sel || val];
289
+
290
+ case 'fill':
291
+ return ['fill', sel, val];
292
+
293
+ case 'type':
294
+ return ['type', sel, val];
295
+
296
+ case 'press':
297
+ return ['press', val];
298
+
299
+ case 'hover':
300
+ return ['hover', sel || val];
301
+
302
+ case 'select':
303
+ return ['select', sel, val];
304
+
305
+ case 'check':
306
+ return ['check', sel || val];
307
+
308
+ case 'uncheck':
309
+ return ['uncheck', sel || val];
310
+
311
+ case 'scroll': {
312
+ const dir = val || 'down';
313
+ const args = ['scroll', dir];
314
+ if (step.params?.pixels) args.push(String(step.params.pixels));
315
+ if (step.params?.selector) args.push('--selector', String(step.params.selector));
316
+ return args;
317
+ }
318
+
319
+ case 'scrollIntoView':
320
+ return ['scrollintoview', sel || val];
321
+
322
+ case 'wait': {
323
+ if (step.params?.text) return ['wait', '--text', String(step.params.text)];
324
+ if (step.params?.url) return ['wait', '--url', String(step.params.url)];
325
+ if (step.params?.load) return ['wait', '--load', String(step.params.load)];
326
+ if (step.params?.fn) return ['wait', '--fn', String(step.params.fn)];
327
+ // Numeric wait (ms) or selector wait
328
+ if (val && /^\d+$/.test(val)) return ['wait', val];
329
+ if (val) return ['wait', val];
330
+ return ['wait', '1000'];
331
+ }
332
+
333
+ case 'snapshot': {
334
+ const args = ['snapshot'];
335
+ if (step.params?.interactive !== false) args.push('-i');
336
+ if (step.params?.compact) args.push('-c');
337
+ if (step.params?.depth) args.push('-d', String(step.params.depth));
338
+ if (step.params?.selector) args.push('-s', String(step.params.selector));
339
+ return args;
340
+ }
341
+
342
+ case 'screenshot': {
343
+ const args = ['screenshot'];
344
+ if (val) args.push(val);
345
+ if (step.params?.full) args.push('--full');
346
+ if (step.params?.annotate) args.push('--annotate');
347
+ return args;
348
+ }
349
+
350
+ case 'assertVisible':
351
+ // Implemented via snapshot + check (handled at runner level)
352
+ return ['snapshot', '-i', '--json'];
353
+
354
+ case 'assertNotVisible':
355
+ return ['snapshot', '-i', '--json'];
356
+
357
+ case 'assertUrl':
358
+ return ['get', 'url'];
359
+
360
+ case 'assertText':
361
+ return ['get', 'text', sel];
362
+
363
+ case 'assertWithAI':
364
+ // Screenshot for AI analysis (handled at runner level)
365
+ return ['screenshot'];
366
+
367
+ case 'back':
368
+ return ['back'];
369
+
370
+ case 'forward':
371
+ return ['forward'];
372
+
373
+ case 'reload':
374
+ return ['reload'];
375
+
376
+ case 'eval':
377
+ return ['eval', val];
378
+
379
+ case 'setViewport': {
380
+ const w = step.params?.width || 1280;
381
+ const h = step.params?.height || 720;
382
+ return ['set', 'viewport', String(w), String(h)];
383
+ }
384
+
385
+ case 'setDevice':
386
+ return ['set', 'device', val];
387
+
388
+ case 'upload':
389
+ return ['upload', sel, val];
390
+
391
+ case 'drag': {
392
+ const target = step.params?.target ? String(step.params.target) : '';
393
+ return ['drag', sel || val, target];
394
+ }
395
+
396
+ case 'tab': {
397
+ if (step.params?.action === 'new') return ['tab', 'new', val].filter(Boolean);
398
+ if (step.params?.action === 'close') return ['tab', 'close', val].filter(Boolean);
399
+ return ['tab', val].filter(Boolean);
400
+ }
401
+
402
+ case 'frame':
403
+ return val === 'main' ? ['frame', 'main'] : ['frame', sel || val];
404
+
405
+ case 'sessionStart':
406
+ return ['open', val || 'about:blank'];
407
+
408
+ case 'sessionEnd':
409
+ return ['close'];
410
+
411
+ default:
412
+ throw new Error(`Unknown step type: ${step.type}`);
413
+ }
414
+ }
415
+
416
+ // ── Validation ────────────────────────────────────────────────────────────────
417
+
418
+ const VALID_STEP_TYPES: Set<string> = new Set([
419
+ 'open', 'click', 'dblclick', 'fill', 'type', 'press',
420
+ 'hover', 'select', 'check', 'uncheck',
421
+ 'scroll', 'scrollIntoView',
422
+ 'wait', 'snapshot', 'screenshot',
423
+ 'assertVisible', 'assertNotVisible', 'assertUrl', 'assertText', 'assertWithAI',
424
+ 'back', 'forward', 'reload',
425
+ 'eval', 'setViewport', 'setDevice',
426
+ 'upload', 'drag',
427
+ 'tab', 'frame',
428
+ 'sessionStart', 'sessionEnd',
429
+ ]);
430
+
431
+ const REQUIRES_SELECTOR: Set<string> = new Set([
432
+ 'fill', 'type', 'assertText',
433
+ ]);
434
+
435
+ const REQUIRES_VALUE: Set<string> = new Set([
436
+ 'open', 'press', 'eval',
437
+ ]);
438
+
439
+ /**
440
+ * Validate a parsed flow and return a list of issues.
441
+ */
442
+ export function validateFlow(flow: FlowFile): string[] {
443
+ const issues: string[] = [];
444
+
445
+ if (flow.config.platform && !['web', 'ios', 'android'].includes(flow.config.platform)) {
446
+ issues.push(`Invalid platform: "${flow.config.platform}". Must be web, ios, or android`);
447
+ }
448
+
449
+ if (flow.steps.length === 0) {
450
+ issues.push('Flow has no steps');
451
+ }
452
+
453
+ for (let i = 0; i < flow.steps.length; i++) {
454
+ const step = flow.steps[i];
455
+ const prefix = `Step ${i + 1}`;
456
+
457
+ if (!VALID_STEP_TYPES.has(step.type)) {
458
+ issues.push(`${prefix}: unknown type "${step.type}"`);
459
+ continue;
460
+ }
461
+
462
+ if (REQUIRES_VALUE.has(step.type) && !step.value) {
463
+ issues.push(`${prefix} (${step.type}): missing required value`);
464
+ }
465
+
466
+ if (REQUIRES_SELECTOR.has(step.type) && !step.selector) {
467
+ issues.push(`${prefix} (${step.type}): missing required selector`);
468
+ }
469
+
470
+ // fill and type need both selector and value
471
+ if ((step.type === 'fill' || step.type === 'type') && !step.value) {
472
+ issues.push(`${prefix} (${step.type}): missing required value`);
473
+ }
474
+
475
+ // click/hover need either selector or value
476
+ if (['click', 'dblclick', 'hover', 'check', 'uncheck', 'scrollIntoView'].includes(step.type)) {
477
+ if (!step.selector && !step.value) {
478
+ issues.push(`${prefix} (${step.type}): missing selector or value`);
479
+ }
480
+ }
481
+ }
482
+
483
+ return issues;
484
+ }
@@ -20,7 +20,8 @@
20
20
  "drizzle-orm": "^0.45.1",
21
21
  "glob": "11.1.0",
22
22
  "gray-matter": "^4.0.3",
23
- "isolated-vm": "^6.1.0"
23
+ "isolated-vm": "^6.1.0",
24
+ "yaml": "^2.8.2"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^25.3.3",