@hailer/mcp 1.1.17-beta.3 → 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.
- package/.claude/CLAUDE.md +94 -135
- package/.claude/skills/create-and-publish-app/SKILL.md +127 -0
- package/.claude/skills/hailer-app-builder/SKILL.md +47 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +35 -4
- package/.claude/skills/sdk-function-fields/SKILL.md +431 -287
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +2 -0
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/bot.d.ts +2 -1
- package/dist/bot/bot.d.ts.map +1 -1
- package/dist/bot/bot.js +109 -41
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/services/message-classifier.d.ts.map +1 -1
- package/dist/bot/services/message-classifier.js +6 -0
- package/dist/bot/services/message-classifier.js.map +1 -1
- package/dist/bot/services/signal-router.d.ts.map +1 -1
- package/dist/bot/services/signal-router.js +1 -0
- package/dist/bot/services/signal-router.js.map +1 -1
- package/dist/bot/services/system-prompt.d.ts +4 -0
- package/dist/bot/services/system-prompt.d.ts.map +1 -1
- package/dist/bot/services/system-prompt.js +41 -12
- package/dist/bot/services/system-prompt.js.map +1 -1
- package/dist/bot/services/types.d.ts +7 -31
- package/dist/bot/services/types.d.ts.map +1 -1
- package/dist/bot/services/workspace-refresh.js.map +1 -1
- package/dist/bot/workspace-overview.d.ts.map +1 -1
- package/dist/bot/workspace-overview.js +4 -1
- package/dist/bot/workspace-overview.js.map +1 -1
- package/dist/bot-config/context.js.map +1 -1
- package/dist/bot-config/loader.d.ts.map +1 -1
- package/dist/bot-config/loader.js +1 -0
- package/dist/bot-config/loader.js.map +1 -1
- package/dist/bot-config/types.d.ts +2 -0
- package/dist/bot-config/types.d.ts.map +1 -1
- package/dist/mcp/UserContextCache.d.ts.map +1 -1
- package/dist/mcp/UserContextCache.js +8 -16
- package/dist/mcp/UserContextCache.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts +3 -2
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +14 -9
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/activity.d.ts.map +1 -1
- package/dist/mcp/tools/activity.js +39 -94
- package/dist/mcp/tools/activity.js.map +1 -1
- package/dist/mcp/tools/app-scaffold.d.ts.map +1 -1
- package/dist/mcp/tools/app-scaffold.js +300 -575
- package/dist/mcp/tools/app-scaffold.js.map +1 -1
- package/dist/mcp/tools/date.d.ts +5 -0
- package/dist/mcp/tools/date.d.ts.map +1 -0
- package/dist/mcp/tools/date.js +23 -0
- package/dist/mcp/tools/date.js.map +1 -0
- package/dist/mcp/tools/discussion.d.ts.map +1 -1
- package/dist/mcp/tools/discussion.js +17 -9
- package/dist/mcp/tools/discussion.js.map +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/insight.d.ts.map +1 -1
- package/dist/mcp/tools/insight.js +13 -19
- package/dist/mcp/tools/insight.js.map +1 -1
- package/dist/mcp/tools/workflow.d.ts +1 -0
- package/dist/mcp/tools/workflow.d.ts.map +1 -1
- package/dist/mcp/tools/workflow.js +293 -46
- package/dist/mcp/tools/workflow.js.map +1 -1
- package/dist/mcp/utils/data-transformers.d.ts +47 -10
- package/dist/mcp/utils/data-transformers.d.ts.map +1 -1
- package/dist/mcp/utils/data-transformers.js +12 -9
- package/dist/mcp/utils/data-transformers.js.map +1 -1
- package/dist/mcp/utils/types.d.ts +2 -0
- package/dist/mcp/utils/types.d.ts.map +1 -1
- package/dist/mcp/utils/types.js.map +1 -1
- package/dist/mcp/webhook-handler.d.ts.map +1 -1
- package/dist/mcp/webhook-handler.js +4 -1
- package/dist/mcp/webhook-handler.js.map +1 -1
- package/dist/mcp/workspace-cache.d.ts +8 -2
- package/dist/mcp/workspace-cache.d.ts.map +1 -1
- package/dist/mcp/workspace-cache.js +12 -8
- package/dist/mcp/workspace-cache.js.map +1 -1
- package/dist/plugins/vipunen/tools.d.ts +1 -0
- package/dist/plugins/vipunen/tools.d.ts.map +1 -1
- package/dist/plugins/vipunen/tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.
|
|
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
|
-
|
|
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().
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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,
|
|
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 = `🚀 **
|
|
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
|
|
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
|
-
|
|
168
|
-
execSync(createCmd, {
|
|
243
|
+
await execAsync(`npx @hailer/create-app ${args.projectName} --template ${args.template}`, {
|
|
169
244
|
cwd: targetDir,
|
|
170
|
-
|
|
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
|
|
260
|
+
responseText += `⏳ Step 2: Installing dependencies...\n`;
|
|
204
261
|
try {
|
|
205
|
-
|
|
262
|
+
await execAsync('npm install', {
|
|
206
263
|
cwd: projectPath,
|
|
207
|
-
|
|
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
|
|
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
|
|
220
|
-
responseText += `⏳ Step
|
|
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
|
-
|
|
226
|
-
|
|
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 += `✅
|
|
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 +=
|
|
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
|
|
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
|
|
311
|
+
responseText += `⚠️ Failed to fix vite.config.ts: ${errorMessage}\n\n`;
|
|
250
312
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
responseText +=
|
|
405
|
-
responseText += `**
|
|
406
|
-
responseText +=
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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 —
|
|
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 (
|
|
522
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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\
|
|
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?.
|
|
437
|
+
if (!packageJson.scripts?.['publish-production']) {
|
|
565
438
|
return {
|
|
566
|
-
content: [{ type: "text", text: '❌ **Missing
|
|
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
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [{
|
|
459
|
+
content: [{
|
|
460
|
+
type: "text",
|
|
461
|
+
text: `❌ **Invalid manifest.json**\n\nFailed to parse \`public/manifest.json\`: ${errorMessage}`,
|
|
462
|
+
}],
|
|
688
463
|
};
|
|
689
464
|
}
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
if (!fs.existsSync(distPath)) {
|
|
465
|
+
// Validate version fields
|
|
466
|
+
if (!manifest.version || manifest.version.trim() === '') {
|
|
693
467
|
return {
|
|
694
|
-
content: [{
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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: `❌ **
|
|
492
|
+
content: [{ type: "text", text: `❌ **No auth key** — cannot authenticate with publish CLI.` }],
|
|
706
493
|
};
|
|
707
494
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
const
|
|
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
|
|
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
|
|
519
|
+
content: [{ type: "text", text: `❌ **Publish Failed**\n\n\`\`\`\n${errorMessage}\n\`\`\`` }],
|
|
719
520
|
};
|
|
720
521
|
}
|
|
721
|
-
//
|
|
722
|
-
|
|
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
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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: "${
|
|
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 */
|