Compare commits
3 Commits
c8998c6b0d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 07e91ea858 | |||
| 06ccd3564e | |||
| 2977ab369e |
@@ -86,6 +86,33 @@ interface PluginAPI {
|
||||
* wrap every tool's execute return; doing it at the `registerTool` boundary
|
||||
* keeps each tool body unchanged.
|
||||
*/
|
||||
/**
|
||||
* Install a fail-open globalThis.__padded stub if PaddedCell hasn't loaded
|
||||
* yet (load order isn't guaranteed). PaddedCell's installGlobalApi drains
|
||||
* `_pendingCatalog` and replaces `allowTool` with the real check when it
|
||||
* starts. This means HF tools registered before PaddedCell are visible
|
||||
* to the agent until PaddedCell takes over, after which they fall under
|
||||
* the per-session cache gate (decision #37, openclaw side).
|
||||
*/
|
||||
function ensurePaddedStub(): void {
|
||||
const g = globalThis as unknown as {
|
||||
__padded?: {
|
||||
_pendingCatalog?: Array<{ name: string; description: string }>;
|
||||
registerCatalogEntry?: (n: string, d: string) => void;
|
||||
allowTool?: (n: string, c: unknown) => boolean;
|
||||
};
|
||||
};
|
||||
if (g.__padded) return;
|
||||
const buf: Array<{ name: string; description: string }> = [];
|
||||
g.__padded = {
|
||||
_pendingCatalog: buf,
|
||||
registerCatalogEntry(name: string, description: string): void {
|
||||
buf.push({ name, description });
|
||||
},
|
||||
allowTool: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureMcpContentShape(result: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
||||
if (
|
||||
result && typeof result === 'object' &&
|
||||
@@ -105,14 +132,35 @@ function register(api: PluginAPI): void {
|
||||
warn: (...args: any[]) => console.warn('[HarborForge]', ...args),
|
||||
};
|
||||
|
||||
// Wrap api.registerTool so every tool's execute() return is coerced into
|
||||
// the MCP `{ content: [...] }` shape openclaw expects. See
|
||||
// `ensureMcpContentShape` above.
|
||||
// PaddedCell tools-cache integration (decision #37, openclaw side).
|
||||
// Stub the global API early so the gate is consistent regardless of
|
||||
// plugin load order; PaddedCell will replace stub with the real impl
|
||||
// when it loads. fail-open until then.
|
||||
ensurePaddedStub();
|
||||
const seenForCatalog = new Set<string>();
|
||||
|
||||
// Wrap api.registerTool so every tool:
|
||||
// (a) registers its name+description into PaddedCell's catalog so
|
||||
// dynamic-list-tools / dynamic-search-tools surface it (#37)
|
||||
// (b) returns null when the per-session cache doesn't include the
|
||||
// name → the tool is hidden from the model that turn
|
||||
// (c) has its execute() return coerced into the MCP `{ content: [...] }`
|
||||
// shape openclaw expects (preserved from earlier).
|
||||
const _origRegisterTool = api.registerTool.bind(api);
|
||||
api.registerTool = (factory: (ctx: any) => any) => {
|
||||
_origRegisterTool((ctx: any) => {
|
||||
const def = factory(ctx);
|
||||
if (!def || typeof def.execute !== 'function') return def;
|
||||
const padded = (globalThis as any).__padded as
|
||||
| { allowTool?: (n: string, c: any) => boolean; registerCatalogEntry?: (n: string, d: string) => void }
|
||||
| undefined;
|
||||
if (def.name && padded?.registerCatalogEntry && !seenForCatalog.has(def.name)) {
|
||||
padded.registerCatalogEntry(def.name, def.description ?? '');
|
||||
seenForCatalog.add(def.name);
|
||||
}
|
||||
if (def.name && padded?.allowTool && !padded.allowTool(def.name, ctx)) {
|
||||
return null;
|
||||
}
|
||||
const origExecute = def.execute;
|
||||
return {
|
||||
...def,
|
||||
|
||||
@@ -31,6 +31,7 @@ const OLD_PLUGIN_NAME = 'harborforge-monitor';
|
||||
const PLUGIN_SRC_DIR = join(__dirname, 'plugin');
|
||||
const SKILLS_SRC_DIR = join(__dirname, 'skills');
|
||||
const MONITOR_REPO_URL = 'https://git.hangman-lab.top/zhi/HarborForge.Monitor.git';
|
||||
const CLI_REPO_URL = 'https://git.hangman-lab.top/zhi/HarborForge.Cli.git';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
@@ -43,6 +44,7 @@ const options = {
|
||||
installCli: args.includes('--install-cli'),
|
||||
installMonitor: 'no',
|
||||
monitorBranch: 'main',
|
||||
cliBranch: 'main',
|
||||
};
|
||||
|
||||
const profileIdx = args.indexOf('--openclaw-profile-path');
|
||||
@@ -60,6 +62,11 @@ if (monitorBranchIdx !== -1 && args[monitorBranchIdx + 1]) {
|
||||
options.monitorBranch = String(args[monitorBranchIdx + 1]);
|
||||
}
|
||||
|
||||
const cliBranchIdx = args.indexOf('--cli-branch');
|
||||
if (cliBranchIdx !== -1 && args[cliBranchIdx + 1]) {
|
||||
options.cliBranch = String(args[cliBranchIdx + 1]);
|
||||
}
|
||||
|
||||
function resolveOpenclawPath() {
|
||||
if (options.openclawProfilePath) return options.openclawProfilePath;
|
||||
if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH);
|
||||
@@ -321,34 +328,35 @@ async function installCli() {
|
||||
const binDir = join(openclawPath, 'bin');
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Find CLI source — look for HarborForge.Cli relative to project root
|
||||
const projectRoot = resolve(__dirname, '..');
|
||||
const cliDir = join(projectRoot, 'HarborForge.Cli');
|
||||
|
||||
if (!existsSync(cliDir)) {
|
||||
// Try parent directory (monorepo layout)
|
||||
const monoCliDir = resolve(projectRoot, '..', 'HarborForge.Cli');
|
||||
if (!existsSync(monoCliDir)) {
|
||||
logErr(`Cannot find HarborForge.Cli at ${cliDir} or ${monoCliDir}`);
|
||||
logWarn('Skipping CLI installation');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveCliDir = existsSync(cliDir)
|
||||
? cliDir
|
||||
: resolve(projectRoot, '..', 'HarborForge.Cli');
|
||||
|
||||
log(` Building hf from ${effectiveCliDir}...`, 'blue');
|
||||
// Clone CLI repo to /tmp, build there, copy artifact out. Mirrors
|
||||
// installManagedMonitor so the install never depends on a checked-out
|
||||
// sibling repo at a fixed path.
|
||||
const tmpDir = join('/tmp', `harborforge-cli-${Date.now()}`);
|
||||
const hfBinary = join(binDir, 'hf');
|
||||
|
||||
try {
|
||||
const hfBinary = join(binDir, 'hf');
|
||||
exec(`go build -o ${hfBinary} ./cmd/hf`, { cwd: effectiveCliDir, silent: !options.verbose });
|
||||
log(` Cloning ${CLI_REPO_URL} (branch ${options.cliBranch}) → ${tmpDir}...`, 'blue');
|
||||
exec(`git clone --branch ${shellEscape(options.cliBranch)} --depth 1 ${shellEscape(CLI_REPO_URL)} ${shellEscape(tmpDir)}`, { silent: !options.verbose });
|
||||
|
||||
// Stamp the binary with the version string the prod CLI surfaces in
|
||||
// `hf version`. Fall back to a date-only label if rev-parse fails for
|
||||
// any reason (shallow clone shouldn't, but be defensive).
|
||||
let versionLabel = `${new Date().toISOString().slice(0, 10)}+install`;
|
||||
try {
|
||||
const sha = exec(`git rev-parse --short HEAD`, { cwd: tmpDir, silent: true }).trim();
|
||||
if (sha) versionLabel = `${new Date().toISOString().slice(0, 10)}+${options.cliBranch}-${sha}`;
|
||||
} catch { /* keep fallback */ }
|
||||
|
||||
log(` Building hf (version=${versionLabel})...`, 'blue');
|
||||
const ldflags = `-X git.hangman-lab.top/zhi/HarborForge.Cli/internal/commands.Version=${versionLabel}`;
|
||||
exec(`go build -ldflags ${shellEscape(ldflags)} -o ${shellEscape(hfBinary)} ./cmd/hf`, { cwd: tmpDir, silent: !options.verbose });
|
||||
chmodSync(hfBinary, 0o755);
|
||||
logOk(`hf binary → ${hfBinary}`);
|
||||
logOk(`hf binary → ${hfBinary} (branch hint: ${options.cliBranch})`);
|
||||
} catch (err) {
|
||||
logErr(`Failed to build hf CLI: ${err.message}`);
|
||||
logWarn('CLI installation failed, plugin still installed');
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user