@hailer/mcp 1.1.17-beta.2 → 1.2.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 (83) hide show
  1. package/.claude/CLAUDE.md +94 -135
  2. package/.claude/skills/create-and-publish-app/SKILL.md +127 -0
  3. package/.claude/skills/hailer-app-builder/SKILL.md +47 -0
  4. package/.claude/skills/publish-hailer-app/SKILL.md +63 -4
  5. package/.claude/skills/sdk-function-fields/SKILL.md +431 -287
  6. package/dist/bot/bot-manager.d.ts.map +1 -1
  7. package/dist/bot/bot-manager.js +2 -0
  8. package/dist/bot/bot-manager.js.map +1 -1
  9. package/dist/bot/bot.d.ts +2 -1
  10. package/dist/bot/bot.d.ts.map +1 -1
  11. package/dist/bot/bot.js +109 -41
  12. package/dist/bot/bot.js.map +1 -1
  13. package/dist/bot/services/message-classifier.d.ts.map +1 -1
  14. package/dist/bot/services/message-classifier.js +6 -0
  15. package/dist/bot/services/message-classifier.js.map +1 -1
  16. package/dist/bot/services/signal-router.d.ts.map +1 -1
  17. package/dist/bot/services/signal-router.js +1 -0
  18. package/dist/bot/services/signal-router.js.map +1 -1
  19. package/dist/bot/services/system-prompt.d.ts +4 -0
  20. package/dist/bot/services/system-prompt.d.ts.map +1 -1
  21. package/dist/bot/services/system-prompt.js +41 -12
  22. package/dist/bot/services/system-prompt.js.map +1 -1
  23. package/dist/bot/services/types.d.ts +7 -31
  24. package/dist/bot/services/types.d.ts.map +1 -1
  25. package/dist/bot/services/workspace-refresh.js.map +1 -1
  26. package/dist/bot/workspace-overview.d.ts.map +1 -1
  27. package/dist/bot/workspace-overview.js +4 -1
  28. package/dist/bot/workspace-overview.js.map +1 -1
  29. package/dist/bot-config/context.js.map +1 -1
  30. package/dist/bot-config/loader.d.ts.map +1 -1
  31. package/dist/bot-config/loader.js +1 -0
  32. package/dist/bot-config/loader.js.map +1 -1
  33. package/dist/bot-config/types.d.ts +2 -0
  34. package/dist/bot-config/types.d.ts.map +1 -1
  35. package/dist/mcp/UserContextCache.d.ts.map +1 -1
  36. package/dist/mcp/UserContextCache.js +8 -16
  37. package/dist/mcp/UserContextCache.js.map +1 -1
  38. package/dist/mcp/tool-registry.d.ts +3 -2
  39. package/dist/mcp/tool-registry.d.ts.map +1 -1
  40. package/dist/mcp/tool-registry.js +14 -9
  41. package/dist/mcp/tool-registry.js.map +1 -1
  42. package/dist/mcp/tools/activity.d.ts.map +1 -1
  43. package/dist/mcp/tools/activity.js +39 -94
  44. package/dist/mcp/tools/activity.js.map +1 -1
  45. package/dist/mcp/tools/app-scaffold.d.ts.map +1 -1
  46. package/dist/mcp/tools/app-scaffold.js +300 -575
  47. package/dist/mcp/tools/app-scaffold.js.map +1 -1
  48. package/dist/mcp/tools/date.d.ts +5 -0
  49. package/dist/mcp/tools/date.d.ts.map +1 -0
  50. package/dist/mcp/tools/date.js +23 -0
  51. package/dist/mcp/tools/date.js.map +1 -0
  52. package/dist/mcp/tools/discussion.d.ts.map +1 -1
  53. package/dist/mcp/tools/discussion.js +17 -9
  54. package/dist/mcp/tools/discussion.js.map +1 -1
  55. package/dist/mcp/tools/index.d.ts.map +1 -1
  56. package/dist/mcp/tools/index.js +2 -0
  57. package/dist/mcp/tools/index.js.map +1 -1
  58. package/dist/mcp/tools/insight.d.ts.map +1 -1
  59. package/dist/mcp/tools/insight.js +13 -19
  60. package/dist/mcp/tools/insight.js.map +1 -1
  61. package/dist/mcp/tools/workflow.d.ts +1 -0
  62. package/dist/mcp/tools/workflow.d.ts.map +1 -1
  63. package/dist/mcp/tools/workflow.js +293 -46
  64. package/dist/mcp/tools/workflow.js.map +1 -1
  65. package/dist/mcp/utils/data-transformers.d.ts +47 -10
  66. package/dist/mcp/utils/data-transformers.d.ts.map +1 -1
  67. package/dist/mcp/utils/data-transformers.js +12 -9
  68. package/dist/mcp/utils/data-transformers.js.map +1 -1
  69. package/dist/mcp/utils/types.d.ts +2 -0
  70. package/dist/mcp/utils/types.d.ts.map +1 -1
  71. package/dist/mcp/utils/types.js.map +1 -1
  72. package/dist/mcp/webhook-handler.d.ts.map +1 -1
  73. package/dist/mcp/webhook-handler.js +4 -1
  74. package/dist/mcp/webhook-handler.js.map +1 -1
  75. package/dist/mcp/workspace-cache.d.ts +8 -2
  76. package/dist/mcp/workspace-cache.d.ts.map +1 -1
  77. package/dist/mcp/workspace-cache.js +12 -8
  78. package/dist/mcp/workspace-cache.js.map +1 -1
  79. package/dist/plugins/vipunen/tools.d.ts +1 -0
  80. package/dist/plugins/vipunen/tools.d.ts.map +1 -1
  81. package/dist/plugins/vipunen/tools.js.map +1 -1
  82. package/package.json +1 -1
  83. package/scripts/postinstall.cjs +0 -9
@@ -42,12 +42,67 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.appScaffoldTools = exports.publishHailerAppTool = exports.scaffoldHailerAppTool = void 0;
44
44
  const zod_1 = require("zod");
45
+ const child_process_1 = require("child_process");
46
+ const util_1 = require("util");
45
47
  const tool_registry_1 = require("../tool-registry");
46
48
  const logger_1 = require("../../lib/logger");
47
49
  const request_logger_1 = require("../../lib/request-logger");
48
50
  const config_1 = require("../../config");
49
51
  const tool_helpers_1 = require("../utils/tool-helpers");
52
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
50
53
  const logger = (0, logger_1.createLogger)({ component: 'app-scaffold' });
54
+ /**
55
+ * Field resolver utility — injected into every scaffolded app at src/hailer/field-resolver.ts.
56
+ * Lets apps reference fields by key (e.g. 'matchDate') or hex ID interchangeably.
57
+ * The resolver checks the workflow schema at runtime: if keys exist, it maps key→ID;
58
+ * if the input is already a valid field ID, it passes through unchanged.
59
+ */
60
+ const FIELD_RESOLVER_SOURCE = `import { Workflow } from '@hailer/app-sdk';
61
+
62
+ /**
63
+ * Creates a resolver that maps field key-or-ID → fieldId.
64
+ *
65
+ * Usage:
66
+ * const workflow = app.workflows.find(w => w._id === WORKFLOW_ID);
67
+ * const f = createFieldResolver(workflow?.fields);
68
+ * const value = activity.fields?.[f('matchDate')];
69
+ *
70
+ * - If input is already a field ID (exists in workflow.fields), returns it as-is.
71
+ * - If input matches a field's \`key\` property, returns that field's hex ID.
72
+ * - Falls back to the input unchanged (for workflows without keys).
73
+ */
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ export function createFieldResolver(workflowFields: Record<string, any> | undefined) {
76
+ const keyToId: Record<string, string> = {};
77
+
78
+ if (workflowFields) {
79
+ for (const [fieldId, fieldDef] of Object.entries(workflowFields)) {
80
+ if (fieldDef?.key) {
81
+ keyToId[fieldDef.key] = fieldId;
82
+ }
83
+ }
84
+ }
85
+
86
+ return (keyOrId: string): string => {
87
+ if (workflowFields && keyOrId in workflowFields) return keyOrId;
88
+ if (keyOrId in keyToId) return keyToId[keyOrId];
89
+ return keyOrId;
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Hook-friendly wrapper: resolves fields for a specific workflow from the app state.
95
+ *
96
+ * Usage:
97
+ * const { app } = useApp();
98
+ * const f = useFieldResolver(app.workflows, WORKFLOW_ID);
99
+ * const date = activity.fields?.[f('matchDate')];
100
+ */
101
+ export function useFieldResolver(workflows: Workflow[], workflowId: string) {
102
+ const workflow = workflows.find(w => w._id === workflowId);
103
+ return createFieldResolver(workflow?.fields);
104
+ }
105
+ `;
51
106
  // Color palette for app icons (gradient pairs)
52
107
  const APP_ICON_COLORS = [
53
108
  { color1: '#3b82f6', color2: '#1d4ed8' }, // Blue
@@ -85,60 +140,82 @@ function getColorFromName(name) {
85
140
  let hash = 0;
86
141
  for (let i = 0; i < name.length; i++) {
87
142
  hash = ((hash << 5) - hash) + name.charCodeAt(i);
88
- hash = hash & hash; // Convert to 32bit integer
143
+ hash |= 0; // Convert to 32-bit signed integer
89
144
  }
90
145
  const index = Math.abs(hash) % APP_ICON_COLORS.length;
91
146
  return APP_ICON_COLORS[index];
92
147
  }
148
+ function hexToRgb(hex) {
149
+ return {
150
+ r: parseInt(hex.slice(1, 3), 16),
151
+ g: parseInt(hex.slice(3, 5), 16),
152
+ b: parseInt(hex.slice(5, 7), 16),
153
+ };
154
+ }
93
155
  /**
94
- * Generate styled app icon as JPEG buffer
156
+ * Generate styled app icon as PNG buffer with solid background.
95
157
  */
96
158
  async function generateAppIcon(name) {
97
159
  const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
98
160
  const initials = getInitials(name);
99
161
  const colors = getColorFromName(name);
100
- const svg = `<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
101
- <defs>
102
- <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
103
- <stop offset="0%" style="stop-color:${colors.color1}"/>
104
- <stop offset="100%" style="stop-color:${colors.color2}"/>
105
- </linearGradient>
106
- <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
107
- <feDropShadow dx="2" dy="4" stdDeviation="3" flood-opacity="0.3"/>
108
- </filter>
109
- </defs>
110
- <rect width="256" height="256" rx="50" fill="url(#bgGrad)"/>
111
- <rect x="8" y="8" width="240" height="240" rx="42" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
162
+ const rgb = hexToRgb(colors.color1);
163
+ // Solid background — no transparency, no SVG gradients
164
+ const bg = await sharp({
165
+ create: { width: 256, height: 256, channels: 3, background: rgb }
166
+ }).png().toBuffer();
167
+ // White text overlay
168
+ const textSvg = Buffer.from(`<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
112
169
  <text x="128" y="168" text-anchor="middle"
113
170
  font-family="Arial Black, Arial, sans-serif"
114
171
  font-size="${initials.length > 2 ? 80 : 110}"
115
172
  font-weight="900"
116
- fill="white"
117
- filter="url(#shadow)">${initials}</text>
118
- </svg>`;
119
- return sharp(Buffer.from(svg))
120
- .flatten({ background: colors.color1 })
121
- .jpeg({ quality: 95 })
173
+ fill="white">${initials}</text>
174
+ </svg>`);
175
+ return sharp(bg)
176
+ .composite([{ input: textSvg, top: 0, left: 0 }])
177
+ .png()
122
178
  .toBuffer();
123
179
  }
124
- const scaffoldHailerAppDescription = `Scaffold a new Hailer app — creates Vite/React project, installs dependencies, creates dev app entry in Hailer, shares with workspace, starts dev server on port 3000. Use this for ALL new app development — never copy an existing app. Load \`hailer-app-builder\` skill for SDK patterns and component examples.`;
180
+ /**
181
+ * Generate app icon, write to temp file, upload with isPublic, clean up.
182
+ * Returns the file ID on success, undefined on failure.
183
+ */
184
+ async function uploadAppIcon(name, context) {
185
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
186
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
187
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
188
+ const iconBuffer = await generateAppIcon(name);
189
+ const tempPath = path.join(os.tmpdir(), `app-icon-${Date.now()}.png`);
190
+ try {
191
+ fs.writeFileSync(tempPath, iconBuffer);
192
+ const result = await context.hailer.uploadFile({
193
+ path: tempPath,
194
+ filename: 'app-icon.png',
195
+ isPublic: true,
196
+ });
197
+ return result.success ? result.fileId : undefined;
198
+ }
199
+ finally {
200
+ try {
201
+ fs.unlinkSync(tempPath);
202
+ }
203
+ catch { /* ignore */ }
204
+ }
205
+ }
206
+ const scaffoldHailerAppDescription = `Scaffold a new Hailer app from template. Creates a Vite/React project, installs dependencies, injects field-resolver utility, and configures vite + index.html. Use this for ALL new app development — never copy an existing app. Load \`hailer-app-builder\` skill for SDK patterns and component examples.`;
125
207
  exports.scaffoldHailerAppTool = {
126
208
  name: 'scaffold_hailer_app',
127
209
  group: tool_registry_1.ToolGroup.PLAYGROUND,
128
210
  description: scaffoldHailerAppDescription,
129
211
  schema: zod_1.z.object({
130
- projectName: zod_1.z.string().min(1).describe("Project folder name"),
131
- template: zod_1.z.enum(['react-ts', 'react', 'preact-ts', 'preact', 'svelte-ts', 'svelte', 'vanilla', 'vanilla-ts']).default('react-ts').describe("Template to use (react-ts recommended - includes Hailer theme, icons, and design system)"),
132
- description: zod_1.z.string().optional().describe("App description for Hailer"),
133
- targetDirectory: zod_1.z.string().optional().describe("Target directory (defaults to DEV_APPS_PATH or current)"),
212
+ projectName: zod_1.z.string().describe("Project folder name"),
213
+ template: zod_1.z.enum(['react-ts', 'react', 'preact-ts', 'preact', 'svelte-ts', 'svelte', 'vanilla', 'vanilla-ts']).default('react-ts').describe("Template to use"),
214
+ targetDirectory: zod_1.z.string().optional().describe("Target directory ALWAYS provide this. Use the project's apps/ folder (e.g., '/home/user/my-project/apps'). Falls back to DEV_APPS_PATH env var."),
215
+ description: zod_1.z.string().optional().describe("App description"),
134
216
  installDependencies: zod_1.z.coerce.boolean().optional().default(true).describe("Run npm install after scaffolding"),
135
- autoCreateDevApp: zod_1.z.coerce.boolean().optional().default(true).describe("Automatically create dev app entry in Hailer"),
136
- autoShareWithWorkspace: zod_1.z.coerce.boolean().optional().default(true).describe("Share app with entire workspace"),
137
- autoStartDevServer: zod_1.z.coerce.boolean().optional().default(true).describe("Start dev server in background on port 3000"),
138
- autoSpawnBuilder: zod_1.z.coerce.boolean().optional().default(false).describe("Return instruction for Claude to spawn a builder agent that will implement the app UI")
139
217
  }),
140
- async execute(args, context) {
141
- const { execSync, spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
218
+ async execute(args, _context) {
142
219
  const path = await Promise.resolve().then(() => __importStar(require('path')));
143
220
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
144
221
  try {
@@ -153,41 +230,21 @@ exports.scaffoldHailerAppTool = {
153
230
  }],
154
231
  };
155
232
  }
156
- let responseText = `🚀 **ONE-SHOT HAILER APP SETUP**\n\n`;
233
+ let responseText = `🚀 **HAILER APP SCAFFOLD**\n\n`;
157
234
  responseText += `**Project:** ${args.projectName}\n`;
158
235
  responseText += `**Template:** ${args.template}\n`;
159
236
  responseText += `**Location:** ${projectPath}\n\n`;
160
- // Step 1: Scaffold project
161
- responseText += `⏳ Step 1/8: Creating project from template...\n`;
237
+ // Step 1: Scaffold project via CLI
238
+ responseText += `⏳ Step 1: Creating project from template...\n`;
162
239
  try {
163
- // Ensure target directory exists (execSync throws misleading ENOENT if cwd doesn't exist)
164
240
  if (!fs.existsSync(targetDir)) {
165
241
  fs.mkdirSync(targetDir, { recursive: true });
166
242
  }
167
- const createCmd = `npm create @hailer/app@beta ${args.projectName} -- --template ${args.template}`;
168
- execSync(createCmd, {
243
+ await execAsync(`npx @hailer/create-app ${args.projectName} --template ${args.template}`, {
169
244
  cwd: targetDir,
170
- stdio: 'pipe'
245
+ timeout: 60_000,
171
246
  });
172
- responseText += `✅ Project scaffolded\n`;
173
- // Immediately update manifest.json with correct author (before Hailer reads it)
174
- try {
175
- const manifestPath = path.join(projectPath, 'public', 'manifest.json');
176
- if (fs.existsSync(manifestPath)) {
177
- const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
178
- const manifest = JSON.parse(manifestContent);
179
- manifest.author = 'Hailer Oy';
180
- if (context.email) {
181
- manifest.contributors = [context.email];
182
- }
183
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
184
- responseText += ` ✅ Set author: Hailer Oy\n`;
185
- }
186
- }
187
- catch {
188
- // Non-fatal, continue
189
- }
190
- responseText += `\n`;
247
+ responseText += `✅ Project scaffolded\n\n`;
191
248
  }
192
249
  catch (error) {
193
250
  const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
@@ -200,11 +257,11 @@ exports.scaffoldHailerAppTool = {
200
257
  }
201
258
  // Step 2: Install dependencies
202
259
  if (args.installDependencies !== false) {
203
- responseText += `⏳ Step 2/8: Installing dependencies...\n`;
260
+ responseText += `⏳ Step 2: Installing dependencies...\n`;
204
261
  try {
205
- execSync('npm install', {
262
+ await execAsync('npm install', {
206
263
  cwd: projectPath,
207
- stdio: 'pipe'
264
+ timeout: 180_000,
208
265
  });
209
266
  responseText += `✅ Dependencies installed\n\n`;
210
267
  }
@@ -214,288 +271,107 @@ exports.scaffoldHailerAppTool = {
214
271
  }
215
272
  }
216
273
  else {
217
- responseText += `⏭️ Step 2/8: Skipped (installDependencies = false)\n\n`;
274
+ responseText += `⏭️ Step 2: Skipped (installDependencies = false)\n\n`;
275
+ }
276
+ // Step 3: Inject field resolver utility into src/hailer/
277
+ responseText += `⏳ Step 3: Injecting field-resolver utility...\n`;
278
+ try {
279
+ const resolverPath = path.join(projectPath, 'src', 'hailer', 'field-resolver.ts');
280
+ fs.writeFileSync(resolverPath, FIELD_RESOLVER_SOURCE);
281
+ responseText += `✅ field-resolver.ts written to src/hailer/\n\n`;
282
+ }
283
+ catch (error) {
284
+ const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
285
+ responseText += `⚠️ Failed to inject field-resolver: ${errorMessage}\n\n`;
218
286
  }
219
- // Step 3: Configure CORS in vite.config.ts
220
- responseText += `⏳ Step 3/8: Configuring CORS for Hailer access...\n`;
287
+ // Step 4: Fix base: './' in vite.config.ts
288
+ responseText += `⏳ Step 4: Fixing vite.config.ts base path...\n`;
221
289
  try {
222
290
  const viteConfigPath = path.join(projectPath, 'vite.config.ts');
223
291
  if (fs.existsSync(viteConfigPath)) {
224
292
  let viteConfig = fs.readFileSync(viteConfigPath, 'utf-8');
225
- // Check if CORS is already configured
226
- if (!viteConfig.includes('cors:')) {
227
- // Add CORS configuration to the server block
228
- // Matches: server: { port: 3000 } or server: { port: 3000, } with optional whitespace
229
- viteConfig = viteConfig.replace(/server:\s*{\s*port:\s*3000\s*,?\s*}/, `server: {
230
- port: 3000,
231
- cors: {
232
- origin: '*',
233
- credentials: true
234
- }
235
- }`);
293
+ if (viteConfig.includes("base: ''")) {
294
+ viteConfig = viteConfig.replace("base: ''", "base: './'");
236
295
  fs.writeFileSync(viteConfigPath, viteConfig);
237
- responseText += `✅ CORS configured in vite.config.ts\n\n`;
296
+ responseText += `✅ Fixed base: './' in vite.config.ts\n\n`;
297
+ }
298
+ else if (viteConfig.includes("base: './'")) {
299
+ responseText += `✅ base already set to './'\n\n`;
238
300
  }
239
301
  else {
240
- responseText += `✅ CORS already configured\n\n`;
302
+ responseText += `⚠️ base not found in vite.config.ts, skipping\n\n`;
241
303
  }
242
304
  }
243
305
  else {
244
- responseText += `⚠️ vite.config.ts not found, skipping CORS configuration\n\n`;
306
+ responseText += `⚠️ vite.config.ts not found, skipping\n\n`;
245
307
  }
246
308
  }
247
309
  catch (error) {
248
310
  const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
249
- responseText += `⚠️ Failed to configure CORS: ${errorMessage}\n\n`;
311
+ responseText += `⚠️ Failed to fix vite.config.ts: ${errorMessage}\n\n`;
250
312
  }
251
- let appId;
252
- let workspaceId;
253
- // Step 4: Find existing dev app or create new one
254
- if (args.autoCreateDevApp !== false) {
255
- responseText += `⏳ Step 4/8: Setting up dev app entry in Hailer...\n`;
256
- try {
257
- workspaceId = (0, tool_helpers_1.getResolvedWorkspaceId)({}, context);
258
- if (!workspaceId) {
259
- responseText += `⚠️ Workspace cache not available, skipping app creation\n\n`;
313
+ // Step 5: Add cache-busting meta tags to index.html
314
+ responseText += `⏳ Step 5: Adding cache-busting meta tags to index.html...\n`;
315
+ try {
316
+ const indexPath = path.join(projectPath, 'index.html');
317
+ if (fs.existsSync(indexPath)) {
318
+ let indexHtml = fs.readFileSync(indexPath, 'utf-8');
319
+ if (!indexHtml.includes('no-cache')) {
320
+ const cacheMeta = [
321
+ ' <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />',
322
+ ' <meta http-equiv="Pragma" content="no-cache" />',
323
+ ' <meta http-equiv="Expires" content="0" />',
324
+ ].join('\n');
325
+ indexHtml = indexHtml.replace('<head>', `<head>\n${cacheMeta}`);
326
+ fs.writeFileSync(indexPath, indexHtml);
327
+ responseText += `✅ Cache-busting meta tags added to index.html\n\n`;
260
328
  }
261
329
  else {
262
- // Check for existing dev app (localhost:3000) to reuse
263
- try {
264
- const apps = await context.hailer.request('v3.app.list', [workspaceId]);
265
- const existingDevApp = apps.find((app) => app.url === 'http://localhost:3000' || app.url === 'http://localhost:3000/');
266
- if (existingDevApp) {
267
- appId = existingDevApp._id;
268
- responseText += `✅ Reusing existing dev app: ${existingDevApp.name} (${appId})\n`;
269
- responseText += ` URL: http://localhost:3000\n\n`;
270
- }
271
- }
272
- catch {
273
- // Non-fatal, will create new app below
274
- }
275
- // Create new dev app only if no existing one found
276
- if (!appId) {
277
- // Generate and upload app icon
278
- let imageId;
279
- try {
280
- responseText += ` 📸 Generating app icon...\n`;
281
- const iconBuffer = await generateAppIcon(args.projectName);
282
- // Save to temp file and upload
283
- const os = await Promise.resolve().then(() => __importStar(require('os')));
284
- const tempPath = path.join(os.tmpdir(), `app-icon-${Date.now()}.jpg`);
285
- fs.writeFileSync(tempPath, iconBuffer);
286
- const uploadResult = await context.hailer.uploadFile({
287
- path: tempPath,
288
- filename: 'app-icon.jpg',
289
- isPublic: true
290
- });
291
- // Clean up temp file
292
- try {
293
- fs.unlinkSync(tempPath);
294
- }
295
- catch { /* ignore */ }
296
- if (uploadResult.success && uploadResult.fileId) {
297
- imageId = uploadResult.fileId;
298
- responseText += ` ✅ Icon uploaded: ${imageId}\n`;
299
- }
300
- }
301
- catch (iconError) {
302
- logger.warn('Failed to generate/upload app icon', { error: iconError });
303
- responseText += ` ⚠️ Icon generation skipped\n`;
304
- }
305
- const appData = {
306
- cid: workspaceId,
307
- name: args.projectName,
308
- url: 'http://localhost:3000'
309
- };
310
- if (args.description) {
311
- appData.description = args.description;
312
- }
313
- if (imageId) {
314
- appData.image = imageId;
315
- }
316
- const result = await context.hailer.request('v3.app.create', [appData]);
317
- appId = result.appId || result._id;
318
- responseText += `✅ Dev app created: ${appId}\n`;
319
- responseText += ` URL: http://localhost:3000\n\n`;
320
- }
330
+ responseText += `✅ Cache-busting meta tags already present\n\n`;
321
331
  }
322
332
  }
323
- catch (error) {
324
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
325
- responseText += `⚠️ Failed to set up app in Hailer: ${errorMessage}\n\n`;
326
- }
327
- }
328
- else {
329
- responseText += `⏭️ Step 4/8: Skipped (autoCreateDevApp = false)\n\n`;
330
- }
331
- // Step 5: Share with workspace
332
- if (args.autoShareWithWorkspace !== false && appId && workspaceId) {
333
- responseText += `⏳ Step 5/8: Sharing app with workspace...\n`;
334
- try {
335
- await context.hailer.request('v3.app.member.add', [
336
- appId,
337
- `network_${workspaceId}`
338
- ]);
339
- responseText += `✅ App shared with entire workspace\n\n`;
340
- }
341
- catch (error) {
342
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
343
- responseText += `⚠️ Failed to share app: ${errorMessage}\n\n`;
333
+ else {
334
+ responseText += `⚠️ index.html not found, skipping\n\n`;
344
335
  }
345
336
  }
346
- else {
347
- responseText += `⏭️ Step 5/8: Skipped (no app ID or autoShareWithWorkspace = false)\n\n`;
337
+ catch (error) {
338
+ const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
339
+ responseText += `⚠️ Failed to update index.html: ${errorMessage}\n\n`;
348
340
  }
349
- // Step 6: Update manifest.json with app ID
350
- if (appId) {
351
- responseText += `⏳ Step 6/8: Adding appId to manifest.json...\n`;
352
- try {
353
- const manifestPath = path.join(projectPath, 'public', 'manifest.json');
354
- if (fs.existsSync(manifestPath)) {
355
- const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
356
- const manifest = JSON.parse(manifestContent);
357
- manifest.appId = appId;
358
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
359
- responseText += ` ✅ appId: ${appId}\n\n`;
360
- }
361
- else {
362
- responseText += `⚠️ manifest.json not found\n\n`;
341
+ // Step 6: Update manifest.json author to "Hailer Oy"
342
+ responseText += `⏳ Step 6: Updating manifest.json...\n`;
343
+ try {
344
+ const manifestPath = path.join(projectPath, 'public', 'manifest.json');
345
+ if (fs.existsSync(manifestPath)) {
346
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
347
+ manifest.author = 'Hailer Oy';
348
+ delete manifest.appId; // Clear template dummy — real appId assigned on first publish
349
+ if (args.description) {
350
+ manifest.description = args.description;
351
+ manifest.versionDescription = args.description;
363
352
  }
353
+ if (!manifest.version)
354
+ manifest.version = '1.0.0';
355
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
356
+ responseText += `✅ manifest.json updated (author: Hailer Oy)\n\n`;
364
357
  }
365
- catch (error) {
366
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
367
- responseText += `⚠️ Failed to update manifest: ${errorMessage}\n\n`;
368
- }
369
- }
370
- else {
371
- responseText += `⏭️ Step 6/8: Skipped (no app ID)\n\n`;
372
- }
373
- // Step 7: Start dev server
374
- if (args.autoStartDevServer !== false) {
375
- responseText += `⏳ Step 7/8: Starting dev server on port 3000...\n`;
376
- try {
377
- // Start dev server in background
378
- const devServer = spawn('npm', ['run', 'dev'], {
379
- cwd: projectPath,
380
- detached: true,
381
- stdio: 'ignore',
382
- shell: true
383
- });
384
- devServer.on('error', (err) => {
385
- logger.warn('Dev server spawn failed', { error: err.message });
386
- });
387
- devServer.unref();
388
- responseText += `✅ Dev server started in background\n`;
389
- responseText += ` PID: ${devServer.pid}\n`;
390
- responseText += ` URL: http://localhost:3000\n\n`;
391
- }
392
- catch (error) {
393
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
394
- responseText += `⚠️ Failed to start dev server: ${errorMessage}\n`;
395
- responseText += ` Start manually: cd ${args.projectName} && npm run dev\n\n`;
358
+ else {
359
+ responseText += `⚠️ manifest.json not found, skipping\n\n`;
396
360
  }
397
361
  }
398
- else {
399
- responseText += `⏭️ Step 7/8: Skipped (autoStartDevServer = false)\n\n`;
400
- }
401
- // Step 8: Complete
402
- responseText += `✅ **Step 8/8: Setup Complete!**\n\n`;
403
- // Final summary
404
- responseText += `## 🎉 Your Hailer App is Ready!\n\n`;
405
- responseText += `**What was done:**\n`;
406
- responseText += `- Project scaffolded from ${args.template} template\n`;
407
- if (args.installDependencies !== false)
408
- responseText += `- Dependencies installed\n`;
409
- responseText += `- ✅ CORS configured for Hailer access\n`;
410
- if (appId)
411
- responseText += `- ✅ Dev app entry created in Hailer (ID: \`${appId}\`)\n`;
412
- if (args.autoShareWithWorkspace !== false && appId)
413
- responseText += `- ✅ App shared with entire workspace\n`;
414
- if (appId)
415
- responseText += `- ✅ manifest.json configured with app ID\n`;
416
- if (args.autoStartDevServer !== false)
417
- responseText += `- ✅ Dev server started on http://localhost:3000\n`;
418
- responseText += `\n## 🚀 Next Steps\n\n`;
419
- responseText += `1. **Open the app in Hailer** - Look for "${args.projectName}" in your apps menu\n`;
420
- responseText += `2. **Start coding** - Files in \`${projectPath}/src\` will hot-reload\n`;
421
- responseText += `3. **View logs** - Check dev server output if needed\n\n`;
422
- responseText += `## 📂 Project Location\n\n`;
423
- responseText += `\`\`\`\n${projectPath}\n\`\`\`\n\n`;
424
- if (appId) {
425
- responseText += `## 🔗 App Info\n\n`;
426
- responseText += `- **App ID:** \`${appId}\`\n`;
427
- responseText += `- **Dev URL:** http://localhost:3000\n`;
428
- responseText += `- **Shared with:** Entire workspace\n`;
429
- }
430
- // Add spawn builder instruction if requested
431
- if (args.autoSpawnBuilder) {
432
- // Note: Workflow list is not available in workspace cache
433
- // The builder agent should call list_workflows_minimal to get workflows
434
- const workflowList = '';
435
- responseText += `\n---\n\n`;
436
- responseText += `## 🤖 SPAWN BUILDER AGENT\n\n`;
437
- responseText += `**IMPORTANT:** You MUST now spawn a builder agent to implement the app UI.\n\n`;
438
- responseText += `Use the Task tool with these parameters:\n\n`;
439
- responseText += `\`\`\`javascript\n`;
440
- responseText += `Task({\n`;
441
- responseText += ` subagent_type: "general-purpose",\n`;
442
- responseText += ` description: "Build ${args.projectName} app",\n`;
443
- responseText += ` prompt: \`You are building a Hailer app.\n`;
444
- responseText += `\n`;
445
- responseText += `PROJECT: ${projectPath}\n`;
446
- responseText += `DESCRIPTION: ${args.description || args.projectName}\n`;
447
- responseText += `\n`;
448
- responseText += `## Available Workflows\n`;
449
- responseText += `${workflowList}\n`;
450
- responseText += `\n`;
451
- responseText += `## MANDATORY: Load Skills First\n`;
452
- responseText += `1. Skill("building-hailer-apps-skill")\n`;
453
- responseText += `2. Skill("hailer-app-builder")\n`;
454
- responseText += `\n`;
455
- responseText += `## CRITICAL FIRST: Fix main.tsx\n`;
456
- responseText += `The template is missing ChakraProvider! Fix src/main.tsx:\n`;
457
- responseText += `\n`;
458
- responseText += `import React from 'react'\n`;
459
- responseText += `import ReactDOM from 'react-dom/client'\n`;
460
- responseText += `import { ChakraProvider } from '@chakra-ui/react'\n`;
461
- responseText += `import App from './App.tsx'\n`;
462
- responseText += `import './index.css'\n`;
463
- responseText += `\n`;
464
- responseText += `ReactDOM.createRoot(document.getElementById('root')!).render(\n`;
465
- responseText += ` <React.StrictMode>\n`;
466
- responseText += ` <ChakraProvider>\n`;
467
- responseText += ` <App />\n`;
468
- responseText += ` </ChakraProvider>\n`;
469
- responseText += ` </React.StrictMode>,\n`;
470
- responseText += `)\n`;
471
- responseText += `\n`;
472
- responseText += `## Build the App\n`;
473
- responseText += `After loading skills and fixing main.tsx:\n`;
474
- responseText += `1. Get workflow schemas with get_workflow_schema for fields you need\n`;
475
- responseText += `2. **CRITICAL: If using insights, call get_insight_data FIRST to get EXACT column structure**\n`;
476
- responseText += `3. Create src/constants.ts with workflow/field IDs\n`;
477
- responseText += `4. Create src/types/ with interfaces matching ACTUAL data structures\n`;
478
- responseText += `5. Create src/hooks/ with data fetching (use inside boolean, NOT hailer in deps)\n`;
479
- responseText += `6. Create src/components/ with Chakra UI\n`;
480
- responseText += `7. Update src/App.tsx\n`;
481
- responseText += `\n`;
482
- responseText += `## INSIGHT DATA WARNING\n`;
483
- responseText += `NEVER assume insight column names or structure. ALWAYS:\n`;
484
- responseText += `1. Call get_insight_data({ insightId, update: true })\n`;
485
- responseText += `2. Note exact column names (e.g., "total_value" not "totalValue")\n`;
486
- responseText += `3. Note column indices (row[0], row[1], etc.)\n`;
487
- responseText += `4. Build interfaces matching ACTUAL structure\n`;
488
- responseText += `\n`;
489
- responseText += `Report back confirming: styled UI works, no infinite loops, fields display correctly.\n`;
490
- responseText += `\`\n`;
491
- responseText += `})\n`;
492
- responseText += `\`\`\`\n`;
493
- }
362
+ catch (error) {
363
+ const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
364
+ responseText += `⚠️ Failed to update manifest.json: ${errorMessage}\n\n`;
365
+ }
366
+ // Summary
367
+ responseText += `## ✅ App Scaffolded\n\n`;
368
+ responseText += `**Location:** \`${projectPath}\`\n\n`;
369
+ responseText += `**Next steps:**\n`;
370
+ responseText += `1. \`cd ${projectPath} && npm run dev\` to start the dev server\n`;
371
+ responseText += `2. Build your app using \`hailer-app-builder\` skill patterns\n`;
372
+ responseText += `3. Run \`publish_hailer_app\` when ready to deploy\n`;
494
373
  return {
495
- content: [{
496
- type: "text",
497
- text: responseText,
498
- }],
374
+ content: [{ type: "text", text: responseText }],
499
375
  };
500
376
  }
501
377
  catch (error) {
@@ -503,7 +379,7 @@ exports.scaffoldHailerAppTool = {
503
379
  return {
504
380
  content: [{
505
381
  type: "text",
506
- text: `❌ **Error in one-shot setup**\n\n**Error:** ${errorMessage}\n\n**Common Issues:**\n- Node.js not installed or version < 18\n- npm not available\n- Insufficient permissions\n- Hailer API connection issues`,
382
+ text: `❌ **Error in scaffold**\n\n**Error:** ${errorMessage}\n\n**Common Issues:**\n- Node.js not installed or version < 18\n- npm not available\n- Insufficient permissions`,
507
383
  }],
508
384
  };
509
385
  }
@@ -512,21 +388,20 @@ exports.scaffoldHailerAppTool = {
512
388
  // ============================================================================
513
389
  // PUBLISH HAILER APP TOOL
514
390
  // ============================================================================
515
- const publishHailerAppDescription = `Publish a Hailer app to production — builds the project, packages as tar (dist + manifest.json), uploads to Hailer CDN, updates app URL from localhost to production. Load \`publish-hailer-app\` skill for the full publishing workflow and manifest validation.`;
391
+ const publishHailerAppDescription = `Publish a Hailer app to production — wraps \`npm run publish-production\` CLI. On first publish (no appId in manifest), creates the app entry, uploads icon, and shares with workspace. On subsequent publishes, updates the existing app. Load \`publish-hailer-app\` skill for the full publishing workflow and manifest validation.`;
516
392
  exports.publishHailerAppTool = {
517
393
  name: 'publish_hailer_app',
518
394
  group: tool_registry_1.ToolGroup.PLAYGROUND,
519
395
  description: publishHailerAppDescription,
520
396
  schema: zod_1.z.object({
521
- projectDirectory: zod_1.z.string().optional().describe("Path to app project (defaults to DEV_APPS_PATH or current directory)"),
522
- appId: zod_1.z.string().optional().describe("App ID to publish to (reads from manifest.json if not provided)"),
523
- targetId: zod_1.z.string().optional().describe("Optional marketplace target ID for publishing to marketplace"),
397
+ projectDirectory: zod_1.z.string().optional().describe("Path to the app project to publish — ALWAYS provide this (e.g., '/home/user/my-project/apps/my-app'). Falls back to DEV_APPS_PATH env var."),
398
+ targetId: zod_1.z.string().optional().describe("Optional marketplace target ID"),
524
399
  }),
525
400
  async execute(args, context) {
526
401
  const path = await Promise.resolve().then(() => __importStar(require('path')));
527
402
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
528
403
  const projectDir = args.projectDirectory || config_1.environment.DEV_APPS_PATH || path.join(process.cwd(), 'apps');
529
- // Check if directory exists
404
+ // Validate directory exists
530
405
  if (!fs.existsSync(projectDir)) {
531
406
  if (!request_logger_1.RequestLogger.getCurrent())
532
407
  logger.error('Project directory does not exist', { projectDir });
@@ -537,7 +412,7 @@ exports.publishHailerAppTool = {
537
412
  }],
538
413
  };
539
414
  }
540
- // Check if package.json exists and has build script
415
+ // Validate package.json exists with publish-production script
541
416
  const packageJsonPath = path.join(projectDir, 'package.json');
542
417
  if (!fs.existsSync(packageJsonPath)) {
543
418
  if (!request_logger_1.RequestLogger.getCurrent())
@@ -545,7 +420,7 @@ exports.publishHailerAppTool = {
545
420
  return {
546
421
  content: [{
547
422
  type: "text",
548
- text: `❌ **Not a Hailer App Project**\n\nNo package.json found in \`${projectDir}\`\n\n**This doesn't appear to be a Hailer app project.**\n\nMake sure you're in the correct directory.`,
423
+ text: `❌ **Not a Hailer App Project**\n\nNo package.json found in \`${projectDir}\`\n\nMake sure you're in the correct directory.`,
549
424
  }],
550
425
  };
551
426
  }
@@ -555,297 +430,147 @@ exports.publishHailerAppTool = {
555
430
  }
556
431
  catch (error) {
557
432
  const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
558
- if (!request_logger_1.RequestLogger.getCurrent())
559
- logger.error('Failed to parse package.json', { error: errorMessage });
560
433
  return {
561
434
  content: [{ type: "text", text: `❌ **Invalid package.json**\n\n${errorMessage}` }],
562
435
  };
563
436
  }
564
- if (!packageJson.scripts?.build) {
437
+ if (!packageJson.scripts?.['publish-production']) {
565
438
  return {
566
- content: [{ type: "text", text: '❌ **Missing Build Script**\n\npackage.json has no "build" script.' }],
439
+ content: [{ type: "text", text: '❌ **Missing publish-production Script**\n\npackage.json has no "publish-production" script.' }],
567
440
  };
568
441
  }
569
- // Read manifest.json to get/verify appId
442
+ // Read manifest.json
570
443
  const manifestPath = path.join(projectDir, 'public', 'manifest.json');
571
- let appId = args.appId;
572
- let appName = 'Unknown';
573
- let manifest;
574
444
  if (!fs.existsSync(manifestPath)) {
575
- if (!appId) {
576
- if (!request_logger_1.RequestLogger.getCurrent())
577
- logger.error('Manifest not found and no appId provided');
578
- return {
579
- content: [{
580
- type: "text",
581
- text: `❌ **Manifest Not Found**\n\nNo \`public/manifest.json\` found and no appId provided.\n\n**Steps to fix:**\n1. Ensure \`public/manifest.json\` exists\n2. Set appId in manifest\n\nOr provide appId parameter when calling this tool.`,
582
- }],
583
- };
584
- }
585
- manifest = {};
586
- }
587
- else {
588
- try {
589
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
590
- }
591
- catch (error) {
592
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
593
- if (!request_logger_1.RequestLogger.getCurrent())
594
- logger.error('Failed to parse manifest.json', { error: errorMessage });
595
- return {
596
- content: [{
597
- type: "text",
598
- text: `❌ **Invalid manifest.json**\n\nFailed to parse \`public/manifest.json\`: ${errorMessage}\n\n**Check:**\n- manifest.json is valid JSON\n- appId field is set correctly`,
599
- }],
600
- };
601
- }
602
- if (!appId) {
603
- appId = manifest.appId;
604
- }
605
- appName = manifest.name || appName;
606
- if (!appId) {
607
- if (!request_logger_1.RequestLogger.getCurrent())
608
- logger.error('No appId found');
609
- return {
610
- content: [{
611
- type: "text",
612
- text: `❌ **App ID Not Configured**\n\nNo appId found in \`manifest.json\` and none provided.\n\n**Steps to fix:**\n1. Get your published app ID from Hailer\n2. Edit \`public/manifest.json\`\n3. Set \`"appId": "your-app-id-here"\`\n\nOr provide appId parameter when calling this tool.`,
613
- }],
614
- };
615
- }
616
- // Validate version fields
617
- if (!manifest.version || manifest.version.trim() === '') {
618
- if (!request_logger_1.RequestLogger.getCurrent())
619
- logger.error('version field missing or empty');
620
- return {
621
- content: [{
622
- type: "text",
623
- text: `❌ **Version Not Set**\n\nThe \`version\` field in \`manifest.json\` is missing or empty.\n\n**Steps to fix:**\nEdit \`public/manifest.json\` and add:\n\`\`\`json\n"version": "1.0.0",\n"versionDescription": "Initial release"\n\`\`\`\n\nThe SDK requires version fields for publishing.`,
624
- }],
625
- };
626
- }
627
- if (!manifest.versionDescription || manifest.versionDescription.trim() === '') {
628
- if (!request_logger_1.RequestLogger.getCurrent())
629
- logger.error('versionDescription field missing or empty');
630
- return {
631
- content: [{
632
- type: "text",
633
- text: `❌ **Version Description Not Set**\n\nThe \`versionDescription\` field in \`manifest.json\` is missing or empty.\n\n**Steps to fix:**\nEdit \`public/manifest.json\` and add:\n\`\`\`json\n"version": "${manifest.version}",\n"versionDescription": "Description of this version"\n\`\`\`\n\nThe SDK requires version fields for publishing.`,
634
- }],
635
- };
636
- }
637
- }
638
- // Check if appId points to a dev app (localhost) — if so, create a new production app
639
- const workspaceId = context.workspaceCache.currentWorkspace._id;
640
- let isNewProdApp = false;
641
- try {
642
- const apps = await context.hailer.request('v3.app.list', [workspaceId]);
643
- const currentApp = apps.find((app) => app._id === appId);
644
- if (currentApp && currentApp.url && (currentApp.url.includes('localhost') || currentApp.url.includes('127.0.0.1'))) {
645
- // Dev app — create a new production app entry
646
- logger.debug('Dev app detected, creating production app entry', { devAppId: appId });
647
- const prodAppData = {
648
- cid: workspaceId,
649
- name: appName,
650
- url: `https://apps.hailer.com/${workspaceId}/placeholder/`
651
- };
652
- const prodResult = await context.hailer.request('v3.app.create', [prodAppData]);
653
- const prodAppId = prodResult.appId || prodResult._id;
654
- if (prodAppId) {
655
- // Share with workspace
656
- try {
657
- await context.hailer.request('v3.app.member.add', [prodAppId, `network_${workspaceId}`]);
658
- }
659
- catch { /* non-fatal */ }
660
- logger.debug('Production app created', { devAppId: appId, prodAppId });
661
- appId = prodAppId;
662
- isNewProdApp = true;
663
- // Update manifest with new production appId
664
- if (fs.existsSync(manifestPath)) {
665
- const updatedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
666
- updatedManifest.appId = appId;
667
- fs.writeFileSync(manifestPath, JSON.stringify(updatedManifest, null, 2));
668
- }
669
- }
670
- }
671
- }
672
- catch (error) {
673
- logger.warn('Failed to check dev app status, proceeding with provided appId', { error: (0, tool_helpers_1.extractErrorMessage)(error) });
445
+ return {
446
+ content: [{
447
+ type: "text",
448
+ text: `❌ **Manifest Not Found**\n\nNo \`public/manifest.json\` found in \`${projectDir}\`.\n\nEnsure \`public/manifest.json\` exists.`,
449
+ }],
450
+ };
674
451
  }
675
- // Log the start of publish
676
- logger.debug('Publishing app via API key...', { appId, appName, isNewProdApp, workspace: context.workspaceCache.currentWorkspace.name });
677
- // Step 1: Run npm run build
678
- const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
452
+ let manifest;
679
453
  try {
680
- execSync('npm run build', { cwd: projectDir, stdio: 'pipe', timeout: 120000 });
454
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
681
455
  }
682
456
  catch (error) {
683
457
  const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
684
- if (!request_logger_1.RequestLogger.getCurrent())
685
- logger.error('Build failed', { error: errorMessage });
686
458
  return {
687
- content: [{ type: "text", text: `❌ **Build Failed**\n\n\`\`\`\n${errorMessage}\n\`\`\`` }],
459
+ content: [{
460
+ type: "text",
461
+ text: `❌ **Invalid manifest.json**\n\nFailed to parse \`public/manifest.json\`: ${errorMessage}`,
462
+ }],
688
463
  };
689
464
  }
690
- // Step 2: Verify dist/ exists
691
- const distPath = path.join(projectDir, 'dist');
692
- if (!fs.existsSync(distPath)) {
465
+ // Validate version fields
466
+ if (!manifest.version || manifest.version.trim() === '') {
693
467
  return {
694
- content: [{ type: "text", text: `❌ **dist/ directory not found after build at ${distPath}**` }],
468
+ content: [{
469
+ type: "text",
470
+ text: `❌ **Version Not Set**\n\nThe \`version\` field in \`manifest.json\` is missing or empty.\n\n**Steps to fix:**\nEdit \`public/manifest.json\` and add:\n\`\`\`json\n"version": "1.0.0",\n"versionDescription": "Initial release"\n\`\`\``,
471
+ }],
695
472
  };
696
473
  }
697
- // Step 3: Copy manifest.json into dist/ (backend expects package/dist/manifest.json)
698
- const distManifestPath = path.join(projectDir, 'dist', 'manifest.json');
699
- try {
700
- fs.copyFileSync(manifestPath, distManifestPath);
474
+ if (!manifest.versionDescription || manifest.versionDescription.trim() === '') {
475
+ return {
476
+ content: [{
477
+ type: "text",
478
+ text: `❌ **Version Description Not Set**\n\nThe \`versionDescription\` field in \`manifest.json\` is missing or empty.\n\n**Steps to fix:**\nEdit \`public/manifest.json\` and add:\n\`\`\`json\n"version": "${manifest.version}",\n"versionDescription": "Description of this version"\n\`\`\``,
479
+ }],
480
+ };
701
481
  }
702
- catch (error) {
703
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
482
+ const workspaceId = (0, tool_helpers_1.getResolvedWorkspaceId)({}, context);
483
+ if (!workspaceId) {
484
+ return {
485
+ content: [{ type: "text", text: `❌ **No workspace** — workspace cache not available.` }],
486
+ };
487
+ }
488
+ // Use session key — the CLI publish script passes it as hlrkey header
489
+ const authKey = context.client.sessionKey || context.apiKey;
490
+ if (!authKey) {
704
491
  return {
705
- content: [{ type: "text", text: `❌ **Failed to copy manifest.json to dist/:** ${errorMessage}` }],
492
+ content: [{ type: "text", text: `❌ **No auth key** cannot authenticate with publish CLI.` }],
706
493
  };
707
494
  }
708
- // Step 4: Create .tgz archive with package/ prefix (matches npm pack format)
709
- const tar = await Promise.resolve().then(() => __importStar(require('tar')));
710
- const tgzFilename = `${appId}-${manifest.version || 'latest'}.tgz`;
711
- const tgzPath = path.join(projectDir, tgzFilename);
495
+ const isFirstPublish = !manifest.appId;
496
+ // Derive human-readable app name from directory (e.g., "upcoming-matches" → "Upcoming Matches")
497
+ const appName = manifest.name || path.basename(projectDir).split(/[-_]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
498
+ const targetId = args.targetId || manifest.targetId;
499
+ let publishCmd;
500
+ if (isFirstPublish) {
501
+ publishCmd = `npm run publish-production -- --create --app-name "${appName}" --workspace ${workspaceId} --user-api-key ${authKey} --force`;
502
+ if (targetId)
503
+ publishCmd += ' --market';
504
+ }
505
+ else {
506
+ publishCmd = `npm run publish-production -- --user-api-key ${authKey} --force`;
507
+ if (targetId)
508
+ publishCmd += ' --market';
509
+ }
510
+ logger.debug('Publishing app via CLI', { isFirstPublish, appName, workspaceId });
712
511
  try {
713
- await tar.create({ gzip: true, file: tgzPath, cwd: projectDir, prefix: 'package' }, ['dist', 'package.json']);
512
+ await execAsync(publishCmd, { cwd: projectDir, timeout: 120_000 });
714
513
  }
715
514
  catch (error) {
716
515
  const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
516
+ if (!request_logger_1.RequestLogger.getCurrent())
517
+ logger.error('Publish CLI failed', { error: errorMessage });
717
518
  return {
718
- content: [{ type: "text", text: `❌ **Failed to create .tgz archive:** ${errorMessage}` }],
519
+ content: [{ type: "text", text: `❌ **Publish Failed**\n\n\`\`\`\n${errorMessage}\n\`\`\`` }],
719
520
  };
720
521
  }
721
- // Step 5: Upload to /app/publish using API key (following file-upload.ts pattern)
722
- const FormData = (await Promise.resolve().then(() => __importStar(require('form-data')))).default;
723
- const formData = new FormData();
724
- formData.append('file', fs.createReadStream(tgzPath), {
725
- filename: tgzFilename,
726
- contentType: 'application/gzip',
727
- });
728
- formData.append('appId', appId);
729
- if (args.targetId) {
730
- formData.append('targetId', args.targetId);
731
- }
732
- const apiBaseUrl = context.client.socket.host;
733
- const sessionKey = context.client.sessionKey;
734
- const publishUrl = new URL(`${apiBaseUrl}/app/publish`);
735
- const https = await Promise.resolve().then(() => __importStar(require('https')));
736
- const http = await Promise.resolve().then(() => __importStar(require('http')));
522
+ // Re-read manifest to get appId written by CLI on first publish
523
+ let publishedAppId = manifest.appId;
737
524
  try {
738
- const uploadResponse = await new Promise((resolve, reject) => {
739
- const options = {
740
- hostname: publishUrl.hostname,
741
- port: publishUrl.port || (publishUrl.protocol === 'https:' ? 443 : 80),
742
- path: publishUrl.pathname,
743
- method: 'POST',
744
- headers: {
745
- 'hlrkey': sessionKey,
746
- ...formData.getHeaders(),
747
- },
748
- };
749
- const req = (publishUrl.protocol === 'https:' ? https : http).request(options, (res) => {
750
- const chunks = [];
751
- res.on('data', (chunk) => chunks.push(chunk));
752
- res.on('end', () => {
753
- resolve({
754
- statusCode: res.statusCode || 500,
755
- body: Buffer.concat(chunks).toString('utf-8'),
756
- });
757
- });
758
- });
759
- req.on('error', reject);
760
- formData.pipe(req);
761
- });
762
- // Clean up .tgz file
763
- try {
764
- fs.unlinkSync(tgzPath);
765
- }
766
- catch { /* non-fatal */ }
767
- if (uploadResponse.statusCode < 200 || uploadResponse.statusCode >= 300) {
768
- if (!request_logger_1.RequestLogger.getCurrent())
769
- logger.error('Publish failed', { status: uploadResponse.statusCode });
770
- return {
771
- content: [{
772
- type: "text",
773
- text: `❌ **Publish Failed (${uploadResponse.statusCode})**\n\n\`\`\`\n${uploadResponse.body}\n\`\`\`\n\n**Common Issues:**\n- Invalid API key\n- App ID not found\n- Network issues`,
774
- }],
775
- };
776
- }
777
- let result;
778
- try {
779
- result = JSON.parse(uploadResponse.body);
780
- }
781
- catch {
782
- result = { raw: uploadResponse.body };
783
- }
784
- logger.debug('App published successfully', { appId, appName });
785
- // Auto-update app URL to production after successful publish
786
- const productionUrl = `https://apps.hailer.com/${workspaceId}/${appId}/`;
787
- let urlUpdated = false;
788
- try {
789
- await context.hailer.request('v3.app.update', [appId, { name: appName, url: productionUrl }]);
790
- urlUpdated = true;
791
- logger.debug('App URL updated to production', { appId, productionUrl });
792
- }
793
- catch (urlError) {
794
- const urlErrorMsg = (0, tool_helpers_1.extractErrorMessage)(urlError);
795
- logger.warn('Failed to update app URL after publish', { appId, error: urlErrorMsg });
796
- }
797
- let responseText = `✅ **App Published Successfully!**\n\n`;
798
- if (isNewProdApp) {
799
- responseText += `🆕 **New production app created** (dev app kept at localhost)\n`;
800
- }
801
- responseText += `**App Name:** ${appName}\n`;
802
- responseText += `**App ID:** \`${appId}\`\n`;
803
- responseText += `**Version:** ${manifest.version || 'unknown'}\n`;
804
- responseText += `**Description:** ${manifest.versionDescription || 'N/A'}\n`;
805
- if (urlUpdated) {
806
- responseText += `**URL:** ${productionUrl}\n`;
807
- }
808
- else {
809
- responseText += `⚠️ **URL not updated** — manually call \`update_app\` to set production URL\n`;
810
- }
811
- if (typeof result.size === 'number') {
812
- responseText += `**Size:** ${(result.size / 1024).toFixed(2)} KB\n`;
813
- }
814
- if (args.targetId) {
815
- responseText += `**Marketplace Target:** ${args.targetId}\n`;
816
- }
817
- if (result.manifest) {
818
- responseText += `\n**Manifest:**\n\`\`\`json\n${JSON.stringify(result.manifest, null, 2)}\n\`\`\`\n`;
819
- }
820
- responseText += `\n## Next Steps\n\n`;
821
- responseText += `1. **Open published app in Hailer** to verify\n`;
822
- responseText += `2. **Share with users** - use \`add_app_member\` tool:\n`;
525
+ const updatedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
526
+ publishedAppId = updatedManifest.appId || publishedAppId;
527
+ }
528
+ catch { /* use existing */ }
529
+ // Post-first-publish: icon + name/description update and workspace share (parallel)
530
+ if (isFirstPublish && publishedAppId) {
531
+ const iconAndUpdate = async () => {
532
+ const imageId = await uploadAppIcon(appName, context);
533
+ const updateData = { name: appName };
534
+ if (manifest.versionDescription)
535
+ updateData.description = manifest.versionDescription;
536
+ if (imageId)
537
+ updateData.image = imageId;
538
+ await context.hailer.request('v3.app.update', [publishedAppId, updateData]);
539
+ logger.debug('App updated with icon/name/description', { publishedAppId, imageId });
540
+ };
541
+ const share = async () => {
542
+ await context.hailer.request('v3.app.member.add', [publishedAppId, `network_${workspaceId}`]);
543
+ logger.debug('App shared with workspace', { publishedAppId, workspaceId });
544
+ };
545
+ await Promise.allSettled([iconAndUpdate(), share()]);
546
+ }
547
+ const productionUrl = publishedAppId ? `https://apps.hailer.com/${workspaceId}/${publishedAppId}/` : undefined;
548
+ let responseText = `✅ **App Published Successfully!**\n\n`;
549
+ if (isFirstPublish)
550
+ responseText += `🆕 **First publish** — app created in Hailer, shared with workspace\n\n`;
551
+ responseText += `**App Name:** ${appName}\n`;
552
+ if (publishedAppId)
553
+ responseText += `**App ID:** \`${publishedAppId}\`\n`;
554
+ responseText += `**Version:** ${manifest.version}\n`;
555
+ responseText += `**Description:** ${manifest.versionDescription}\n`;
556
+ if (productionUrl)
557
+ responseText += `**URL:** ${productionUrl}\n`;
558
+ if (targetId)
559
+ responseText += `**Marketplace Target:** ${targetId}\n`;
560
+ responseText += `\n## Next Steps\n\n`;
561
+ responseText += `1. **Open published app in Hailer** to verify\n`;
562
+ if (publishedAppId) {
563
+ responseText += `2. **Share with specific users** - use \`add_app_member\` tool:\n`;
823
564
  responseText += ` \`\`\`javascript\n`;
824
565
  responseText += ` add_app_member({\n`;
825
- responseText += ` appId: "${appId}",\n`;
566
+ responseText += ` appId: "${publishedAppId}",\n`;
826
567
  responseText += ` member: "network_${workspaceId}" // or team_*, user_*\n`;
827
568
  responseText += ` })\n`;
828
569
  responseText += ` \`\`\`\n`;
829
- return {
830
- content: [{ type: "text", text: responseText }],
831
- };
832
- }
833
- catch (error) {
834
- // Clean up .tgz file on failure
835
- try {
836
- fs.unlinkSync(tgzPath);
837
- }
838
- catch { /* non-fatal */ }
839
- const errorMessage = (0, tool_helpers_1.extractErrorMessage)(error);
840
- if (!request_logger_1.RequestLogger.getCurrent())
841
- logger.error('Error publishing Hailer app', { error: errorMessage });
842
- return {
843
- content: [{
844
- type: "text",
845
- text: `❌ **Upload Failed**\n\n**Error:** ${errorMessage}\n\n**Common Issues:**\n- Network connectivity issues\n- Invalid API key\n- Hailer API unavailable`,
846
- }],
847
- };
848
570
  }
571
+ return {
572
+ content: [{ type: "text", text: responseText }],
573
+ };
849
574
  }
850
575
  };
851
576
  /** All app scaffold tools */