@clearlysh/beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log redaction — strip likely-sensitive values before they hit stderr,
|
|
3
|
+
* log files, or anywhere else a downstream tool might scrape them.
|
|
4
|
+
*
|
|
5
|
+
* The logger is the last line of defense. Individual call sites try not
|
|
6
|
+
* to log tokens in the first place, but the daemon handles a lot of
|
|
7
|
+
* untrusted input (IPC messages, URLs, SQL, prompts) and it's easy to
|
|
8
|
+
* accidentally interpolate a token into a format string — especially
|
|
9
|
+
* during debugging. Every string the logger writes passes through this
|
|
10
|
+
* function first so a stray secret doesn't land on disk.
|
|
11
|
+
*
|
|
12
|
+
* Patterns covered:
|
|
13
|
+
* - JWT-like tokens (three base64url segments joined by dots)
|
|
14
|
+
* - Firebase/Google access tokens (ya29.xxx, 1//xxx)
|
|
15
|
+
* - OpenAI/Anthropic/Vertex API keys (sk-xxx, ak-xxx, xoxb-xxx, etc.)
|
|
16
|
+
* - Authorization headers (Bearer, Basic, Digest)
|
|
17
|
+
* - key=value/secret=value in JSON, URL query, form bodies
|
|
18
|
+
* - AWS access keys (AKIA…)
|
|
19
|
+
* - Hex blobs of ≥32 chars (IPC auth tokens are 64-char hex)
|
|
20
|
+
* - Email addresses in URL query-param-style contexts (?email=, &user=)
|
|
21
|
+
*
|
|
22
|
+
* The patterns are deliberately over-eager — we'd rather show
|
|
23
|
+
* `[REDACTED]` than leak a credential. False positives in logs are
|
|
24
|
+
* annoying; false negatives are a breach.
|
|
25
|
+
*/
|
|
26
|
+
export function redactLogText(text) {
|
|
27
|
+
// Coerce non-strings via String() — log() callers occasionally pass
|
|
28
|
+
// numbers, errors, or undefined. We return JS-standard string forms
|
|
29
|
+
// (`'null'`, `'undefined'`, `'123'`) rather than swallowing them so a
|
|
30
|
+
// future "where did this log come from" search still finds the entry.
|
|
31
|
+
if (typeof text !== 'string')
|
|
32
|
+
return String(text);
|
|
33
|
+
if (text.length === 0)
|
|
34
|
+
return text;
|
|
35
|
+
return (text
|
|
36
|
+
// JWT-like (header.payload.signature) — base64url segments, dots, min lengths
|
|
37
|
+
.replace(/\beyJ[a-zA-Z0-9_\-]{8,}\.[a-zA-Z0-9_\-]{8,}\.[a-zA-Z0-9_\-]{8,}\b/g, '[REDACTED_JWT]')
|
|
38
|
+
// Google OAuth2 access tokens (ya29.xxx) and refresh tokens (1//xxx)
|
|
39
|
+
.replace(/\bya29\.[a-zA-Z0-9_\-]{20,}\b/g, '[REDACTED_GOOGLE_TOKEN]')
|
|
40
|
+
.replace(/\b1\/\/[a-zA-Z0-9_\-]{40,}\b/g, '[REDACTED_GOOGLE_REFRESH]')
|
|
41
|
+
// Common API key prefixes: sk-ant-, sk-, ak-, xoxb-, xoxp-, ghp_, ghs_, gho_
|
|
42
|
+
// sk-ant must come BEFORE sk in alternation so longer prefixes win.
|
|
43
|
+
.replace(/\b(sk-ant|sk|ak|xoxb|xoxp)-[A-Za-z0-9_\-]{16,}\b/g, '$1-[REDACTED]')
|
|
44
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9]{30,}\b/g, '[REDACTED_GITHUB_TOKEN]')
|
|
45
|
+
// Authorization headers
|
|
46
|
+
.replace(/(Authorization:\s*)(Bearer|Basic|Digest)\s+[A-Za-z0-9._\-/+=]{8,}/gi, (_m, pre, scheme) => `${pre}${scheme} [REDACTED]`)
|
|
47
|
+
.replace(/\b(Bearer|Basic|Digest)\s+[A-Za-z0-9._\-/+=]{8,}\b/g, '$1 [REDACTED]')
|
|
48
|
+
// key=value / key: "value" for common secret names
|
|
49
|
+
.replace(/(\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|secret|password|passwd|pwd|auth|authorization|client[_-]?secret)\b)\s*[:=]\s*['"]?[A-Za-z0-9_\-./+=]{8,}['"]?/gi, '$1=[REDACTED]')
|
|
50
|
+
// Query-string token/secret/key parameters
|
|
51
|
+
.replace(/([?&](?:token|secret|key|apikey|api_key|access_token|id_token)=)[^&\s"']+/gi, '$1[REDACTED]')
|
|
52
|
+
// AWS access key IDs
|
|
53
|
+
.replace(/\b(AKIA[A-Z0-9]{16})\b/g, '[REDACTED_AWS_KEY]')
|
|
54
|
+
// Long hex blobs (IPC auth tokens are 64-char hex; match ≥32 hex chars when they look standalone)
|
|
55
|
+
.replace(/\b([a-f0-9]{32,})\b/gi, (m) => (m.length >= 32 ? '[REDACTED_HEX]' : m))
|
|
56
|
+
// Emails in URL-ish contexts (?email=foo@bar.com, &user=…@…)
|
|
57
|
+
.replace(/([?&](?:email|user|username|uid)=)[^\s&"']+@[^\s&"']+/gi, '$1[REDACTED_EMAIL]'));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Redact an arbitrary JSON-safe value. Recursively walks objects/arrays.
|
|
61
|
+
* Keys whose name matches a sensitive-looking pattern have their value
|
|
62
|
+
* replaced with `[REDACTED]` wholesale, regardless of content.
|
|
63
|
+
*/
|
|
64
|
+
const SENSITIVE_KEY = /token|secret|password|passwd|pwd|apikey|api[_-]?key|auth|authorization|session|cookie|bearer|refresh|access[_-]?token|id[_-]?token|client[_-]?secret/i;
|
|
65
|
+
export function redactLogObject(value, depth = 0) {
|
|
66
|
+
if (depth > 6)
|
|
67
|
+
return '[…too deep…]';
|
|
68
|
+
if (value === null || value === undefined)
|
|
69
|
+
return value;
|
|
70
|
+
if (typeof value === 'string')
|
|
71
|
+
return redactLogText(value);
|
|
72
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
73
|
+
return value;
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return value.map((v) => redactLogObject(v, depth + 1));
|
|
76
|
+
if (typeof value === 'object') {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [k, v] of Object.entries(value)) {
|
|
79
|
+
if (SENSITIVE_KEY.test(k)) {
|
|
80
|
+
out[k] = '[REDACTED]';
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
out[k] = redactLogObject(v, depth + 1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Beehaven Daemon — Structured Logger
|
|
3
|
+
// JSON log lines to ~/.beehaven/logs/daemon.log with rotation.
|
|
4
|
+
//
|
|
5
|
+
// All messages + data are passed through log-redaction before being written
|
|
6
|
+
// so a secret accidentally interpolated into a format string (or captured
|
|
7
|
+
// by a catch-and-rethrow) doesn't land on disk verbatim. Keep the redaction
|
|
8
|
+
// layer as the last line of defense — call sites should still try not to
|
|
9
|
+
// log secrets in the first place.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
import { existsSync, mkdirSync, appendFileSync, statSync, renameSync, chmodSync, writeFileSync, } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { redactLogText, redactLogObject } from './log-redaction.js';
|
|
15
|
+
const LOG_DIR = join(homedir(), '.beehaven', 'logs');
|
|
16
|
+
const LOG_FILE = join(LOG_DIR, 'daemon.log');
|
|
17
|
+
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
18
|
+
const MAX_ROTATIONS = 3;
|
|
19
|
+
let currentLevel = 'info';
|
|
20
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
21
|
+
function ensureLogDir() {
|
|
22
|
+
// Logs can contain IPC payloads that — despite redactSecrets — may still
|
|
23
|
+
// leak request shapes, user emails, etc. Restrict to the owning user.
|
|
24
|
+
if (!existsSync(LOG_DIR)) {
|
|
25
|
+
mkdirSync(LOG_DIR, { recursive: true, mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Existing dir may have been created by an older build without the
|
|
29
|
+
// mode. Re-apply defensively; chmod is a no-op when already correct.
|
|
30
|
+
try {
|
|
31
|
+
chmodSync(LOG_DIR, 0o700);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
// If the log file exists, make sure it's 0600. We can't pass mode to
|
|
36
|
+
// appendFileSync directly without creating the file ourselves first.
|
|
37
|
+
if (!existsSync(LOG_FILE)) {
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(LOG_FILE, '', { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
try {
|
|
45
|
+
chmodSync(LOG_FILE, 0o600);
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function rotate() {
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(LOG_FILE))
|
|
53
|
+
return;
|
|
54
|
+
const size = statSync(LOG_FILE).size;
|
|
55
|
+
if (size < MAX_SIZE)
|
|
56
|
+
return;
|
|
57
|
+
// Rotate: daemon.log.3 → deleted, daemon.log.2 → .3, daemon.log.1 → .2, daemon.log → .1
|
|
58
|
+
for (let i = MAX_ROTATIONS; i >= 1; i--) {
|
|
59
|
+
const from = i === 1 ? LOG_FILE : `${LOG_FILE}.${i - 1}`;
|
|
60
|
+
const to = `${LOG_FILE}.${i}`;
|
|
61
|
+
if (existsSync(from))
|
|
62
|
+
renameSync(from, to);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* never crash on rotation failure */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function setLogLevel(level) {
|
|
70
|
+
currentLevel = level;
|
|
71
|
+
}
|
|
72
|
+
export function log(level, category, message, data) {
|
|
73
|
+
if (LEVELS[level] < LEVELS[currentLevel])
|
|
74
|
+
return;
|
|
75
|
+
// Redact before composing the log entry. Messages get the text-level
|
|
76
|
+
// regex pass; `data` gets the recursive object walk that additionally
|
|
77
|
+
// zeros out any key whose name looks sensitive (token, secret, …).
|
|
78
|
+
const safeMessage = redactLogText(message);
|
|
79
|
+
const safeData = data !== undefined ? redactLogObject(data) : undefined;
|
|
80
|
+
const entry = {
|
|
81
|
+
t: Date.now(),
|
|
82
|
+
l: level,
|
|
83
|
+
c: category,
|
|
84
|
+
m: safeMessage,
|
|
85
|
+
...(safeData !== undefined ? { d: safeData } : {}),
|
|
86
|
+
};
|
|
87
|
+
const line = JSON.stringify(entry) + '\n';
|
|
88
|
+
// Always write to stderr for systemd/launchd capture
|
|
89
|
+
if (level === 'error' || level === 'warn') {
|
|
90
|
+
process.stderr.write(`[${category}] ${safeMessage}\n`);
|
|
91
|
+
}
|
|
92
|
+
// Write to log file
|
|
93
|
+
try {
|
|
94
|
+
ensureLogDir();
|
|
95
|
+
rotate();
|
|
96
|
+
appendFileSync(LOG_FILE, line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* never crash on log failure */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Convenience methods
|
|
103
|
+
export const logger = {
|
|
104
|
+
debug: (cat, msg, data) => log('debug', cat, msg, data),
|
|
105
|
+
info: (cat, msg, data) => log('info', cat, msg, data),
|
|
106
|
+
warn: (cat, msg, data) => log('warn', cat, msg, data),
|
|
107
|
+
error: (cat, msg, data) => log('error', cat, msg, data),
|
|
108
|
+
};
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest — reads and validates beehaven.json from project directories.
|
|
3
|
+
*
|
|
4
|
+
* The manifest is the contract between a local project and the daemon.
|
|
5
|
+
* It declares tools (shell commands, HTTP endpoints), documentation paths,
|
|
6
|
+
* and metadata for marketplace publishing.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
/**
|
|
11
|
+
* Load beehaven.json from a directory. Returns null if not found.
|
|
12
|
+
*/
|
|
13
|
+
export function loadManifest(projectDir) {
|
|
14
|
+
const manifestPath = join(projectDir, 'beehaven.json');
|
|
15
|
+
if (!existsSync(manifestPath))
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
19
|
+
const manifest = JSON.parse(raw);
|
|
20
|
+
// Basic validation
|
|
21
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
22
|
+
console.warn(`[manifest] beehaven.json in ${projectDir} missing required "name" field`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return manifest;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn(`[manifest] Failed to parse ${manifestPath}: ${err.message}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get all tools from a manifest, with project context attached.
|
|
34
|
+
*/
|
|
35
|
+
export function getManifestTools(manifest, projectDir) {
|
|
36
|
+
if (!manifest.tools || manifest.tools.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
return manifest.tools.map((tool) => ({
|
|
39
|
+
...tool,
|
|
40
|
+
projectDir,
|
|
41
|
+
projectName: manifest.name,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a ManifestTool to the tool schema the cloud agent uses.
|
|
46
|
+
*/
|
|
47
|
+
export function toolToSchema(tool, projectName) {
|
|
48
|
+
const properties = {};
|
|
49
|
+
const required = [];
|
|
50
|
+
if (tool.input) {
|
|
51
|
+
for (const [key, param] of Object.entries(tool.input)) {
|
|
52
|
+
properties[key] = {
|
|
53
|
+
type: param.type,
|
|
54
|
+
description: param.description || key,
|
|
55
|
+
};
|
|
56
|
+
if (param.enum)
|
|
57
|
+
properties[key].enum = param.enum;
|
|
58
|
+
if (param.default !== undefined)
|
|
59
|
+
properties[key].default = param.default;
|
|
60
|
+
if (param.required !== false)
|
|
61
|
+
required.push(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name: `${projectName}:${tool.name}`,
|
|
66
|
+
description: `[${projectName}] ${tool.description}`,
|
|
67
|
+
input_schema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties,
|
|
70
|
+
...(required.length > 0 ? { required } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute a shell command tool.
|
|
76
|
+
* Substitutes {{param}} placeholders with input values.
|
|
77
|
+
* Returns stdout as string.
|
|
78
|
+
*/
|
|
79
|
+
export async function executeCommandTool(tool, input) {
|
|
80
|
+
const { execFile } = await import('child_process');
|
|
81
|
+
const { promisify } = await import('util');
|
|
82
|
+
const execFileAsync = promisify(execFile);
|
|
83
|
+
if (!tool.command)
|
|
84
|
+
throw new Error(`Tool ${tool.name} has no command`);
|
|
85
|
+
// Substitute {{param}} placeholders
|
|
86
|
+
let cmd = tool.command;
|
|
87
|
+
for (const [key, value] of Object.entries(input)) {
|
|
88
|
+
cmd = cmd.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
|
|
89
|
+
}
|
|
90
|
+
// Split command into executable and args
|
|
91
|
+
const parts = cmd.split(/\s+/);
|
|
92
|
+
const executable = parts[0];
|
|
93
|
+
const args = parts.slice(1);
|
|
94
|
+
const cwd = tool.cwd ? join(tool.projectDir, tool.cwd) : tool.projectDir;
|
|
95
|
+
const timeout = tool.timeout ?? 30_000;
|
|
96
|
+
try {
|
|
97
|
+
const { stdout, stderr } = await execFileAsync(executable, args, {
|
|
98
|
+
cwd,
|
|
99
|
+
timeout,
|
|
100
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
101
|
+
shell: true,
|
|
102
|
+
});
|
|
103
|
+
if (stderr)
|
|
104
|
+
console.warn(`[tool:${tool.name}] stderr: ${stderr.slice(0, 500)}`);
|
|
105
|
+
return { result: stdout.trim(), exitCode: 0 };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { result: err.stderr || err.message, exitCode: err.code ?? 1 };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Execute an HTTP endpoint tool.
|
|
113
|
+
* Sends input as JSON body (POST) or query params (GET).
|
|
114
|
+
* Returns response body.
|
|
115
|
+
*/
|
|
116
|
+
export async function executeEndpointTool(tool, input, baseEndpoint) {
|
|
117
|
+
const url = tool.endpoint || baseEndpoint;
|
|
118
|
+
if (!url)
|
|
119
|
+
throw new Error(`Tool ${tool.name} has no endpoint`);
|
|
120
|
+
const method = tool.method ?? 'POST';
|
|
121
|
+
const timeout = tool.timeout ?? 30_000;
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
124
|
+
try {
|
|
125
|
+
const opts = {
|
|
126
|
+
method,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
};
|
|
130
|
+
if (method !== 'GET') {
|
|
131
|
+
opts.body = JSON.stringify(input);
|
|
132
|
+
}
|
|
133
|
+
const resp = await fetch(url, opts);
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
136
|
+
const result = contentType.includes('json') ? await resp.json() : await resp.text();
|
|
137
|
+
return { result, status: resp.status };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
throw new Error(`HTTP ${method} ${url} failed: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|