feat: add skills settings and gateway skills management
This commit is contained in:
147
src/agents/skills-install.ts
Normal file
147
src/agents/skills-install.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import {
|
||||
loadWorkspaceSkillEntries,
|
||||
type SkillEntry,
|
||||
type SkillInstallSpec,
|
||||
} from "./skills.js";
|
||||
|
||||
export type SkillInstallRequest = {
|
||||
workspaceDir: string;
|
||||
skillName: string;
|
||||
installId: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type SkillInstallResult = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
};
|
||||
|
||||
function resolveInstallId(spec: SkillInstallSpec, index: number): string {
|
||||
return (spec.id ?? `${spec.kind}-${index}`).trim();
|
||||
}
|
||||
|
||||
function findInstallSpec(
|
||||
entry: SkillEntry,
|
||||
installId: string,
|
||||
): SkillInstallSpec | undefined {
|
||||
const specs = entry.clawdis?.install ?? [];
|
||||
for (const [index, spec] of specs.entries()) {
|
||||
if (resolveInstallId(spec, index) === installId) return spec;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function runShell(command: string, timeoutMs: number) {
|
||||
return runCommandWithTimeout(["/bin/zsh", "-lc", command], { timeoutMs });
|
||||
}
|
||||
|
||||
function buildInstallCommand(spec: SkillInstallSpec): {
|
||||
argv: string[] | null;
|
||||
shell: string | null;
|
||||
cwd?: string;
|
||||
error?: string;
|
||||
} {
|
||||
switch (spec.kind) {
|
||||
case "brew": {
|
||||
if (!spec.formula) return { argv: null, shell: null, error: "missing brew formula" };
|
||||
return { argv: ["brew", "install", spec.formula], shell: null };
|
||||
}
|
||||
case "node": {
|
||||
if (!spec.package) return { argv: null, shell: null, error: "missing node package" };
|
||||
return { argv: ["npm", "install", "-g", spec.package], shell: null };
|
||||
}
|
||||
case "go": {
|
||||
if (!spec.module) return { argv: null, shell: null, error: "missing go module" };
|
||||
return { argv: ["go", "install", spec.module], shell: null };
|
||||
}
|
||||
case "pnpm": {
|
||||
if (!spec.repoPath || !spec.script) {
|
||||
return { argv: null, shell: null, error: "missing pnpm repoPath/script" };
|
||||
}
|
||||
const repoPath = resolveUserPath(spec.repoPath);
|
||||
const cmd = `cd ${JSON.stringify(repoPath)} && pnpm install && pnpm run ${JSON.stringify(spec.script)}`;
|
||||
return { argv: null, shell: cmd };
|
||||
}
|
||||
case "git": {
|
||||
if (!spec.url || !spec.destination) {
|
||||
return { argv: null, shell: null, error: "missing git url/destination" };
|
||||
}
|
||||
const dest = resolveUserPath(spec.destination);
|
||||
const cmd = `if [ -d ${JSON.stringify(dest)} ]; then echo "Already cloned"; else git clone ${JSON.stringify(spec.url)} ${JSON.stringify(dest)}; fi`;
|
||||
return { argv: null, shell: cmd };
|
||||
}
|
||||
case "shell": {
|
||||
if (!spec.command) return { argv: null, shell: null, error: "missing shell command" };
|
||||
return { argv: null, shell: spec.command };
|
||||
}
|
||||
default:
|
||||
return { argv: null, shell: null, error: "unsupported installer" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function installSkill(
|
||||
params: SkillInstallRequest,
|
||||
): Promise<SkillInstallResult> {
|
||||
const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 300_000, 1_000), 900_000);
|
||||
const workspaceDir = resolveUserPath(params.workspaceDir);
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir);
|
||||
const entry = entries.find((item) => item.skill.name === params.skillName);
|
||||
if (!entry) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `Skill not found: ${params.skillName}`,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
}
|
||||
|
||||
const spec = findInstallSpec(entry, params.installId);
|
||||
if (!spec) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `Installer not found: ${params.installId}`,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
}
|
||||
|
||||
const command = buildInstallCommand(spec);
|
||||
if (command.error) {
|
||||
return {
|
||||
ok: false,
|
||||
message: command.error,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
}
|
||||
if (!command.shell && (!command.argv || command.argv.length === 0)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "invalid install command",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = command.shell
|
||||
? await runShell(command.shell, timeoutMs)
|
||||
: await runCommandWithTimeout(command.argv, { timeoutMs, cwd: command.cwd });
|
||||
|
||||
const success = result.code === 0;
|
||||
return {
|
||||
ok: success,
|
||||
message: success ? "Installed" : "Install failed",
|
||||
stdout: result.stdout.trim(),
|
||||
stderr: result.stderr.trim(),
|
||||
code: result.code,
|
||||
};
|
||||
}
|
||||
174
src/agents/skills-status.ts
Normal file
174
src/agents/skills-status.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import path from "node:path";
|
||||
|
||||
import type { ClawdisConfig } from "../config/config.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
import {
|
||||
hasBinary,
|
||||
isConfigPathTruthy,
|
||||
loadWorkspaceSkillEntries,
|
||||
resolveConfigPath,
|
||||
resolveSkillConfig,
|
||||
type SkillEntry,
|
||||
type SkillInstallSpec,
|
||||
} from "./skills.js";
|
||||
|
||||
export type SkillStatusConfigCheck = {
|
||||
path: string;
|
||||
value: unknown;
|
||||
satisfied: boolean;
|
||||
};
|
||||
|
||||
export type SkillInstallOption = {
|
||||
id: string;
|
||||
kind: SkillInstallSpec["kind"];
|
||||
label: string;
|
||||
bins: string[];
|
||||
};
|
||||
|
||||
export type SkillStatusEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
source: string;
|
||||
filePath: string;
|
||||
baseDir: string;
|
||||
skillKey: string;
|
||||
primaryEnv?: string;
|
||||
always: boolean;
|
||||
disabled: boolean;
|
||||
eligible: boolean;
|
||||
requirements: {
|
||||
bins: string[];
|
||||
env: string[];
|
||||
config: string[];
|
||||
};
|
||||
missing: {
|
||||
bins: string[];
|
||||
env: string[];
|
||||
config: string[];
|
||||
};
|
||||
configChecks: SkillStatusConfigCheck[];
|
||||
install: SkillInstallOption[];
|
||||
};
|
||||
|
||||
export type SkillStatusReport = {
|
||||
workspaceDir: string;
|
||||
managedSkillsDir: string;
|
||||
skills: SkillStatusEntry[];
|
||||
};
|
||||
|
||||
function resolveSkillKey(entry: SkillEntry): string {
|
||||
return entry.clawdis?.skillKey ?? entry.skill.name;
|
||||
}
|
||||
|
||||
function normalizeInstallOptions(entry: SkillEntry): SkillInstallOption[] {
|
||||
const install = entry.clawdis?.install ?? [];
|
||||
if (install.length === 0) return [];
|
||||
return install.map((spec, index) => {
|
||||
const id = (spec.id ?? `${spec.kind}-${index}`).trim();
|
||||
const bins = spec.bins ?? [];
|
||||
let label = (spec.label ?? "").trim();
|
||||
if (!label) {
|
||||
if (spec.kind === "brew" && spec.formula) {
|
||||
label = `Install ${spec.formula} (brew)`;
|
||||
} else if (spec.kind === "node" && spec.package) {
|
||||
label = `Install ${spec.package} (node)`;
|
||||
} else if (spec.kind === "go" && spec.module) {
|
||||
label = `Install ${spec.module} (go)`;
|
||||
} else if (spec.kind === "pnpm" && spec.repoPath) {
|
||||
label = `Install ${spec.repoPath} (pnpm)`;
|
||||
} else if (spec.kind === "git" && spec.url) {
|
||||
label = `Clone ${spec.url}`;
|
||||
} else {
|
||||
label = "Run installer";
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
kind: spec.kind,
|
||||
label,
|
||||
bins,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildSkillStatus(entry: SkillEntry, config?: ClawdisConfig): SkillStatusEntry {
|
||||
const skillKey = resolveSkillKey(entry);
|
||||
const skillConfig = resolveSkillConfig(config, skillKey);
|
||||
const disabled = skillConfig?.enabled === false;
|
||||
const always = entry.clawdis?.always === true;
|
||||
|
||||
const requiredBins = entry.clawdis?.requires?.bins ?? [];
|
||||
const requiredEnv = entry.clawdis?.requires?.env ?? [];
|
||||
const requiredConfig = entry.clawdis?.requires?.config ?? [];
|
||||
|
||||
const missingBins = requiredBins.filter((bin) => !hasBinary(bin));
|
||||
|
||||
const missingEnv: string[] = [];
|
||||
for (const envName of requiredEnv) {
|
||||
if (process.env[envName]) continue;
|
||||
if (skillConfig?.env?.[envName]) continue;
|
||||
if (skillConfig?.apiKey && entry.clawdis?.primaryEnv === envName) {
|
||||
continue;
|
||||
}
|
||||
missingEnv.push(envName);
|
||||
}
|
||||
|
||||
const configChecks: SkillStatusConfigCheck[] = requiredConfig.map((pathStr) => {
|
||||
const value = resolveConfigPath(config, pathStr);
|
||||
const satisfied = isConfigPathTruthy(config, pathStr);
|
||||
return { path: pathStr, value, satisfied };
|
||||
});
|
||||
const missingConfig = configChecks
|
||||
.filter((check) => !check.satisfied)
|
||||
.map((check) => check.path);
|
||||
|
||||
const missing = always
|
||||
? { bins: [], env: [], config: [] }
|
||||
: { bins: missingBins, env: missingEnv, config: missingConfig };
|
||||
const eligible =
|
||||
!disabled &&
|
||||
(always ||
|
||||
(missing.bins.length === 0 &&
|
||||
missing.env.length === 0 &&
|
||||
missing.config.length === 0));
|
||||
|
||||
return {
|
||||
name: entry.skill.name,
|
||||
description: entry.skill.description,
|
||||
source: entry.skill.source,
|
||||
filePath: entry.skill.filePath,
|
||||
baseDir: entry.skill.baseDir,
|
||||
skillKey,
|
||||
primaryEnv: entry.clawdis?.primaryEnv,
|
||||
always,
|
||||
disabled,
|
||||
eligible,
|
||||
requirements: {
|
||||
bins: requiredBins,
|
||||
env: requiredEnv,
|
||||
config: requiredConfig,
|
||||
},
|
||||
missing,
|
||||
configChecks,
|
||||
install: normalizeInstallOptions(entry),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWorkspaceSkillStatus(
|
||||
workspaceDir: string,
|
||||
opts?: {
|
||||
config?: ClawdisConfig;
|
||||
managedSkillsDir?: string;
|
||||
entries?: SkillEntry[];
|
||||
},
|
||||
): SkillStatusReport {
|
||||
const managedSkillsDir =
|
||||
opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
|
||||
const skillEntries =
|
||||
opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts);
|
||||
return {
|
||||
workspaceDir,
|
||||
managedSkillsDir,
|
||||
skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config)),
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
buildWorkspaceSkillsPrompt,
|
||||
loadWorkspaceSkillEntries,
|
||||
} from "./skills.js";
|
||||
import { buildWorkspaceSkillStatus } from "./skills-status.js";
|
||||
|
||||
async function writeSkill(params: {
|
||||
dir: string;
|
||||
@@ -295,6 +296,34 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildWorkspaceSkillStatus", () => {
|
||||
it("reports missing requirements and install options", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-"));
|
||||
const skillDir = path.join(workspaceDir, "skills", "status-skill");
|
||||
|
||||
await writeSkill({
|
||||
dir: skillDir,
|
||||
name: "status-skill",
|
||||
description: "Needs setup",
|
||||
metadata:
|
||||
'{"clawdis":{"requires":{"bins":["fakebin"],"env":["ENV_KEY"],"config":["browser.enabled"]},"install":[{"id":"brew","kind":"brew","formula":"fakebin","bins":["fakebin"],"label":"Install fakebin"}]}}',
|
||||
});
|
||||
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
config: { browser: { enabled: false } },
|
||||
});
|
||||
const skill = report.skills.find((entry) => entry.name === "status-skill");
|
||||
|
||||
expect(skill).toBeDefined();
|
||||
expect(skill?.eligible).toBe(false);
|
||||
expect(skill?.missing.bins).toContain("fakebin");
|
||||
expect(skill?.missing.env).toContain("ENV_KEY");
|
||||
expect(skill?.missing.config).toContain("browser.enabled");
|
||||
expect(skill?.install[0]?.id).toBe("brew");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applySkillEnvOverrides", () => {
|
||||
it("sets and restores env vars", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-"));
|
||||
|
||||
@@ -76,7 +76,6 @@ function resolveBundledSkillsDir(): string | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getFrontmatterValue(
|
||||
frontmatter: ParsedSkillFrontmatter,
|
||||
key: string,
|
||||
@@ -180,7 +179,10 @@ const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
|
||||
"browser.enabled": true,
|
||||
};
|
||||
|
||||
function resolveConfigPath(config: ClawdisConfig | undefined, pathStr: string) {
|
||||
export function resolveConfigPath(
|
||||
config: ClawdisConfig | undefined,
|
||||
pathStr: string,
|
||||
) {
|
||||
const parts = pathStr.split(".").filter(Boolean);
|
||||
let current: unknown = config;
|
||||
for (const part of parts) {
|
||||
@@ -190,7 +192,7 @@ function resolveConfigPath(config: ClawdisConfig | undefined, pathStr: string) {
|
||||
return current;
|
||||
}
|
||||
|
||||
function isConfigPathTruthy(
|
||||
export function isConfigPathTruthy(
|
||||
config: ClawdisConfig | undefined,
|
||||
pathStr: string,
|
||||
): boolean {
|
||||
@@ -201,7 +203,7 @@ function isConfigPathTruthy(
|
||||
return isTruthy(value);
|
||||
}
|
||||
|
||||
function resolveSkillConfig(
|
||||
export function resolveSkillConfig(
|
||||
config: ClawdisConfig | undefined,
|
||||
skillKey: string,
|
||||
): SkillConfig | undefined {
|
||||
@@ -212,7 +214,7 @@ function resolveSkillConfig(
|
||||
return entry;
|
||||
}
|
||||
|
||||
function hasBinary(bin: string): boolean {
|
||||
export function hasBinary(bin: string): boolean {
|
||||
const pathEnv = process.env.PATH ?? "";
|
||||
const parts = pathEnv.split(path.delimiter).filter(Boolean);
|
||||
for (const part of parts) {
|
||||
@@ -277,6 +279,7 @@ function resolveSkillKey(skill: Skill, entry?: SkillEntry): string {
|
||||
return entry?.clawdis?.skillKey ?? skill.name;
|
||||
}
|
||||
|
||||
|
||||
function shouldIncludeSkill(params: {
|
||||
entry: SkillEntry;
|
||||
config?: ClawdisConfig;
|
||||
@@ -326,6 +329,7 @@ function filterSkillEntries(
|
||||
return entries.filter((entry) => shouldIncludeSkill({ entry, config }));
|
||||
}
|
||||
|
||||
|
||||
export function applySkillEnvOverrides(params: {
|
||||
skills: SkillEntry[];
|
||||
config?: ClawdisConfig;
|
||||
@@ -435,11 +439,11 @@ function loadSkillEntries(
|
||||
const managedSkills = loadSkillsFromDir({
|
||||
dir: managedSkillsDir,
|
||||
source: "clawdis-managed",
|
||||
});
|
||||
}).skills;
|
||||
const workspaceSkills = loadSkillsFromDir({
|
||||
dir: workspaceSkillsDir,
|
||||
source: "clawdis-workspace",
|
||||
});
|
||||
}).skills;
|
||||
|
||||
const merged = new Map<string, Skill>();
|
||||
// Precedence: extra < bundled < managed < workspace
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
ConfigGetParamsSchema,
|
||||
type ConfigSetParams,
|
||||
ConfigSetParamsSchema,
|
||||
type SkillsInstallParams,
|
||||
SkillsInstallParamsSchema,
|
||||
type SkillsStatusParams,
|
||||
SkillsStatusParamsSchema,
|
||||
type SkillsUpdateParams,
|
||||
SkillsUpdateParamsSchema,
|
||||
type ConnectParams,
|
||||
ConnectParamsSchema,
|
||||
type CronAddParams,
|
||||
@@ -135,6 +141,15 @@ export const validateConfigGetParams = ajv.compile<ConfigGetParams>(
|
||||
export const validateConfigSetParams = ajv.compile<ConfigSetParams>(
|
||||
ConfigSetParamsSchema,
|
||||
);
|
||||
export const validateSkillsStatusParams = ajv.compile<SkillsStatusParams>(
|
||||
SkillsStatusParamsSchema,
|
||||
);
|
||||
export const validateSkillsInstallParams = ajv.compile<SkillsInstallParams>(
|
||||
SkillsInstallParamsSchema,
|
||||
);
|
||||
export const validateSkillsUpdateParams = ajv.compile<SkillsUpdateParams>(
|
||||
SkillsUpdateParamsSchema,
|
||||
);
|
||||
export const validateCronListParams =
|
||||
ajv.compile<CronListParams>(CronListParamsSchema);
|
||||
export const validateCronStatusParams = ajv.compile<CronStatusParams>(
|
||||
@@ -193,6 +208,9 @@ export {
|
||||
SessionsPatchParamsSchema,
|
||||
ConfigGetParamsSchema,
|
||||
ConfigSetParamsSchema,
|
||||
SkillsStatusParamsSchema,
|
||||
SkillsInstallParamsSchema,
|
||||
SkillsUpdateParamsSchema,
|
||||
CronJobSchema,
|
||||
CronListParamsSchema,
|
||||
CronStatusParamsSchema,
|
||||
@@ -232,6 +250,9 @@ export type {
|
||||
NodePairApproveParams,
|
||||
ConfigGetParams,
|
||||
ConfigSetParams,
|
||||
SkillsStatusParams,
|
||||
SkillsInstallParams,
|
||||
SkillsUpdateParams,
|
||||
NodePairRejectParams,
|
||||
NodePairVerifyParams,
|
||||
NodeListParams,
|
||||
|
||||
@@ -305,6 +305,30 @@ export const ConfigSetParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SkillsStatusParamsSchema = Type.Object(
|
||||
{},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SkillsInstallParamsSchema = Type.Object(
|
||||
{
|
||||
name: NonEmptyString,
|
||||
installId: NonEmptyString,
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SkillsUpdateParamsSchema = Type.Object(
|
||||
{
|
||||
skillKey: NonEmptyString,
|
||||
enabled: Type.Optional(Type.Boolean()),
|
||||
apiKey: Type.Optional(Type.String()),
|
||||
env: Type.Optional(Type.Record(NonEmptyString, Type.String())),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const CronScheduleSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
@@ -557,6 +581,9 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
ConfigGetParams: ConfigGetParamsSchema,
|
||||
ConfigSetParams: ConfigSetParamsSchema,
|
||||
SkillsStatusParams: SkillsStatusParamsSchema,
|
||||
SkillsInstallParams: SkillsInstallParamsSchema,
|
||||
SkillsUpdateParams: SkillsUpdateParamsSchema,
|
||||
CronJob: CronJobSchema,
|
||||
CronListParams: CronListParamsSchema,
|
||||
CronStatusParams: CronStatusParamsSchema,
|
||||
@@ -600,6 +627,9 @@ export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||
export type ConfigGetParams = Static<typeof ConfigGetParamsSchema>;
|
||||
export type ConfigSetParams = Static<typeof ConfigSetParamsSchema>;
|
||||
export type SkillsStatusParams = Static<typeof SkillsStatusParamsSchema>;
|
||||
export type SkillsInstallParams = Static<typeof SkillsInstallParamsSchema>;
|
||||
export type SkillsUpdateParams = Static<typeof SkillsUpdateParamsSchema>;
|
||||
export type CronJob = Static<typeof CronJobSchema>;
|
||||
export type CronListParams = Static<typeof CronListParamsSchema>;
|
||||
export type CronStatusParams = Static<typeof CronStatusParamsSchema>;
|
||||
|
||||
@@ -11,6 +11,9 @@ import chalk from "chalk";
|
||||
import { type WebSocket, WebSocketServer } from "ws";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import { installSkill } from "../agents/skills-install.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../agents/workspace.js";
|
||||
import {
|
||||
normalizeThinkLevel,
|
||||
normalizeVerboseLevel,
|
||||
@@ -90,7 +93,7 @@ import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
||||
import { sendMessageTelegram } from "../telegram/send.js";
|
||||
import { normalizeE164 } from "../utils.js";
|
||||
import { normalizeE164, resolveUserPath } from "../utils.js";
|
||||
import { setHeartbeatsEnabled } from "../web/auto-reply.js";
|
||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||
import { requestReplyHeartbeatNow } from "../web/reply-heartbeat-wake.js";
|
||||
@@ -150,6 +153,9 @@ import {
|
||||
validateSendParams,
|
||||
validateSessionsListParams,
|
||||
validateSessionsPatchParams,
|
||||
validateSkillsInstallParams,
|
||||
validateSkillsStatusParams,
|
||||
validateSkillsUpdateParams,
|
||||
validateWakeParams,
|
||||
} from "./protocol/index.js";
|
||||
import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.js";
|
||||
@@ -210,6 +216,9 @@ const METHODS = [
|
||||
"status",
|
||||
"config.get",
|
||||
"config.set",
|
||||
"skills.status",
|
||||
"skills.install",
|
||||
"skills.update",
|
||||
"voicewake.get",
|
||||
"voicewake.set",
|
||||
"sessions.list",
|
||||
@@ -3063,6 +3072,119 @@ export async function startGatewayServer(
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "skills.status": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSkillsStatusParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid skills.status params: ${formatValidationErrors(validateSkillsStatusParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw =
|
||||
cfg.inbound?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const workspaceDir = resolveUserPath(workspaceDirRaw);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
||||
config: cfg,
|
||||
});
|
||||
respond(true, report, undefined);
|
||||
break;
|
||||
}
|
||||
case "skills.install": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSkillsInstallParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid skills.install params: ${formatValidationErrors(validateSkillsInstallParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as {
|
||||
name: string;
|
||||
installId: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw =
|
||||
cfg.inbound?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const result = await installSkill({
|
||||
workspaceDir: workspaceDirRaw,
|
||||
skillName: p.name,
|
||||
installId: p.installId,
|
||||
timeoutMs: p.timeoutMs,
|
||||
});
|
||||
respond(
|
||||
result.ok,
|
||||
result,
|
||||
result.ok
|
||||
? undefined
|
||||
: errorShape(ErrorCodes.UNAVAILABLE, result.message),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "skills.update": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSkillsUpdateParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid skills.update params: ${formatValidationErrors(validateSkillsUpdateParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as {
|
||||
skillKey: string;
|
||||
enabled?: boolean;
|
||||
apiKey?: string;
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
const cfg = loadConfig();
|
||||
const skills = { ...(cfg.skills ?? {}) };
|
||||
const current = { ...(skills[p.skillKey] ?? {}) };
|
||||
if (typeof p.enabled === "boolean") {
|
||||
current.enabled = p.enabled;
|
||||
}
|
||||
if (typeof p.apiKey === "string") {
|
||||
const trimmed = p.apiKey.trim();
|
||||
if (trimmed) current.apiKey = trimmed;
|
||||
else delete current.apiKey;
|
||||
}
|
||||
if (p.env && typeof p.env === "object") {
|
||||
const nextEnv = { ...(current.env ?? {}) };
|
||||
for (const [key, value] of Object.entries(p.env)) {
|
||||
const trimmedKey = key.trim();
|
||||
if (!trimmedKey) continue;
|
||||
const trimmedVal = String(value ?? "").trim();
|
||||
if (!trimmedVal) delete nextEnv[trimmedKey];
|
||||
else nextEnv[trimmedKey] = trimmedVal;
|
||||
}
|
||||
current.env = nextEnv;
|
||||
}
|
||||
skills[p.skillKey] = current;
|
||||
const nextConfig: ClawdisConfig = {
|
||||
...cfg,
|
||||
skills,
|
||||
};
|
||||
await writeConfigFile(nextConfig);
|
||||
respond(
|
||||
true,
|
||||
{ ok: true, skillKey: p.skillKey, config: current },
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sessions.list": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSessionsListParams(params)) {
|
||||
|
||||
Reference in New Issue
Block a user