@happycastle/oh-my-openclaw 0.13.3 → 0.14.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/dist/cli/setup.d.ts +0 -5
- package/dist/cli/setup.js +5 -31
- package/dist/commands/persona-commands.js +11 -6
- package/dist/hooks/session-sync.d.ts +3 -0
- package/dist/hooks/session-sync.js +33 -0
- package/dist/hooks/spawn-guard.d.ts +3 -0
- package/dist/hooks/spawn-guard.js +33 -0
- package/dist/index.js +12 -6
- package/dist/types.d.ts +1 -0
- package/dist/utils/persona-state.d.ts +3 -0
- package/dist/utils/persona-state.js +64 -2
- package/package.json +1 -1
- package/dist/cli/mcporter-setup.d.ts +0 -41
- package/dist/cli/mcporter-setup.js +0 -102
- package/dist/hooks/persona-injector.d.ts +0 -2
- package/dist/hooks/persona-injector.js +0 -56
package/dist/cli/setup.d.ts
CHANGED
|
@@ -27,8 +27,6 @@ export interface MergeResult {
|
|
|
27
27
|
added: string[];
|
|
28
28
|
skipped: string[];
|
|
29
29
|
updated: string[];
|
|
30
|
-
mcporterAdded?: string[];
|
|
31
|
-
mcporterSkipped?: string[];
|
|
32
30
|
}
|
|
33
31
|
export declare function mergeAgentConfigs(existing: Array<{
|
|
34
32
|
id: string;
|
|
@@ -43,7 +41,6 @@ export declare function mergeAgentConfigs(existing: Array<{
|
|
|
43
41
|
export declare function applyProviderToConfigs(configs: OmocAgentConfig[], provider: string): OmocAgentConfig[];
|
|
44
42
|
export declare function runInteractiveSetup(logger: Logger): Promise<{
|
|
45
43
|
provider: string;
|
|
46
|
-
setupMcporter: boolean;
|
|
47
44
|
}>;
|
|
48
45
|
export interface SetupOptions {
|
|
49
46
|
configPath?: string;
|
|
@@ -51,8 +48,6 @@ export interface SetupOptions {
|
|
|
51
48
|
force?: boolean;
|
|
52
49
|
dryRun?: boolean;
|
|
53
50
|
provider?: string;
|
|
54
|
-
setupMcporter?: boolean;
|
|
55
|
-
mcporterConfigPath?: string;
|
|
56
51
|
interactive?: boolean;
|
|
57
52
|
logger: Logger;
|
|
58
53
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -4,7 +4,6 @@ import * as readline from 'node:readline';
|
|
|
4
4
|
import JSON5 from 'json5';
|
|
5
5
|
import { OMOC_AGENT_CONFIGS } from '../agents/agent-configs.js';
|
|
6
6
|
import { PROVIDER_PRESETS, PROVIDER_LABELS, AGENT_TIER_MAP, MODEL_TIERS, applyProviderPreset, getProviderNames, buildCustomPreset, registerCustomPreset, } from './model-presets.js';
|
|
7
|
-
import { OMOC_MCP_SERVERS, runMcporterSetup } from './mcporter-setup.js';
|
|
8
7
|
const CONFIG_FILENAMES = [
|
|
9
8
|
'openclaw.json5',
|
|
10
9
|
'openclaw.json',
|
|
@@ -149,7 +148,8 @@ function printPreview(logger, provider) {
|
|
|
149
148
|
}
|
|
150
149
|
async function runCustomProviderFlow(rl, logger) {
|
|
151
150
|
logger.info('');
|
|
152
|
-
logger.info('
|
|
151
|
+
logger.info(' Enter model IDs for each tier.');
|
|
152
|
+
logger.info(' Format: provider/model (e.g., cliproxy/claude-opus-4-6, z.ai/gpt-5.3-codex)');
|
|
153
153
|
logger.info('');
|
|
154
154
|
const tierModels = {};
|
|
155
155
|
for (const tier of MODEL_TIERS) {
|
|
@@ -211,29 +211,17 @@ export async function runInteractiveSetup(logger) {
|
|
|
211
211
|
logger.info('');
|
|
212
212
|
logger.info(` ✓ Selected: ${PROVIDER_LABELS[provider] ?? 'Custom'}`);
|
|
213
213
|
logger.info('');
|
|
214
|
-
logger.info('Step 2/
|
|
214
|
+
logger.info('Step 2/2: Model configuration preview');
|
|
215
215
|
logger.info('');
|
|
216
216
|
printPreview(logger, provider);
|
|
217
217
|
logger.info('');
|
|
218
218
|
const confirm = await askQuestion(rl, ' Apply this configuration? (Y/n): ');
|
|
219
219
|
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
|
|
220
220
|
logger.info(' Setup cancelled.');
|
|
221
|
-
return { provider: ''
|
|
221
|
+
return { provider: '' };
|
|
222
222
|
}
|
|
223
223
|
logger.info('');
|
|
224
|
-
|
|
225
|
-
logger.info('');
|
|
226
|
-
logger.info(' OmOC agents use mcporter MCP servers for web search,');
|
|
227
|
-
logger.info(' documentation lookup, and code search:');
|
|
228
|
-
logger.info('');
|
|
229
|
-
for (const [name, entry] of Object.entries(OMOC_MCP_SERVERS)) {
|
|
230
|
-
logger.info(` ${name}: ${entry.description}`);
|
|
231
|
-
}
|
|
232
|
-
logger.info('');
|
|
233
|
-
const mcpConfirm = await askQuestion(rl, ' Set up these MCP servers? (Y/n): ');
|
|
234
|
-
const setupMcporter = mcpConfirm.toLowerCase() !== 'n' && mcpConfirm.toLowerCase() !== 'no';
|
|
235
|
-
logger.info('');
|
|
236
|
-
return { provider, setupMcporter };
|
|
224
|
+
return { provider };
|
|
237
225
|
}
|
|
238
226
|
finally {
|
|
239
227
|
rl.close();
|
|
@@ -293,17 +281,6 @@ export function runSetup(options) {
|
|
|
293
281
|
if (result.added.length === 0 && result.updated.length === 0) {
|
|
294
282
|
logger.info('No changes needed — all OmOC agents already present.');
|
|
295
283
|
}
|
|
296
|
-
if (options.setupMcporter) {
|
|
297
|
-
logger.info('');
|
|
298
|
-
logger.info('Setting up mcporter MCP servers...');
|
|
299
|
-
const mcpResult = runMcporterSetup({
|
|
300
|
-
configPath: options.mcporterConfigPath,
|
|
301
|
-
dryRun,
|
|
302
|
-
logger,
|
|
303
|
-
});
|
|
304
|
-
result.mcporterAdded = mcpResult.added;
|
|
305
|
-
result.mcporterSkipped = mcpResult.skipped;
|
|
306
|
-
}
|
|
307
284
|
return result;
|
|
308
285
|
}
|
|
309
286
|
export function registerSetupCli(ctx) {
|
|
@@ -322,13 +299,11 @@ export function registerSetupCli(ctx) {
|
|
|
322
299
|
const valid = getProviderNames().join(', ');
|
|
323
300
|
throw new Error(`Unknown provider "${provider}". Valid: ${valid}`);
|
|
324
301
|
}
|
|
325
|
-
let setupMcporter = false;
|
|
326
302
|
if (!provider && process.stdin.isTTY) {
|
|
327
303
|
const result = await runInteractiveSetup(ctx.logger);
|
|
328
304
|
if (!result.provider)
|
|
329
305
|
return;
|
|
330
306
|
provider = result.provider;
|
|
331
|
-
setupMcporter = result.setupMcporter;
|
|
332
307
|
}
|
|
333
308
|
runSetup({
|
|
334
309
|
configPath: opts.config,
|
|
@@ -336,7 +311,6 @@ export function registerSetupCli(ctx) {
|
|
|
336
311
|
force: provider ? true : opts.force,
|
|
337
312
|
dryRun: opts.dryRun,
|
|
338
313
|
provider,
|
|
339
|
-
setupMcporter,
|
|
340
314
|
logger: ctx.logger,
|
|
341
315
|
});
|
|
342
316
|
ctx.logger.info('');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
-
import { getActivePersona, setActivePersonaId, resetPersonaState } from '../utils/persona-state.js';
|
|
3
|
-
import { resolvePersonaId, listPersonas, DEFAULT_PERSONA_ID } from '../agents/persona-prompts.js';
|
|
2
|
+
import { getActivePersona, setActivePersonaId, resetPersonaState, replaceAgentsMd, restoreAgentsMdToDefault } from '../utils/persona-state.js';
|
|
3
|
+
import { resolvePersonaId, listPersonas, readPersonaPrompt, DEFAULT_PERSONA_ID } from '../agents/persona-prompts.js';
|
|
4
4
|
function getDisplayName(personaId) {
|
|
5
5
|
const persona = listPersonas().find((p) => p.id === personaId);
|
|
6
6
|
return persona ? `${persona.emoji} ${persona.displayName}` : personaId;
|
|
@@ -17,22 +17,25 @@ export function registerPersonaCommands(api) {
|
|
|
17
17
|
if (!args) {
|
|
18
18
|
const previousId = await getActivePersona();
|
|
19
19
|
await setActivePersonaId(DEFAULT_PERSONA_ID);
|
|
20
|
+
const content = await readPersonaPrompt(DEFAULT_PERSONA_ID);
|
|
21
|
+
await replaceAgentsMd(content);
|
|
20
22
|
const name = getDisplayName(DEFAULT_PERSONA_ID);
|
|
21
23
|
const switchNote = previousId && previousId !== DEFAULT_PERSONA_ID
|
|
22
24
|
? `\n\nSwitched from **${getDisplayName(previousId)}**.`
|
|
23
25
|
: '';
|
|
24
26
|
return {
|
|
25
|
-
text: `# OmOC Mode: ON\n\nActive persona: **${name}**${switchNote}\n\
|
|
27
|
+
text: `# OmOC Mode: ON\n\nActive persona: **${name}**${switchNote}\n\nAGENTS.md replaced with persona prompt. Your next message will use this persona.\n\nUse \`/omoc list\` to see available personas, or \`/omoc <name>\` to switch.`,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
if (args === 'off') {
|
|
29
31
|
const wasActive = await getActivePersona();
|
|
30
32
|
const wasName = wasActive ? getDisplayName(wasActive) : null;
|
|
31
33
|
await resetPersonaState();
|
|
34
|
+
await restoreAgentsMdToDefault();
|
|
32
35
|
return {
|
|
33
36
|
text: wasName
|
|
34
|
-
? `# OmOC Mode: OFF\n\nPersona **${wasName}** deactivated.
|
|
35
|
-
: '# OmOC Mode: OFF\n\nNo persona was active.',
|
|
37
|
+
? `# OmOC Mode: OFF\n\nPersona **${wasName}** deactivated. AGENTS.md restored to default.`
|
|
38
|
+
: '# OmOC Mode: OFF\n\nNo persona was active. AGENTS.md restored to default.',
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
41
|
if (args === 'list') {
|
|
@@ -66,13 +69,15 @@ export function registerPersonaCommands(api) {
|
|
|
66
69
|
}
|
|
67
70
|
const previousId = await getActivePersona();
|
|
68
71
|
await setActivePersonaId(resolvedId);
|
|
72
|
+
const content = await readPersonaPrompt(resolvedId);
|
|
73
|
+
await replaceAgentsMd(content);
|
|
69
74
|
const displayName = getDisplayName(resolvedId);
|
|
70
75
|
const switched = listPersonas().find((p) => p.id === resolvedId);
|
|
71
76
|
const switchNote = previousId && previousId !== resolvedId
|
|
72
77
|
? `\n\nSwitched from **${getDisplayName(previousId)}**.`
|
|
73
78
|
: '';
|
|
74
79
|
return {
|
|
75
|
-
text: `# Persona Switched\n\nActive persona: **${displayName}**${switchNote}\n\
|
|
80
|
+
text: `# Persona Switched\n\nActive persona: **${displayName}**${switchNote}\n\nAGENTS.md replaced. Your next message will use the ${switched?.theme ?? 'persona'} prompt.`,
|
|
76
81
|
};
|
|
77
82
|
},
|
|
78
83
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
+
import { getActivePersona, resolveAgentsMdPath, replaceAgentsMd } from '../utils/persona-state.js';
|
|
3
|
+
import { readPersonaPromptSync } from '../agents/persona-prompts.js';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
/** session_start hook: re-sync AGENTS.md from `.omoc-state` (source of truth). */
|
|
6
|
+
export function registerSessionSync(api) {
|
|
7
|
+
api.on('session_start', async (_event, _ctx) => {
|
|
8
|
+
try {
|
|
9
|
+
const activePersona = await getActivePersona();
|
|
10
|
+
if (!activePersona)
|
|
11
|
+
return;
|
|
12
|
+
const personaContent = readPersonaPromptSync(activePersona);
|
|
13
|
+
if (personaContent.startsWith('[OmOC]')) {
|
|
14
|
+
api.logger.warn(`${LOG_PREFIX} Session sync: persona file issue for ${activePersona}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const agentsPath = resolveAgentsMdPath();
|
|
18
|
+
try {
|
|
19
|
+
const current = readFileSync(agentsPath, 'utf-8');
|
|
20
|
+
if (current.includes(personaContent.slice(0, 100)))
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// AGENTS.md missing or unreadable — needs sync
|
|
25
|
+
}
|
|
26
|
+
await replaceAgentsMd(personaContent);
|
|
27
|
+
api.logger.info(`${LOG_PREFIX} Session sync: AGENTS.md re-synced with .omoc-state (persona=${activePersona})`);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
api.logger.error(`${LOG_PREFIX} Session sync failed:`, err);
|
|
31
|
+
}
|
|
32
|
+
}, { priority: 200 });
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
+
import { getActivePersona } from '../utils/persona-state.js';
|
|
3
|
+
import { ALL_AGENT_IDS } from '../agents/agent-ids.js';
|
|
4
|
+
const SPAWN_TOOL_NAME = 'sessions_spawn';
|
|
5
|
+
const AVAILABLE_AGENTS = ALL_AGENT_IDS.map((id) => id.replace('omoc_', '')).join(', ');
|
|
6
|
+
/** before_tool_call hook: block sessions_spawn without agentId when OmOC persona is active. */
|
|
7
|
+
export function registerSpawnGuard(api) {
|
|
8
|
+
api.on('before_tool_call', async (event, _ctx) => {
|
|
9
|
+
if (event.toolName !== SPAWN_TOOL_NAME) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
let activePersona;
|
|
13
|
+
try {
|
|
14
|
+
activePersona = await getActivePersona();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!activePersona)
|
|
20
|
+
return;
|
|
21
|
+
const agentId = event.params.agentId;
|
|
22
|
+
if (typeof agentId === 'string' && agentId.trim().length > 0)
|
|
23
|
+
return;
|
|
24
|
+
api.logger.info(`${LOG_PREFIX} Spawn guard: blocked sessions_spawn without agentId (active persona=${activePersona})`);
|
|
25
|
+
return {
|
|
26
|
+
block: true,
|
|
27
|
+
blockReason: `[OmOC] Sub-agent spawn BLOCKED: agentId is required when OmOC persona is active (current: ${activePersona}). ` +
|
|
28
|
+
`You MUST provide the agentId parameter in your sessions_spawn call. ` +
|
|
29
|
+
`Available agents: ${AVAILABLE_AGENTS}. ` +
|
|
30
|
+
`Retry with agentId set to the appropriate agent for this task.`,
|
|
31
|
+
};
|
|
32
|
+
}, { priority: 150 });
|
|
33
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,9 @@ import { registerWorkflowCommands } from './commands/workflow-commands.js';
|
|
|
14
14
|
import { registerRalphCommands } from './commands/ralph-commands.js';
|
|
15
15
|
import { registerStatusCommands } from './commands/status-commands.js';
|
|
16
16
|
import { registerPersonaCommands } from './commands/persona-commands.js';
|
|
17
|
-
import { registerPersonaInjector } from './hooks/persona-injector.js';
|
|
18
17
|
import { registerContextInjector } from './hooks/context-injector.js';
|
|
18
|
+
import { registerSessionSync } from './hooks/session-sync.js';
|
|
19
|
+
import { registerSpawnGuard } from './hooks/spawn-guard.js';
|
|
19
20
|
import { registerSetupCli } from './cli/setup.js';
|
|
20
21
|
/**
|
|
21
22
|
* Generation counter for multi-registration handling.
|
|
@@ -77,16 +78,21 @@ export default function register(api) {
|
|
|
77
78
|
registry.hooks.push('gateway-startup');
|
|
78
79
|
api.logger.info(`[${PLUGIN_ID}] Gateway startup hook registered`);
|
|
79
80
|
});
|
|
80
|
-
safeRegister(api, 'persona-injector', 'hook', () => {
|
|
81
|
-
registerPersonaInjector(guarded);
|
|
82
|
-
registry.hooks.push('persona-injector');
|
|
83
|
-
api.logger.info(`[${PLUGIN_ID}] Persona injector hook registered`);
|
|
84
|
-
});
|
|
85
81
|
safeRegister(api, 'context-injector', 'hook', () => {
|
|
86
82
|
registerContextInjector(guarded);
|
|
87
83
|
registry.hooks.push('context-injector');
|
|
88
84
|
api.logger.info(`[${PLUGIN_ID}] Context injector hook registered (before_prompt_build)`);
|
|
89
85
|
});
|
|
86
|
+
safeRegister(api, 'session-sync', 'hook', () => {
|
|
87
|
+
registerSessionSync(api);
|
|
88
|
+
registry.hooks.push('session-sync');
|
|
89
|
+
api.logger.info(`[${PLUGIN_ID}] Session sync hook registered (session_start)`);
|
|
90
|
+
});
|
|
91
|
+
safeRegister(api, 'spawn-guard', 'hook', () => {
|
|
92
|
+
registerSpawnGuard(api);
|
|
93
|
+
registry.hooks.push('spawn-guard');
|
|
94
|
+
api.logger.info(`[${PLUGIN_ID}] Spawn guard hook registered (before_tool_call)`);
|
|
95
|
+
});
|
|
90
96
|
safeRegister(api, 'ralph-loop', 'service', () => {
|
|
91
97
|
registerRalphLoop(api);
|
|
92
98
|
registry.services.push('ralph-loop');
|
package/dist/types.d.ts
CHANGED
|
@@ -4,3 +4,6 @@ export declare function setActivePersonaId(id: string | null): Promise<void>;
|
|
|
4
4
|
export declare function setActivePersona(id: string | null): Promise<void>;
|
|
5
5
|
export declare function getActivePersona(): Promise<string | null>;
|
|
6
6
|
export declare function resetPersonaState(): Promise<void>;
|
|
7
|
+
export declare function resolveAgentsMdPath(): string;
|
|
8
|
+
export declare function replaceAgentsMd(personaContent: string): Promise<void>;
|
|
9
|
+
export declare function restoreAgentsMdToDefault(): Promise<void>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
3
4
|
let activePersonaId = null;
|
|
4
5
|
let loaded = false;
|
|
5
|
-
|
|
6
|
+
const stateDir = join('workspace', '.omoc-state');
|
|
7
|
+
let stateFilePath = join(stateDir, 'active-persona');
|
|
6
8
|
export async function initPersonaState(_api) {
|
|
7
9
|
try {
|
|
8
10
|
await mkdir(dirname(stateFilePath), { recursive: true });
|
|
@@ -50,7 +52,67 @@ async function saveToDisk() {
|
|
|
50
52
|
await writeFile(stateFilePath, activePersonaId ?? '', 'utf-8');
|
|
51
53
|
}
|
|
52
54
|
catch (error) {
|
|
53
|
-
// silent fail — in-memory state still works, but log for debugging
|
|
54
55
|
console.warn('[omoc] Failed to persist persona state to disk:', error);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
export function resolveAgentsMdPath() {
|
|
59
|
+
const profile = process.env.OPENCLAW_PROFILE?.trim();
|
|
60
|
+
const wsDir = (profile && profile.toLowerCase() !== 'default')
|
|
61
|
+
? join(homedir(), '.openclaw', `workspace-${profile}`)
|
|
62
|
+
: join(homedir(), '.openclaw', 'workspace');
|
|
63
|
+
return join(wsDir, 'AGENTS.md');
|
|
64
|
+
}
|
|
65
|
+
export async function replaceAgentsMd(personaContent) {
|
|
66
|
+
const agentsPath = resolveAgentsMdPath();
|
|
67
|
+
await mkdir(dirname(agentsPath), { recursive: true });
|
|
68
|
+
const merged = `${DEFAULT_AGENTS_MD}\n---\n\n${personaContent}`;
|
|
69
|
+
await writeFile(agentsPath, merged, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
export async function restoreAgentsMdToDefault() {
|
|
72
|
+
const agentsPath = resolveAgentsMdPath();
|
|
73
|
+
await mkdir(dirname(agentsPath), { recursive: true });
|
|
74
|
+
await writeFile(agentsPath, DEFAULT_AGENTS_MD, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
const DEFAULT_AGENTS_MD = `# AGENTS.md - Your Workspace
|
|
77
|
+
|
|
78
|
+
This folder is home. Treat it that way.
|
|
79
|
+
|
|
80
|
+
## First Run
|
|
81
|
+
|
|
82
|
+
If \`BOOTSTRAP.md\` exists, that's your birth certificate. Follow it, figure out who you are, then delete it. You won't need it again.
|
|
83
|
+
|
|
84
|
+
## Every Session
|
|
85
|
+
|
|
86
|
+
Before doing anything else:
|
|
87
|
+
|
|
88
|
+
1. Read \`SOUL.md\` — this is who you are
|
|
89
|
+
2. Read \`USER.md\` — this is who you're helping
|
|
90
|
+
3. Read \`memory/YYYY-MM-DD.md\` (today + yesterday) for recent context
|
|
91
|
+
4. **If in MAIN SESSION** (direct chat with your human): Also read \`MEMORY.md\`
|
|
92
|
+
|
|
93
|
+
Don't ask permission. Just do it.
|
|
94
|
+
|
|
95
|
+
## Memory
|
|
96
|
+
|
|
97
|
+
You wake up fresh each session. These files are your continuity:
|
|
98
|
+
|
|
99
|
+
- **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed) — raw logs of what happened
|
|
100
|
+
- **Long-term:** \`MEMORY.md\` — your curated memories, like a human's long-term memory
|
|
101
|
+
|
|
102
|
+
Capture what matters. Decisions, context, things to remember. Skip the secrets unless asked to keep them.
|
|
103
|
+
|
|
104
|
+
## Safety
|
|
105
|
+
|
|
106
|
+
- Don't exfiltrate private data. Ever.
|
|
107
|
+
- Don't run destructive commands without asking.
|
|
108
|
+
- \`trash\` > \`rm\` (recoverable beats gone forever)
|
|
109
|
+
- When in doubt, ask.
|
|
110
|
+
|
|
111
|
+
## Tools
|
|
112
|
+
|
|
113
|
+
Skills provide your tools. When you need one, check its \`SKILL.md\`. Keep local notes in \`TOOLS.md\`.
|
|
114
|
+
|
|
115
|
+
## Make It Yours
|
|
116
|
+
|
|
117
|
+
This is a starting point. Add your own conventions, style, and rules as you figure out what works.
|
|
118
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
type McpServerEntry = {
|
|
2
|
-
url: string;
|
|
3
|
-
description: string;
|
|
4
|
-
};
|
|
5
|
-
export declare const OMOC_MCP_SERVERS: Record<string, McpServerEntry>;
|
|
6
|
-
type McporterConfig = {
|
|
7
|
-
mcpServers: Record<string, {
|
|
8
|
-
url?: string;
|
|
9
|
-
baseUrl?: string;
|
|
10
|
-
type?: string;
|
|
11
|
-
[key: string]: unknown;
|
|
12
|
-
}>;
|
|
13
|
-
[key: string]: unknown;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Resolve mcporter config path.
|
|
17
|
-
* Priority: ~/.openclaw/workspace/config/mcporter.json > ~/.config/mcporter/mcporter.json
|
|
18
|
-
*/
|
|
19
|
-
export declare function resolveMcporterConfigPath(): string;
|
|
20
|
-
export declare function readMcporterConfig(configPath: string): McporterConfig;
|
|
21
|
-
export declare function writeMcporterConfig(configPath: string, config: McporterConfig): void;
|
|
22
|
-
export interface McporterMergeResult {
|
|
23
|
-
added: string[];
|
|
24
|
-
skipped: string[];
|
|
25
|
-
}
|
|
26
|
-
export declare function mergeMcpServers(existing: McporterConfig, servers: Record<string, McpServerEntry>): {
|
|
27
|
-
config: McporterConfig;
|
|
28
|
-
result: McporterMergeResult;
|
|
29
|
-
};
|
|
30
|
-
type Logger = {
|
|
31
|
-
info: (msg: string) => void;
|
|
32
|
-
warn: (msg: string) => void;
|
|
33
|
-
error: (msg: string) => void;
|
|
34
|
-
};
|
|
35
|
-
export interface McporterSetupOptions {
|
|
36
|
-
configPath?: string;
|
|
37
|
-
dryRun?: boolean;
|
|
38
|
-
logger: Logger;
|
|
39
|
-
}
|
|
40
|
-
export declare function runMcporterSetup(options: McporterSetupOptions): McporterMergeResult;
|
|
41
|
-
export {};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
export const OMOC_MCP_SERVERS = {
|
|
4
|
-
'web-search-prime': {
|
|
5
|
-
url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
|
|
6
|
-
description: 'Keyword-based web search (news, blogs, general)',
|
|
7
|
-
},
|
|
8
|
-
'web-reader': {
|
|
9
|
-
url: 'https://api.z.ai/api/mcp/web_reader/mcp',
|
|
10
|
-
description: 'Clean full-page content extraction',
|
|
11
|
-
},
|
|
12
|
-
exa: {
|
|
13
|
-
url: 'https://mcp.exa.ai/mcp?tools=web_search_exa',
|
|
14
|
-
description: 'Semantic web search (Exa)',
|
|
15
|
-
},
|
|
16
|
-
context7: {
|
|
17
|
-
url: 'https://mcp.context7.com/mcp',
|
|
18
|
-
description: 'Library/framework documentation search',
|
|
19
|
-
},
|
|
20
|
-
grep_app: {
|
|
21
|
-
url: 'https://mcp.grep.app',
|
|
22
|
-
description: 'Open-source code search on GitHub',
|
|
23
|
-
},
|
|
24
|
-
zread: {
|
|
25
|
-
url: 'https://api.z.ai/api/mcp/zread/mcp',
|
|
26
|
-
description: 'Direct GitHub repository exploration',
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Resolve mcporter config path.
|
|
31
|
-
* Priority: ~/.openclaw/workspace/config/mcporter.json > ~/.config/mcporter/mcporter.json
|
|
32
|
-
*/
|
|
33
|
-
export function resolveMcporterConfigPath() {
|
|
34
|
-
const homeDir = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
35
|
-
const openclawPath = path.join(homeDir, '.openclaw', 'workspace', 'config', 'mcporter.json');
|
|
36
|
-
if (fs.existsSync(openclawPath)) {
|
|
37
|
-
return openclawPath;
|
|
38
|
-
}
|
|
39
|
-
const mcporterHomePath = path.join(homeDir, '.config', 'mcporter', 'mcporter.json');
|
|
40
|
-
if (fs.existsSync(mcporterHomePath)) {
|
|
41
|
-
return mcporterHomePath;
|
|
42
|
-
}
|
|
43
|
-
return openclawPath;
|
|
44
|
-
}
|
|
45
|
-
export function readMcporterConfig(configPath) {
|
|
46
|
-
if (!fs.existsSync(configPath)) {
|
|
47
|
-
return { mcpServers: {} };
|
|
48
|
-
}
|
|
49
|
-
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
50
|
-
const parsed = JSON.parse(raw);
|
|
51
|
-
if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
|
|
52
|
-
parsed.mcpServers = {};
|
|
53
|
-
}
|
|
54
|
-
return parsed;
|
|
55
|
-
}
|
|
56
|
-
export function writeMcporterConfig(configPath, config) {
|
|
57
|
-
const dir = path.dirname(configPath);
|
|
58
|
-
if (!fs.existsSync(dir)) {
|
|
59
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
62
|
-
}
|
|
63
|
-
export function mergeMcpServers(existing, servers) {
|
|
64
|
-
const result = { added: [], skipped: [] };
|
|
65
|
-
const merged = { ...existing, mcpServers: { ...existing.mcpServers } };
|
|
66
|
-
for (const [name, entry] of Object.entries(servers)) {
|
|
67
|
-
if (merged.mcpServers[name]) {
|
|
68
|
-
result.skipped.push(name);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
merged.mcpServers[name] = { url: entry.url };
|
|
72
|
-
result.added.push(name);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { config: merged, result };
|
|
76
|
-
}
|
|
77
|
-
export function runMcporterSetup(options) {
|
|
78
|
-
const { logger, dryRun = false } = options;
|
|
79
|
-
const configPath = options.configPath ?? resolveMcporterConfigPath();
|
|
80
|
-
logger.info(`mcporter config: ${configPath}`);
|
|
81
|
-
const existing = readMcporterConfig(configPath);
|
|
82
|
-
const { config: merged, result } = mergeMcpServers(existing, OMOC_MCP_SERVERS);
|
|
83
|
-
if (result.added.length === 0) {
|
|
84
|
-
logger.info('No changes needed — all MCP servers already configured.');
|
|
85
|
-
return result;
|
|
86
|
-
}
|
|
87
|
-
if (dryRun) {
|
|
88
|
-
logger.info(`[dry-run] Would add ${result.added.length} MCP server(s): ${result.added.join(', ')}`);
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
if (fs.existsSync(configPath)) {
|
|
92
|
-
const backupPath = configPath + '.bak';
|
|
93
|
-
fs.copyFileSync(configPath, backupPath);
|
|
94
|
-
logger.info(`Backup created: ${backupPath}`);
|
|
95
|
-
}
|
|
96
|
-
writeMcporterConfig(configPath, merged);
|
|
97
|
-
logger.info(`Added ${result.added.length} MCP server(s): ${result.added.join(', ')}`);
|
|
98
|
-
if (result.skipped.length > 0) {
|
|
99
|
-
logger.info(`Skipped ${result.skipped.length} existing server(s): ${result.skipped.join(', ')}`);
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
-
import { getActivePersona } from '../utils/persona-state.js';
|
|
3
|
-
import { readPersonaPromptSync, resolvePersonaId } from '../agents/persona-prompts.js';
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the effective persona ID.
|
|
6
|
-
*
|
|
7
|
-
* Priority:
|
|
8
|
-
* 1. Manually set persona via /omoc command (getActivePersona())
|
|
9
|
-
* 2. agentId from the hook context (set by OpenClaw core)
|
|
10
|
-
* 3. null — no persona to inject
|
|
11
|
-
*/
|
|
12
|
-
async function resolveEffectivePersona(ctx) {
|
|
13
|
-
const manual = await getActivePersona();
|
|
14
|
-
if (manual) {
|
|
15
|
-
const resolved = resolvePersonaId(manual);
|
|
16
|
-
if (resolved)
|
|
17
|
-
return { personaId: resolved, source: 'manual' };
|
|
18
|
-
}
|
|
19
|
-
const agentId = ctx.agentId;
|
|
20
|
-
if (!agentId)
|
|
21
|
-
return null;
|
|
22
|
-
const resolved = resolvePersonaId(agentId);
|
|
23
|
-
if (!resolved)
|
|
24
|
-
return null;
|
|
25
|
-
return { personaId: resolved, source: 'auto' };
|
|
26
|
-
}
|
|
27
|
-
export function registerPersonaInjector(api) {
|
|
28
|
-
// Use the typed hook system (api.on) for before_prompt_build.
|
|
29
|
-
// This directly injects into the system prompt via prependContext,
|
|
30
|
-
// which is more reliable than bootstrapFiles via agent:bootstrap.
|
|
31
|
-
//
|
|
32
|
-
// api.registerHook('before_prompt_build', ...) registers into the internal
|
|
33
|
-
// hook system which does NOT trigger before_prompt_build — only hookRunner
|
|
34
|
-
// (typed hooks via api.on) does.
|
|
35
|
-
api.on('before_prompt_build', async (_event, ctx) => {
|
|
36
|
-
const result = await resolveEffectivePersona(ctx);
|
|
37
|
-
if (!result) {
|
|
38
|
-
const manual = await getActivePersona();
|
|
39
|
-
api.logger.info(`${LOG_PREFIX} Persona injector: no persona resolved (agentId=${ctx.agentId ?? 'none'}, manual=${manual ?? 'none'})`);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const { personaId, source } = result;
|
|
43
|
-
try {
|
|
44
|
-
const content = readPersonaPromptSync(personaId);
|
|
45
|
-
api.logger.info(`${LOG_PREFIX} Persona injected via before_prompt_build: ${personaId} (${source}, agentId=${ctx.agentId ?? 'none'})`);
|
|
46
|
-
return {
|
|
47
|
-
prependContext: content,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
api.logger.error(`${LOG_PREFIX} Failed to inject persona ${personaId}:`, err);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
}, { priority: 100 } // High priority — persona prompt should be prepended first
|
|
55
|
-
);
|
|
56
|
-
}
|