@hailer/mcp 1.1.10 → 1.1.12
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/.opencode/agent/agent-helga-workflow-config.md +1 -2
- package/SESSION-HANDOFF.md +68 -0
- package/dist/bot/bot-config.d.ts +2 -0
- package/dist/bot/bot-config.js +34 -11
- package/dist/bot/bot-manager.d.ts +11 -3
- package/dist/bot/bot-manager.js +73 -25
- package/dist/bot/bot.d.ts +13 -0
- package/dist/bot/bot.js +100 -25
- package/dist/mcp/hailer-clients.js +4 -1
- package/dist/mcp/signal-handler.js +10 -12
- package/dist/mcp/webhook-handler.d.ts +2 -0
- package/dist/mcp/webhook-handler.js +3 -0
- package/inbox/2026-03-04-bot-config-patterns.md +24 -0
- package/package.json +3 -2
- package/scripts/postinstall.cjs +64 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-helga-workflow-config
|
|
3
3
|
description: Manages Hailer workspace configuration as infrastructure-as-code using SDK v0.8.4.
|
|
4
|
-
model: sonnet
|
|
4
|
+
model: anthropic/claude-sonnet-4-5
|
|
5
5
|
tools:
|
|
6
6
|
bash: true
|
|
7
7
|
read: true
|
|
@@ -53,7 +53,6 @@ For on-demand skills, the orchestrator will say "Load skill X" — use the Skill
|
|
|
53
53
|
11. **ACTIVITYLINK FIELDS** - data must be plain string array: `["workflowId"]` NOT `[{workflowId: "..."}]`.
|
|
54
54
|
12. **NUMBER FIELDS** - Type: `numeric` (not number).
|
|
55
55
|
13. **FIELD TYPES ARE IMMUTABLE** - Cannot change field type via API. To change text→number: create new field, migrate data, delete old field. Or change in Hailer UI manually.
|
|
56
|
-
14. **CANNOT RENAME WORKFLOWS** - Workflow names cannot be changed via SDK. To "rename": create new workflow with desired name, migrate data, delete old workflow. Or rename in Hailer UI manually.
|
|
57
56
|
|
|
58
57
|
**Delegation to specialists:**
|
|
59
58
|
- For insight SQL query design → delegate to Viktor (after creating insight entry in insights.ts)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Session Handoff
|
|
2
|
+
|
|
3
|
+
**Last Updated:** 2026-03-04
|
|
4
|
+
|
|
5
|
+
## What Was Done
|
|
6
|
+
|
|
7
|
+
### System Prompt — Full End-to-End Wiring
|
|
8
|
+
Wired `systemPrompt` field from Agent Directory through the entire bot pipeline:
|
|
9
|
+
|
|
10
|
+
- **`src/bot-config/constants.ts`** — added `FIELD_KEY_SYSTEM_PROMPT`
|
|
11
|
+
- **`src/bot-config/context.ts`** — `BotSchema.fields.systemPrompt`, `BotCredentials.systemPrompt`, schema discovery, `extractCredentials()`
|
|
12
|
+
- **`src/bot-config/webhooks.ts`** — extract systemPrompt in `extractCredentialsFromActivity()`
|
|
13
|
+
- **`src/bot-config/loader.ts`** — `BotConfigFile` types + `saveBotConfig()` includes systemPrompt
|
|
14
|
+
- **`src/bot/bot-config.ts`** — `BotConfig.systemPrompt`, reads from `orchestrator.systemPrompt`
|
|
15
|
+
- **`src/bot/bot-manager.ts`** — `BotUpdateEntry.systemPrompt`, passes to Bot; smart restart logic: if only systemPrompt changed (credentials same) → `bot.updateSystemPrompt()` without restart
|
|
16
|
+
- **`src/bot/bot.ts`** — `_systemPrompt` instance field (hot-swappable), `updateSystemPrompt()` public method, `buildSystemPrompt()` uses custom prompt + always appends `<bot-identity>` + `<workspace-context>`
|
|
17
|
+
- **`src/commands/seed-config.ts`** — `extractSystemPromptFromActivity()`, wired into orchestrator + specialist output
|
|
18
|
+
- **`src/mcp/webhook-handler.ts`** — `BotEntry.systemPrompt`, `WorkspaceConfig.orchestrator.systemPrompt`, extracts from webhook payload
|
|
19
|
+
|
|
20
|
+
### System Prompt Design
|
|
21
|
+
- Custom prompt from AI Hub used as full body; `{wsName}`, `{userId}`, `{botName}` substituted at runtime
|
|
22
|
+
- `<bot-identity>` and `<workspace-context>` always appended by bot — never stored
|
|
23
|
+
- Workspace-isolated: each `Bot` instance has own `_systemPrompt`, `BotManager.bots` Map keyed by workspaceId
|
|
24
|
+
- Phase change → full restart picks up new prompt from config file
|
|
25
|
+
- Field edit → webhook fires (with delay) → `handleBotConfigWebhook()` → `updateSystemPrompt()` (no restart)
|
|
26
|
+
|
|
27
|
+
## Current Work
|
|
28
|
+
|
|
29
|
+
### Uncommitted — All System Prompt Changes
|
|
30
|
+
10 files modified, compiles clean (`tsc --noEmit` passes). Not yet committed or pushed.
|
|
31
|
+
|
|
32
|
+
## Next Steps
|
|
33
|
+
|
|
34
|
+
1. **Commit the system prompt wiring** — all 10 files are ready
|
|
35
|
+
2. **Test end-to-end**: edit prompt in AI Hub → wait for webhook → verify bot uses new prompt
|
|
36
|
+
3. **Cloudflare** — user mentioned needing this (for remote MCP / public webhook URL). See `docs/prd-remote-mcp-server.md` and `docs/prd-remote-mcp-connector.md`
|
|
37
|
+
4. **`activityId` passthrough** (optional improvement) — pass orchestrator's `activityId` into Bot so it can subscribe to `activities.updated` socket signal for instant prompt updates (vs waiting for webhook)
|
|
38
|
+
|
|
39
|
+
## Key Decisions
|
|
40
|
+
|
|
41
|
+
- **Hot-swap without restart**: Only credentials change triggers bot restart; systemPrompt-only changes apply live via `updateSystemPrompt()`
|
|
42
|
+
- **Workspace isolation**: Guaranteed by `Map<workspaceId, Bot>` — webhook `cid` field routes to correct bot instance
|
|
43
|
+
- **Webhook-driven updates**: Phase change → webhook (requires public URL). Field-only changes → webhook fires with delay. Socket `activities.updated` would be instant but requires `activityId` passthrough (not yet done)
|
|
44
|
+
- **Prompt structure**: Custom prompt replaces the rules section; `<bot-identity>` + `<workspace-context>` always runtime-injected
|
|
45
|
+
|
|
46
|
+
## Files Modified (uncommitted)
|
|
47
|
+
|
|
48
|
+
- `src/bot-config/constants.ts` — FIELD_KEY_SYSTEM_PROMPT
|
|
49
|
+
- `src/bot-config/context.ts` — BotSchema, BotCredentials, discovery, extractCredentials
|
|
50
|
+
- `src/bot-config/loader.ts` — BotConfigFile types + saveBotConfig
|
|
51
|
+
- `src/bot-config/webhooks.ts` — extractCredentialsFromActivity
|
|
52
|
+
- `src/bot/bot-config.ts` — BotConfig + loadBotConfigs
|
|
53
|
+
- `src/bot/bot-manager.ts` — BotUpdateEntry, smart restart, updateSystemPrompt call
|
|
54
|
+
- `src/bot/bot.ts` — _systemPrompt field, updateSystemPrompt(), buildSystemPrompt(), email/password getters
|
|
55
|
+
- `src/commands/seed-config.ts` — extractSystemPromptFromActivity + wired into output
|
|
56
|
+
- `src/mcp/webhook-handler.ts` — BotEntry, WorkspaceConfig types + extraction
|
|
57
|
+
|
|
58
|
+
## Context to Preserve
|
|
59
|
+
|
|
60
|
+
- Agent Directory workflow ID: `69885261c432a499a35313b6`
|
|
61
|
+
- Deployed phase: `8fa543c69749ab4e8f9e3967`, Retired phase: `df66a80fe7cfde1c3b928038`
|
|
62
|
+
- AI Hub app: `695e3665701ef8e3824beebe`, marketplace targetId: `698351e8dda756e689368b60`
|
|
63
|
+
- `systemPrompt` field key must be exactly `systemPrompt` (camelCase) — field type: textarea
|
|
64
|
+
- **seed-config.ts is an independent copy** of field extraction — NOT shared with context.ts. Always update both when adding new Agent Directory fields
|
|
65
|
+
- **Two bot-config readers**: `src/bot-config/loader.ts` (BotContext/webhook path) AND `src/bot/bot-config.ts` (BotManager startup path) — both need updating
|
|
66
|
+
- **webhook-handler.ts has its own local types** (BotEntry, WorkspaceConfig) — update independently
|
|
67
|
+
- Webhook fires on phase changes; field edits may have delay before webhook triggers
|
|
68
|
+
- `app-edit-guard --agent-on` does NOT propagate to subagents — do ai-hub edits directly
|
package/dist/bot/bot-config.d.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* Extracts orchestrator credentials into a flat BotConfig interface.
|
|
6
6
|
*/
|
|
7
7
|
export interface BotConfig {
|
|
8
|
+
activityId: string;
|
|
8
9
|
workspaceId: string;
|
|
9
10
|
workspaceName: string;
|
|
10
11
|
email: string;
|
|
11
12
|
password: string;
|
|
12
13
|
displayName?: string;
|
|
14
|
+
systemPrompt?: string;
|
|
13
15
|
enabled: boolean;
|
|
14
16
|
apiHost: string;
|
|
15
17
|
}
|
package/dist/bot/bot-config.js
CHANGED
|
@@ -68,21 +68,43 @@ function loadBotConfigs() {
|
|
|
68
68
|
try {
|
|
69
69
|
const filePath = path.join(configDir, file);
|
|
70
70
|
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
71
|
+
// Load orchestrator
|
|
71
72
|
const orch = raw.orchestrator;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
if (orch?.email && orch?.password) {
|
|
74
|
+
configs.push({
|
|
75
|
+
activityId: orch.activityId || '',
|
|
76
|
+
workspaceId: raw.workspaceId,
|
|
77
|
+
workspaceName: raw.workspaceName,
|
|
78
|
+
email: orch.email,
|
|
79
|
+
password: orch.password,
|
|
80
|
+
displayName: orch.displayName,
|
|
81
|
+
systemPrompt: orch.systemPrompt,
|
|
82
|
+
enabled: true,
|
|
83
|
+
apiHost: getApiHost(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Load specialists
|
|
87
|
+
if (Array.isArray(raw.specialists)) {
|
|
88
|
+
for (const spec of raw.specialists) {
|
|
89
|
+
if (spec?.email && spec?.password && spec?.enabled !== false) {
|
|
90
|
+
configs.push({
|
|
91
|
+
activityId: spec.activityId || '',
|
|
92
|
+
workspaceId: raw.workspaceId,
|
|
93
|
+
workspaceName: raw.workspaceName,
|
|
94
|
+
email: spec.email,
|
|
95
|
+
password: spec.password,
|
|
96
|
+
displayName: spec.displayName,
|
|
97
|
+
systemPrompt: spec.systemPrompt,
|
|
98
|
+
enabled: true,
|
|
99
|
+
apiHost: getApiHost(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
82
104
|
logger.debug('Loaded bot config', {
|
|
83
105
|
workspaceId: raw.workspaceId,
|
|
84
106
|
workspaceName: raw.workspaceName,
|
|
85
|
-
|
|
107
|
+
botCount: configs.filter(c => c.workspaceId === raw.workspaceId).length,
|
|
86
108
|
});
|
|
87
109
|
}
|
|
88
110
|
catch (error) {
|
|
@@ -124,6 +146,7 @@ function saveBotConfig(config) {
|
|
|
124
146
|
email: config.email,
|
|
125
147
|
password: config.password,
|
|
126
148
|
displayName: config.displayName,
|
|
149
|
+
systemPrompt: config.systemPrompt,
|
|
127
150
|
} : null,
|
|
128
151
|
specialists: existing.specialists ?? [],
|
|
129
152
|
lastSynced: new Date().toISOString(),
|
|
@@ -7,23 +7,31 @@
|
|
|
7
7
|
import { ToolRegistry } from '../mcp/tool-registry';
|
|
8
8
|
import { BotConfig } from './bot-config';
|
|
9
9
|
interface BotUpdateEntry {
|
|
10
|
+
activityId: string;
|
|
10
11
|
email: string;
|
|
11
12
|
password: string;
|
|
12
13
|
botType: string;
|
|
13
14
|
enabled: boolean;
|
|
14
15
|
displayName?: string;
|
|
16
|
+
systemPrompt?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare class BotManager {
|
|
17
19
|
private toolRegistry;
|
|
18
20
|
private anthropicApiKey;
|
|
21
|
+
/** Key: activityId → Bot instance */
|
|
19
22
|
private bots;
|
|
23
|
+
/** Key: workspaceId → Set of activityIds (for cross-bot awareness) */
|
|
24
|
+
private workspaceBots;
|
|
20
25
|
private apiHost;
|
|
21
26
|
constructor(toolRegistry: ToolRegistry, anthropicApiKey: string);
|
|
27
|
+
/** Get all bot userIds in a workspace (for self-message guard) */
|
|
28
|
+
getBotUserIdsForWorkspace(workspaceId: string): Set<string>;
|
|
22
29
|
startAll(): Promise<void>;
|
|
23
30
|
startBot(config: BotConfig): Promise<void>;
|
|
24
|
-
stopBot(
|
|
31
|
+
stopBot(botKey: string, workspaceId?: string): Promise<void>;
|
|
25
32
|
stopAll(): Promise<void>;
|
|
26
33
|
getStatus(): Array<{
|
|
34
|
+
activityId: string;
|
|
27
35
|
workspaceId: string;
|
|
28
36
|
connected: boolean;
|
|
29
37
|
displayName: string;
|
|
@@ -31,8 +39,8 @@ export declare class BotManager {
|
|
|
31
39
|
getBotCount(): number;
|
|
32
40
|
/**
|
|
33
41
|
* Handle hot reload from webhook updates.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
42
|
+
* Supports all bot types. Routes by activityId so multiple bots
|
|
43
|
+
* can run in the same workspace independently.
|
|
36
44
|
*/
|
|
37
45
|
handleBotUpdate(workspaceId: string, bot: BotUpdateEntry, action: 'add' | 'update' | 'remove'): Promise<void>;
|
|
38
46
|
}
|
package/dist/bot/bot-manager.js
CHANGED
|
@@ -14,13 +14,29 @@ const logger = (0, logger_1.createLogger)({ component: 'bot-manager' });
|
|
|
14
14
|
class BotManager {
|
|
15
15
|
toolRegistry;
|
|
16
16
|
anthropicApiKey;
|
|
17
|
+
/** Key: activityId → Bot instance */
|
|
17
18
|
bots = new Map();
|
|
19
|
+
/** Key: workspaceId → Set of activityIds (for cross-bot awareness) */
|
|
20
|
+
workspaceBots = new Map();
|
|
18
21
|
apiHost;
|
|
19
22
|
constructor(toolRegistry, anthropicApiKey) {
|
|
20
23
|
this.toolRegistry = toolRegistry;
|
|
21
24
|
this.anthropicApiKey = anthropicApiKey;
|
|
22
25
|
this.apiHost = process.env.BOT_API_BASE_URL || 'https://api.hailer.com';
|
|
23
26
|
}
|
|
27
|
+
/** Get all bot userIds in a workspace (for self-message guard) */
|
|
28
|
+
getBotUserIdsForWorkspace(workspaceId) {
|
|
29
|
+
const ids = new Set();
|
|
30
|
+
const activityIds = this.workspaceBots.get(workspaceId);
|
|
31
|
+
if (activityIds) {
|
|
32
|
+
for (const actId of activityIds) {
|
|
33
|
+
const bot = this.bots.get(actId);
|
|
34
|
+
if (bot?.botUserId)
|
|
35
|
+
ids.add(bot.botUserId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
24
40
|
async startAll() {
|
|
25
41
|
const configs = (0, bot_config_1.loadBotConfigs)();
|
|
26
42
|
const enabled = configs.filter(c => c.enabled);
|
|
@@ -37,11 +53,13 @@ class BotManager {
|
|
|
37
53
|
logger.debug('Bot startup complete', { succeeded, failed });
|
|
38
54
|
}
|
|
39
55
|
async startBot(config) {
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
const botKey = config.activityId || config.workspaceId; // fallback for legacy configs
|
|
57
|
+
if (this.bots.has(botKey)) {
|
|
58
|
+
logger.warn('Bot already running', { activityId: botKey, workspaceId: config.workspaceId });
|
|
42
59
|
return;
|
|
43
60
|
}
|
|
44
61
|
logger.debug('Starting bot', {
|
|
62
|
+
activityId: botKey,
|
|
45
63
|
workspaceId: config.workspaceId,
|
|
46
64
|
workspace: config.workspaceName,
|
|
47
65
|
});
|
|
@@ -52,32 +70,50 @@ class BotManager {
|
|
|
52
70
|
anthropicApiKey: this.anthropicApiKey,
|
|
53
71
|
toolRegistry: this.toolRegistry,
|
|
54
72
|
workspaceId: config.workspaceId,
|
|
73
|
+
systemPrompt: config.systemPrompt,
|
|
74
|
+
botManager: this,
|
|
55
75
|
});
|
|
56
76
|
await bot.start();
|
|
57
|
-
this.bots.set(
|
|
58
|
-
|
|
77
|
+
this.bots.set(botKey, bot);
|
|
78
|
+
// Track in workspace lookup
|
|
79
|
+
if (!this.workspaceBots.has(config.workspaceId)) {
|
|
80
|
+
this.workspaceBots.set(config.workspaceId, new Set());
|
|
81
|
+
}
|
|
82
|
+
this.workspaceBots.get(config.workspaceId).add(botKey);
|
|
83
|
+
logger.debug('Bot started', { activityId: botKey, workspaceId: config.workspaceId });
|
|
59
84
|
}
|
|
60
|
-
async stopBot(workspaceId) {
|
|
61
|
-
const bot = this.bots.get(
|
|
85
|
+
async stopBot(botKey, workspaceId) {
|
|
86
|
+
const bot = this.bots.get(botKey);
|
|
62
87
|
if (!bot)
|
|
63
88
|
return;
|
|
64
89
|
await bot.stop();
|
|
65
|
-
this.bots.delete(
|
|
66
|
-
|
|
90
|
+
this.bots.delete(botKey);
|
|
91
|
+
// Clean up workspace tracking
|
|
92
|
+
if (workspaceId) {
|
|
93
|
+
const wsSet = this.workspaceBots.get(workspaceId);
|
|
94
|
+
if (wsSet) {
|
|
95
|
+
wsSet.delete(botKey);
|
|
96
|
+
if (wsSet.size === 0)
|
|
97
|
+
this.workspaceBots.delete(workspaceId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
logger.debug('Bot stopped', { activityId: botKey, workspaceId });
|
|
67
101
|
}
|
|
68
102
|
async stopAll() {
|
|
69
103
|
logger.debug('Stopping all bots', { count: this.bots.size });
|
|
70
|
-
await Promise.allSettled(Array.from(this.bots.entries()).map(async ([
|
|
104
|
+
await Promise.allSettled(Array.from(this.bots.entries()).map(async ([botKey, bot]) => {
|
|
71
105
|
await bot.stop();
|
|
72
|
-
logger.debug('Bot stopped', {
|
|
106
|
+
logger.debug('Bot stopped', { activityId: botKey });
|
|
73
107
|
}));
|
|
74
108
|
this.bots.clear();
|
|
109
|
+
this.workspaceBots.clear();
|
|
75
110
|
}
|
|
76
111
|
getStatus() {
|
|
77
|
-
return Array.from(this.bots.entries()).map(([
|
|
78
|
-
|
|
112
|
+
return Array.from(this.bots.entries()).map(([botKey, bot]) => ({
|
|
113
|
+
activityId: botKey,
|
|
114
|
+
workspaceId: bot.workspaceId || botKey,
|
|
79
115
|
connected: bot.connected,
|
|
80
|
-
displayName:
|
|
116
|
+
displayName: bot.workspaceId || botKey,
|
|
81
117
|
}));
|
|
82
118
|
}
|
|
83
119
|
getBotCount() {
|
|
@@ -85,48 +121,60 @@ class BotManager {
|
|
|
85
121
|
}
|
|
86
122
|
/**
|
|
87
123
|
* Handle hot reload from webhook updates.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
124
|
+
* Supports all bot types. Routes by activityId so multiple bots
|
|
125
|
+
* can run in the same workspace independently.
|
|
90
126
|
*/
|
|
91
127
|
async handleBotUpdate(workspaceId, bot, action) {
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
const botKey = bot.activityId;
|
|
129
|
+
if (!botKey) {
|
|
130
|
+
logger.warn('Bot update missing activityId, skipping', { workspaceId });
|
|
94
131
|
return;
|
|
95
132
|
}
|
|
96
|
-
const running = this.bots.has(
|
|
133
|
+
const running = this.bots.has(botKey);
|
|
97
134
|
if (action === 'remove' || !bot.enabled) {
|
|
98
135
|
if (running) {
|
|
99
|
-
logger.info('Stopping bot (disabled via webhook)', { workspaceId });
|
|
100
|
-
await this.stopBot(workspaceId);
|
|
136
|
+
logger.info('Stopping bot (disabled via webhook)', { activityId: botKey, workspaceId });
|
|
137
|
+
await this.stopBot(botKey, workspaceId);
|
|
101
138
|
}
|
|
102
139
|
return;
|
|
103
140
|
}
|
|
104
141
|
// Validate credentials before proceeding
|
|
105
142
|
if (!/.+@.+\..+/.test(bot.email) || !bot.password || bot.password.length < 6) {
|
|
106
|
-
logger.warn('Invalid bot credentials, skipping', { workspaceId });
|
|
143
|
+
logger.warn('Invalid bot credentials, skipping', { activityId: botKey, workspaceId });
|
|
107
144
|
return;
|
|
108
145
|
}
|
|
109
146
|
// action is 'add' or 'update' with enabled=true
|
|
110
147
|
if (running) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
const runningBot = this.bots.get(botKey);
|
|
149
|
+
// If only the system prompt changed, update it live — no restart needed
|
|
150
|
+
const credentialsChanged = runningBot.email !== bot.email || runningBot.password !== bot.password;
|
|
151
|
+
if (!credentialsChanged) {
|
|
152
|
+
logger.info('Updating system prompt live (no restart needed)', { activityId: botKey, workspaceId });
|
|
153
|
+
runningBot.updateSystemPrompt(bot.systemPrompt);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Credentials changed — full restart required
|
|
157
|
+
logger.info('Restarting bot (credentials updated via webhook)', { activityId: botKey, workspaceId });
|
|
158
|
+
await this.stopBot(botKey, workspaceId);
|
|
114
159
|
}
|
|
115
160
|
const config = {
|
|
161
|
+
activityId: botKey,
|
|
116
162
|
workspaceId,
|
|
117
163
|
workspaceName: workspaceId,
|
|
118
164
|
email: bot.email,
|
|
119
165
|
password: bot.password,
|
|
120
166
|
displayName: bot.displayName,
|
|
167
|
+
systemPrompt: bot.systemPrompt,
|
|
121
168
|
enabled: true,
|
|
122
169
|
apiHost: this.apiHost,
|
|
123
170
|
};
|
|
124
171
|
try {
|
|
125
172
|
await this.startBot(config);
|
|
126
|
-
logger.info('Bot started via webhook', { workspaceId, displayName: bot.displayName });
|
|
173
|
+
logger.info('Bot started via webhook', { activityId: botKey, workspaceId, displayName: bot.displayName });
|
|
127
174
|
}
|
|
128
175
|
catch (error) {
|
|
129
176
|
logger.error('Failed to start bot via webhook', {
|
|
177
|
+
activityId: botKey,
|
|
130
178
|
workspaceId,
|
|
131
179
|
error: error instanceof Error ? error.message : String(error),
|
|
132
180
|
});
|
package/dist/bot/bot.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class Bot {
|
|
|
18
18
|
private workspaceOverview;
|
|
19
19
|
private userId;
|
|
20
20
|
private _workspaceId;
|
|
21
|
+
private _systemPrompt;
|
|
21
22
|
private conversationManager;
|
|
22
23
|
private messageClassifier;
|
|
23
24
|
private messageFormatter;
|
|
@@ -45,6 +46,7 @@ export declare class Bot {
|
|
|
45
46
|
private adminUserIds;
|
|
46
47
|
private ownerUserIds;
|
|
47
48
|
private config;
|
|
49
|
+
private botManager;
|
|
48
50
|
constructor(config: {
|
|
49
51
|
email: string;
|
|
50
52
|
password: string;
|
|
@@ -53,9 +55,20 @@ export declare class Bot {
|
|
|
53
55
|
model?: string;
|
|
54
56
|
toolRegistry: ToolRegistry;
|
|
55
57
|
workspaceId?: string;
|
|
58
|
+
systemPrompt?: string;
|
|
59
|
+
botManager?: any;
|
|
56
60
|
});
|
|
61
|
+
get email(): string;
|
|
62
|
+
get password(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Hot-update the system prompt without restarting the bot.
|
|
65
|
+
* Takes effect on the next message processed.
|
|
66
|
+
*/
|
|
67
|
+
updateSystemPrompt(prompt: string | undefined): void;
|
|
57
68
|
get connected(): boolean;
|
|
58
69
|
get workspaceId(): string | undefined;
|
|
70
|
+
/** Exposed for BotManager cross-bot awareness */
|
|
71
|
+
get botUserId(): string;
|
|
59
72
|
start(): Promise<void>;
|
|
60
73
|
stop(): Promise<void>;
|
|
61
74
|
private handleSignal;
|
package/dist/bot/bot.js
CHANGED
|
@@ -72,6 +72,19 @@ const UNGATED_TOOLS = new Set([
|
|
|
72
72
|
'fetch_discussion_messages',
|
|
73
73
|
'get_activity_from_discussion',
|
|
74
74
|
]);
|
|
75
|
+
/** Minimal tool set for SIMPLE-routed messages (greetings, quick lookups). */
|
|
76
|
+
const SIMPLE_TOOLS = new Set([
|
|
77
|
+
'list_workflows_minimal',
|
|
78
|
+
'list_activities',
|
|
79
|
+
'show_activity_by_id',
|
|
80
|
+
'count_activities',
|
|
81
|
+
'search_workspace_users',
|
|
82
|
+
'get_workspace_balance',
|
|
83
|
+
'list_my_discussions',
|
|
84
|
+
'fetch_discussion_messages',
|
|
85
|
+
'add_discussion_message',
|
|
86
|
+
'get_insight_data',
|
|
87
|
+
]);
|
|
75
88
|
const MODEL_HAIKU = 'claude-haiku-4-5-20251001';
|
|
76
89
|
const MODEL_SONNET = 'claude-sonnet-4-5-20250929';
|
|
77
90
|
const MAX_TOOL_ITERATIONS = 10;
|
|
@@ -91,6 +104,8 @@ class Bot {
|
|
|
91
104
|
workspaceOverview = '';
|
|
92
105
|
userId = '';
|
|
93
106
|
_workspaceId;
|
|
107
|
+
// Live-updatable system prompt (separate from config so it can be hot-swapped)
|
|
108
|
+
_systemPrompt;
|
|
94
109
|
// Services
|
|
95
110
|
conversationManager = null;
|
|
96
111
|
messageClassifier = null;
|
|
@@ -121,23 +136,44 @@ class Bot {
|
|
|
121
136
|
ownerUserIds = new Set();
|
|
122
137
|
// Config
|
|
123
138
|
config;
|
|
139
|
+
botManager; // BotManager ref for cross-bot awareness
|
|
124
140
|
constructor(config) {
|
|
125
141
|
this.config = {
|
|
126
142
|
...config,
|
|
127
143
|
model: config.model || 'claude-haiku-4-5-20251001',
|
|
128
144
|
};
|
|
145
|
+
this.botManager = config.botManager || null;
|
|
146
|
+
this._systemPrompt = config.systemPrompt;
|
|
129
147
|
this.logger = (0, logger_1.createLogger)({ component: 'Bot' });
|
|
130
148
|
this.clientManager = new hailer_clients_1.HailerClientManager(config.apiHost, config.email, config.password);
|
|
131
149
|
this.toolExecutor = new tool_executor_1.ToolExecutor(config.toolRegistry);
|
|
132
150
|
}
|
|
151
|
+
get email() { return this.config.email; }
|
|
152
|
+
get password() { return this.config.password; }
|
|
153
|
+
/**
|
|
154
|
+
* Hot-update the system prompt without restarting the bot.
|
|
155
|
+
* Takes effect on the next message processed.
|
|
156
|
+
*/
|
|
157
|
+
updateSystemPrompt(prompt) {
|
|
158
|
+
this._systemPrompt = prompt;
|
|
159
|
+
this.logger.info('System prompt updated live', { hasPrompt: !!prompt });
|
|
160
|
+
}
|
|
133
161
|
get connected() {
|
|
134
162
|
return this._connected;
|
|
135
163
|
}
|
|
136
164
|
get workspaceId() {
|
|
137
165
|
return this._workspaceId;
|
|
138
166
|
}
|
|
167
|
+
/** Exposed for BotManager cross-bot awareness */
|
|
168
|
+
get botUserId() {
|
|
169
|
+
return this.userId;
|
|
170
|
+
}
|
|
139
171
|
// ===== LIFECYCLE =====
|
|
140
172
|
async start() {
|
|
173
|
+
if (this._connected) {
|
|
174
|
+
this.logger.warn('Bot.start() called while already connected, ignoring');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
141
177
|
this.logger.debug('Starting bot', { email: this.config.email });
|
|
142
178
|
// 1. Connect to Hailer
|
|
143
179
|
this.client = await this.clientManager.connect();
|
|
@@ -271,10 +307,16 @@ class Bot {
|
|
|
271
307
|
return;
|
|
272
308
|
try {
|
|
273
309
|
const rawMsgId = signal.data.msg_id;
|
|
274
|
-
|
|
275
|
-
|
|
310
|
+
const discussionId = signal.data.discussion;
|
|
311
|
+
const dedupKey = rawMsgId || (discussionId ? `${discussionId}:${signal.data.uid}:${signal.data.created}` : null);
|
|
312
|
+
if (dedupKey) {
|
|
313
|
+
if (this.processedMessageIds.has(dedupKey))
|
|
276
314
|
return;
|
|
277
|
-
this.processedMessageIds.add(
|
|
315
|
+
this.processedMessageIds.add(dedupKey);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
this.logger.warn('messenger.new signal has no dedup key, skipping');
|
|
319
|
+
return;
|
|
278
320
|
}
|
|
279
321
|
const message = await this.messageClassifier.extractIncomingMessage(signal);
|
|
280
322
|
if (!message) {
|
|
@@ -282,9 +324,14 @@ class Bot {
|
|
|
282
324
|
this.processedMessageIds.delete(rawMsgId);
|
|
283
325
|
return;
|
|
284
326
|
}
|
|
285
|
-
// Self-message guard: prevent bot feedback loops
|
|
327
|
+
// Self-message guard: prevent bot feedback loops (checks ALL bots in this workspace)
|
|
286
328
|
if (message.senderId === this.userId)
|
|
287
329
|
return;
|
|
330
|
+
if (this.botManager && this._workspaceId) {
|
|
331
|
+
const botUserIds = this.botManager.getBotUserIdsForWorkspace(this._workspaceId);
|
|
332
|
+
if (botUserIds.has(message.senderId))
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
288
335
|
// Rate limiting
|
|
289
336
|
const now = Date.now();
|
|
290
337
|
const cutoff = now - RATE_LIMIT_WINDOW;
|
|
@@ -598,7 +645,7 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
598
645
|
async runLlmLoop(message, route, signal) {
|
|
599
646
|
const conversation = this.conversationManager.getConversation(message.discussionId);
|
|
600
647
|
const systemPrompt = this.buildSystemPrompt();
|
|
601
|
-
const tools = this.getAnthropicTools();
|
|
648
|
+
const tools = this.getAnthropicTools(route.classification);
|
|
602
649
|
const processingStartTime = Date.now();
|
|
603
650
|
for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
|
|
604
651
|
const cachedConversation = this.conversationManager.prepareForCaching(conversation);
|
|
@@ -987,41 +1034,69 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
987
1034
|
buildSystemPrompt() {
|
|
988
1035
|
const wsName = this.init?.network?.name || 'Workspace';
|
|
989
1036
|
const botName = this.getBotDisplayName();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
<bot-identity>
|
|
1037
|
+
const identity = `<bot-identity>
|
|
993
1038
|
Your user ID: ${this.userId}
|
|
994
1039
|
Your display name: ${botName}
|
|
995
|
-
|
|
1040
|
+
Workspace: ${wsName}
|
|
1041
|
+
</bot-identity>`;
|
|
1042
|
+
// Platform rules — always applied regardless of custom prompt.
|
|
1043
|
+
// These are operational requirements that keep the bot functional.
|
|
1044
|
+
const platformRules = `<platform-rules>
|
|
1045
|
+
- Use tools to answer questions — don't guess or make up data.
|
|
1046
|
+
- When a tool call fails, tell the user what failed and why. Never silently try alternatives.
|
|
1047
|
+
- For bulk operations, batch into groups of 25-50 per tool call.
|
|
1048
|
+
- NEVER output raw URLs like https://app.hailer.com/...
|
|
996
1049
|
|
|
997
|
-
|
|
1050
|
+
HAILERTAG LINKING (mandatory):
|
|
1051
|
+
- When referencing activities, discussions, or users, ALWAYS use: [hailerTag|Display Name](objectId)
|
|
1052
|
+
- Examples:
|
|
1053
|
+
[hailerTag|AC Milan deal](691ffe654217e9e8434e578a)
|
|
1054
|
+
[hailerTag|John Smith](691ffe654217e9e8434e5123)
|
|
1055
|
+
- Tool responses include both names and IDs — use them directly in this format.
|
|
1056
|
+
- NEVER use hailerTag for workflows, phases, teams, or groups — these don't support linking. Just use their plain text names.
|
|
1057
|
+
- NEVER output bare 24-character hex IDs to the user.
|
|
1058
|
+
</platform-rules>`;
|
|
1059
|
+
const wsContext = `<workspace-context>
|
|
1060
|
+
${this.workspaceOverview}
|
|
1061
|
+
</workspace-context>`;
|
|
1062
|
+
const customPrompt = this._systemPrompt?.trim();
|
|
1063
|
+
if (customPrompt) {
|
|
1064
|
+
// Custom prompt = the bot's mission brief. It defines personality, scope, and behavior.
|
|
1065
|
+
// Platform rules and identity are always appended — they keep the bot functional.
|
|
1066
|
+
const body = customPrompt
|
|
1067
|
+
.replace(/\{wsName\}/g, wsName)
|
|
1068
|
+
.replace(/\{userId\}/g, this.userId)
|
|
1069
|
+
.replace(/\{botName\}/g, botName);
|
|
1070
|
+
return `${body}\n\n${platformRules}\n\n${identity}\n\n${wsContext}`;
|
|
1071
|
+
}
|
|
1072
|
+
// Default prompt — general-purpose workspace assistant with behavioral defaults.
|
|
1073
|
+
const defaultBehavior = `<behavior>
|
|
998
1074
|
- Be concise. Short answers, no filler.
|
|
999
1075
|
- Never list your capabilities unprompted. If asked what you can do, give a 1-2 sentence summary based on the workspace context below, not a feature dump.
|
|
1000
1076
|
- Never use emojis unless the user does first.
|
|
1001
|
-
-
|
|
1002
|
-
- When a task will require multiple steps (investigating errors, fixing data, bulk lookups), briefly tell the user what you're specifically about to do before starting. Be concrete: "I'll check each insight's SQL and fix any broken column references." NEVER use vague filler like "Let me look into that" — always name the actual action.
|
|
1077
|
+
- When a task will require multiple steps, briefly tell the user what you're about to do. Be concrete — never use vague filler like "Let me look into that".
|
|
1003
1078
|
- When showing data, use clean formatting (tables, bullet points). Don't over-explain.
|
|
1004
1079
|
- If a request is ambiguous, ask a clarifying question instead of guessing.
|
|
1005
|
-
- Always reference actual workspace data (workflow names, field values)
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
- NEVER output raw URLs like https://app.hailer.com/...
|
|
1009
|
-
- When referencing activities, discussions, or users, use hailerTag format: [hailerTag|Display Name](objectId)
|
|
1010
|
-
Examples: [hailerTag|AC Milan](691ffe654217e9e8434e578a) or [hailerTag|John Smith](691ffe654217e9e8434e5123)
|
|
1011
|
-
You always have the name and ID from tool responses - use them directly in this format.
|
|
1012
|
-
</rules>
|
|
1080
|
+
- Always reference actual workspace data (workflow names, field values) — never speak in generic terms.
|
|
1081
|
+
</behavior>`;
|
|
1082
|
+
return `You are a workspace assistant for "${wsName}" on Hailer.
|
|
1013
1083
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1084
|
+
${identity}
|
|
1085
|
+
|
|
1086
|
+
${platformRules}
|
|
1087
|
+
|
|
1088
|
+
${defaultBehavior}
|
|
1089
|
+
|
|
1090
|
+
${wsContext}`;
|
|
1017
1091
|
}
|
|
1018
1092
|
// ===== TOOLS =====
|
|
1019
|
-
getAnthropicTools() {
|
|
1093
|
+
getAnthropicTools(classification) {
|
|
1094
|
+
const allowedTools = classification === 'SIMPLE' ? SIMPLE_TOOLS : BOT_TOOLS;
|
|
1020
1095
|
const defs = this.toolExecutor.getToolDefinitions({
|
|
1021
1096
|
allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.BOT_INTERNAL],
|
|
1022
1097
|
});
|
|
1023
1098
|
return defs
|
|
1024
|
-
.filter(d =>
|
|
1099
|
+
.filter(d => allowedTools.has(d.name))
|
|
1025
1100
|
.map(d => ({
|
|
1026
1101
|
name: d.name,
|
|
1027
1102
|
description: d.description,
|
|
@@ -204,7 +204,10 @@ class HailerClientManager {
|
|
|
204
204
|
if (!this.signalHandlers.has(eventType)) {
|
|
205
205
|
this.signalHandlers.set(eventType, []);
|
|
206
206
|
}
|
|
207
|
-
this.signalHandlers.get(eventType)
|
|
207
|
+
const handlers = this.signalHandlers.get(eventType);
|
|
208
|
+
if (!handlers.includes(handler)) {
|
|
209
|
+
handlers.push(handler);
|
|
210
|
+
}
|
|
208
211
|
}
|
|
209
212
|
// Remove a signal handler
|
|
210
213
|
offSignal(eventType, handler) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SignalHandler = void 0;
|
|
4
4
|
const logger_1 = require("../lib/logger");
|
|
5
|
+
const webhooks_1 = require("../bot-config/webhooks");
|
|
5
6
|
// Optional: bot-config only available in bot server mode, not MCP terminal
|
|
6
7
|
let reloadConfigFromHailer = null;
|
|
7
8
|
let setActiveWorkspace = null;
|
|
@@ -169,18 +170,15 @@ class SignalHandler {
|
|
|
169
170
|
prevPhase: data.prevPhase || data.meta?.prevPhase
|
|
170
171
|
});
|
|
171
172
|
// Handle Agent Directory phase changes for bot enable/disable
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// logger.error('Failed to handle activity phase change', err);
|
|
182
|
-
// });
|
|
183
|
-
// }
|
|
173
|
+
// Socket signals as primary path, webhook as backup
|
|
174
|
+
if (processId && phase && activityIdRaw) {
|
|
175
|
+
const activityIds = Array.isArray(activityIdRaw)
|
|
176
|
+
? activityIdRaw
|
|
177
|
+
: [activityIdRaw];
|
|
178
|
+
(0, webhooks_1.handleActivityPhaseChange)(processId, activityIds, phase).catch(err => {
|
|
179
|
+
logger.error('Failed to handle activity phase change', err);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
184
182
|
}
|
|
185
183
|
handleActivityCreated(signal) {
|
|
186
184
|
const data = signal.data;
|
|
@@ -49,6 +49,7 @@ interface BotEntry {
|
|
|
49
49
|
botType: string;
|
|
50
50
|
enabled: boolean;
|
|
51
51
|
displayName?: string;
|
|
52
|
+
systemPrompt?: string;
|
|
52
53
|
}
|
|
53
54
|
interface WorkspaceConfig {
|
|
54
55
|
workspaceId: string;
|
|
@@ -59,6 +60,7 @@ interface WorkspaceConfig {
|
|
|
59
60
|
email: string;
|
|
60
61
|
password: string;
|
|
61
62
|
displayName?: string;
|
|
63
|
+
systemPrompt?: string;
|
|
62
64
|
};
|
|
63
65
|
specialists: BotEntry[];
|
|
64
66
|
lastSynced: string;
|
|
@@ -221,6 +221,7 @@ function handleBotConfigWebhook(payload) {
|
|
|
221
221
|
const botType = getFieldValue(payload.fields, 'botType');
|
|
222
222
|
const userId = getFieldValue(payload.fields, 'hailerProfile');
|
|
223
223
|
const schemaConfigStr = getFieldValue(payload.fields, 'schemaConfig');
|
|
224
|
+
const systemPrompt = getFieldValue(payload.fields, 'systemPrompt') || undefined;
|
|
224
225
|
// Validate required fields
|
|
225
226
|
if (!email || !password) {
|
|
226
227
|
logger.warn('Webhook missing credentials', {
|
|
@@ -262,6 +263,7 @@ function handleBotConfigWebhook(payload) {
|
|
|
262
263
|
botType: botType || 'unknown',
|
|
263
264
|
enabled,
|
|
264
265
|
displayName: payload.name, // Activity name from Agent Directory
|
|
266
|
+
systemPrompt,
|
|
265
267
|
};
|
|
266
268
|
let action;
|
|
267
269
|
// Handle orchestrator
|
|
@@ -273,6 +275,7 @@ function handleBotConfigWebhook(payload) {
|
|
|
273
275
|
email,
|
|
274
276
|
password,
|
|
275
277
|
displayName: payload.name,
|
|
278
|
+
systemPrompt,
|
|
276
279
|
};
|
|
277
280
|
action = 'update';
|
|
278
281
|
logger.info('Updated orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email), displayName: payload.name });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Bot Config Field Wiring Patterns
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-04
|
|
4
|
+
**Context:** Adding systemPrompt field to Agent Directory pipeline
|
|
5
|
+
|
|
6
|
+
## Key Gotchas
|
|
7
|
+
|
|
8
|
+
### seed-config.ts is an independent copy
|
|
9
|
+
`src/commands/seed-config.ts` has its own field extraction logic completely separate from `src/bot-config/context.ts`. When adding a new Agent Directory field, you MUST update BOTH files — they don't share code.
|
|
10
|
+
|
|
11
|
+
### Two separate bot-config readers
|
|
12
|
+
- `src/bot-config/loader.ts` — BotContext system, used by webhook/signal path
|
|
13
|
+
- `src/bot/bot-config.ts` — simple flat reader, used by BotManager on startup
|
|
14
|
+
|
|
15
|
+
Both read from `.bot-config/*.json` but in different ways. Both need updating when adding new fields.
|
|
16
|
+
|
|
17
|
+
### webhook-handler.ts has its own local types
|
|
18
|
+
`BotEntry` and `WorkspaceConfig` are defined locally inside `webhook-handler.ts`, not imported from anywhere. Must update them independently when adding fields.
|
|
19
|
+
|
|
20
|
+
### Webhook fires on phase changes only (with delay)
|
|
21
|
+
Field edits (e.g. systemPrompt) trigger the webhook but with a delay — Hailer batches/debounces. Phase changes are more immediate. For truly instant field-change detection, subscribe to the `activities.updated` socket signal inside the Bot (requires knowing the bot's own `activityId`).
|
|
22
|
+
|
|
23
|
+
### Hot-swap system prompts without restart
|
|
24
|
+
Pattern for live-updatable config in Bot: store as a separate instance field (`_systemPrompt`) distinct from `this.config`. Expose `updateSystemPrompt(prompt)` public method. BotManager can call it without restarting the bot — only restart when credentials actually change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hailer/mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"config": {
|
|
5
5
|
"docker": {
|
|
6
6
|
"registry": "registry.gitlab.com/hailer-repos/hailer-mcp"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"release:patch": "npm version patch -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
26
26
|
"release:minor": "npm version minor -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
27
27
|
"release:major": "npm version major -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
28
|
-
"seed-config": "tsx src/commands/seed-config.ts"
|
|
28
|
+
"seed-config": "tsx src/commands/seed-config.ts",
|
|
29
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@anthropic-ai/sdk": "^0.54.0",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall: Copies .claude/ (agents, skills, hooks) to the project root.
|
|
4
|
+
* Runs automatically after `npm install @hailer/mcp`.
|
|
5
|
+
* Skips when installing in the hailer-mcp repo itself (development).
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Find the project root by walking up from node_modules/@hailer/mcp/
|
|
11
|
+
function findProjectRoot() {
|
|
12
|
+
let dir = __dirname;
|
|
13
|
+
// Walk up until we find a package.json that isn't ours
|
|
14
|
+
for (let i = 0; i < 10; i++) {
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
17
|
+
if (fs.existsSync(pkgPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
20
|
+
// Skip if this is our own package.json
|
|
21
|
+
if (pkg.name === '@hailer/mcp') continue;
|
|
22
|
+
return dir;
|
|
23
|
+
} catch { continue; }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function copyDir(src, dest) {
|
|
30
|
+
if (!fs.existsSync(src)) return;
|
|
31
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
32
|
+
|
|
33
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
34
|
+
const srcPath = path.join(src, entry.name);
|
|
35
|
+
const destPath = path.join(dest, entry.name);
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
copyDir(srcPath, destPath);
|
|
39
|
+
} else {
|
|
40
|
+
// Don't overwrite user's local settings
|
|
41
|
+
if (entry.name === 'settings.local.json' && fs.existsSync(destPath)) continue;
|
|
42
|
+
fs.copyFileSync(srcPath, destPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const projectRoot = findProjectRoot();
|
|
48
|
+
|
|
49
|
+
// Skip if we can't find a project root or if running in dev (our own repo)
|
|
50
|
+
if (!projectRoot) {
|
|
51
|
+
console.log('@hailer/mcp: skipping agent install (no project root found)');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const src = path.join(__dirname, '..', '.claude');
|
|
56
|
+
const dest = path.join(projectRoot, '.claude');
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(src)) {
|
|
59
|
+
console.log('@hailer/mcp: no .claude/ directory in package, skipping');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
copyDir(src, dest);
|
|
64
|
+
console.log(`@hailer/mcp: agents installed to ${dest}`);
|