heyhank 0.1.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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import * as promptManager from "../prompt-manager.js";
|
|
3
|
+
|
|
4
|
+
function sanitizePaths(value: unknown): string[] | undefined {
|
|
5
|
+
if (!Array.isArray(value)) return undefined;
|
|
6
|
+
return value.filter((p): p is string => typeof p === "string");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerPromptRoutes(api: Hono): void {
|
|
10
|
+
api.get("/prompts", (c) => {
|
|
11
|
+
try {
|
|
12
|
+
const cwd = c.req.query("cwd");
|
|
13
|
+
const scope = c.req.query("scope");
|
|
14
|
+
const normalizedScope =
|
|
15
|
+
scope === "global" || scope === "project" || scope === "all"
|
|
16
|
+
? scope
|
|
17
|
+
: undefined;
|
|
18
|
+
return c.json(promptManager.listPrompts({ cwd, scope: normalizedScope }));
|
|
19
|
+
} catch (e: unknown) {
|
|
20
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
api.get("/prompts/:id", (c) => {
|
|
25
|
+
const prompt = promptManager.getPrompt(c.req.param("id"));
|
|
26
|
+
if (!prompt) return c.json({ error: "Prompt not found" }, 404);
|
|
27
|
+
return c.json(prompt);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
api.post("/prompts", async (c) => {
|
|
31
|
+
const body = await c.req.json().catch(() => ({}));
|
|
32
|
+
try {
|
|
33
|
+
const prompt = promptManager.createPrompt(
|
|
34
|
+
String(body.title || body.name || ""),
|
|
35
|
+
String(body.content || ""),
|
|
36
|
+
body.scope,
|
|
37
|
+
body.cwd ?? body.projectPath,
|
|
38
|
+
sanitizePaths(body.projectPaths),
|
|
39
|
+
);
|
|
40
|
+
return c.json(prompt, 201);
|
|
41
|
+
} catch (e: unknown) {
|
|
42
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
api.put("/prompts/:id", async (c) => {
|
|
47
|
+
const body = await c.req.json().catch(() => ({}));
|
|
48
|
+
try {
|
|
49
|
+
const prompt = promptManager.updatePrompt(c.req.param("id"), {
|
|
50
|
+
name: body.title ?? body.name,
|
|
51
|
+
content: body.content,
|
|
52
|
+
scope: body.scope,
|
|
53
|
+
projectPaths: sanitizePaths(body.projectPaths),
|
|
54
|
+
});
|
|
55
|
+
if (!prompt) return c.json({ error: "Prompt not found" }, 404);
|
|
56
|
+
return c.json(prompt);
|
|
57
|
+
} catch (e: unknown) {
|
|
58
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
api.delete("/prompts/:id", (c) => {
|
|
63
|
+
const deleted = promptManager.deletePrompt(c.req.param("id"));
|
|
64
|
+
if (!deleted) return c.json({ error: "Prompt not found" }, 404);
|
|
65
|
+
return c.json({ ok: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API routes for provider management
|
|
3
|
+
*/
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import { getAllProviders, getProviderById } from "../provider-registry.js";
|
|
6
|
+
import {
|
|
7
|
+
listProviderConfigs,
|
|
8
|
+
getProviderConfig,
|
|
9
|
+
upsertProviderConfig,
|
|
10
|
+
deleteProviderConfig,
|
|
11
|
+
} from "../provider-manager.js";
|
|
12
|
+
|
|
13
|
+
export function registerProviderRoutes(app: Hono): void {
|
|
14
|
+
// Static registry — all known providers with their metadata
|
|
15
|
+
app.get("/providers/registry", (c) => {
|
|
16
|
+
return c.json(getAllProviders());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// User's configured providers — merged with registry metadata, secrets masked
|
|
20
|
+
app.get("/providers", (c) => {
|
|
21
|
+
const registry = getAllProviders();
|
|
22
|
+
const configs = listProviderConfigs();
|
|
23
|
+
const configMap = new Map(configs.map((cfg) => [cfg.providerId, cfg]));
|
|
24
|
+
|
|
25
|
+
const merged = registry.map((def) => {
|
|
26
|
+
const cfg = configMap.get(def.id);
|
|
27
|
+
const envConfigured: Record<string, boolean> = {};
|
|
28
|
+
for (const field of def.envFields) {
|
|
29
|
+
envConfigured[field.key] = !!(cfg?.envValues[field.key]);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...def,
|
|
33
|
+
configured: !!cfg,
|
|
34
|
+
enabled: cfg?.enabled ?? false,
|
|
35
|
+
envConfigured,
|
|
36
|
+
customModel: cfg?.customModel ?? undefined,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return c.json(merged);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Single provider detail
|
|
44
|
+
app.get("/providers/:id", (c) => {
|
|
45
|
+
const id = c.req.param("id");
|
|
46
|
+
const def = getProviderById(id);
|
|
47
|
+
if (!def) return c.json({ error: "Unknown provider" }, 404);
|
|
48
|
+
|
|
49
|
+
const cfg = getProviderConfig(id);
|
|
50
|
+
const envConfigured: Record<string, boolean> = {};
|
|
51
|
+
// For non-secret fields, also return the actual value
|
|
52
|
+
const envValues: Record<string, string> = {};
|
|
53
|
+
for (const field of def.envFields) {
|
|
54
|
+
envConfigured[field.key] = !!(cfg?.envValues[field.key]);
|
|
55
|
+
if (!field.secret && cfg?.envValues[field.key]) {
|
|
56
|
+
envValues[field.key] = cfg.envValues[field.key];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return c.json({
|
|
61
|
+
...def,
|
|
62
|
+
configured: !!cfg,
|
|
63
|
+
enabled: cfg?.enabled ?? false,
|
|
64
|
+
envConfigured,
|
|
65
|
+
envValues,
|
|
66
|
+
customModel: cfg?.customModel ?? undefined,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Create or update a provider config
|
|
71
|
+
app.put("/providers/:id", async (c) => {
|
|
72
|
+
const id = c.req.param("id");
|
|
73
|
+
const def = getProviderById(id);
|
|
74
|
+
if (!def) return c.json({ error: "Unknown provider" }, 404);
|
|
75
|
+
|
|
76
|
+
const body = await c.req.json();
|
|
77
|
+
const config = upsertProviderConfig(id, {
|
|
78
|
+
enabled: body.enabled,
|
|
79
|
+
envValues: body.envValues,
|
|
80
|
+
customModel: body.customModel,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Return masked response
|
|
84
|
+
const envConfigured: Record<string, boolean> = {};
|
|
85
|
+
const envValues: Record<string, string> = {};
|
|
86
|
+
for (const field of def.envFields) {
|
|
87
|
+
envConfigured[field.key] = !!(config.envValues[field.key]);
|
|
88
|
+
if (!field.secret && config.envValues[field.key]) {
|
|
89
|
+
envValues[field.key] = config.envValues[field.key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return c.json({
|
|
94
|
+
...def,
|
|
95
|
+
configured: true,
|
|
96
|
+
enabled: config.enabled,
|
|
97
|
+
envConfigured,
|
|
98
|
+
envValues,
|
|
99
|
+
customModel: config.customModel ?? undefined,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Delete a provider config
|
|
104
|
+
app.delete("/providers/:id", (c) => {
|
|
105
|
+
const id = c.req.param("id");
|
|
106
|
+
const deleted = deleteProviderConfig(id);
|
|
107
|
+
return c.json({ ok: deleted });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import * as sandboxManager from "../sandbox-manager.js";
|
|
4
|
+
import { containerManager, type ContainerConfig } from "../container-manager.js";
|
|
5
|
+
import { imagePullManager } from "../image-pull-manager.js";
|
|
6
|
+
|
|
7
|
+
export function registerSandboxRoutes(
|
|
8
|
+
api: Hono,
|
|
9
|
+
): void {
|
|
10
|
+
api.get("/sandboxes", (c) => {
|
|
11
|
+
try {
|
|
12
|
+
return c.json(sandboxManager.listSandboxes());
|
|
13
|
+
} catch (e: unknown) {
|
|
14
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
api.get("/sandboxes/:slug", (c) => {
|
|
19
|
+
const sandbox = sandboxManager.getSandbox(c.req.param("slug"));
|
|
20
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
21
|
+
return c.json(sandbox);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
api.post("/sandboxes", async (c) => {
|
|
25
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26
|
+
try {
|
|
27
|
+
const sandbox = sandboxManager.createSandbox(body.name, {
|
|
28
|
+
initScript: body.initScript,
|
|
29
|
+
});
|
|
30
|
+
return c.json(sandbox, 201);
|
|
31
|
+
} catch (e: unknown) {
|
|
32
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
api.put("/sandboxes/:slug", async (c) => {
|
|
37
|
+
const slug = c.req.param("slug");
|
|
38
|
+
const body = await c.req.json().catch(() => ({}));
|
|
39
|
+
try {
|
|
40
|
+
const sandbox = sandboxManager.updateSandbox(slug, {
|
|
41
|
+
name: body.name,
|
|
42
|
+
initScript: body.initScript,
|
|
43
|
+
});
|
|
44
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
45
|
+
return c.json(sandbox);
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
api.delete("/sandboxes/:slug", (c) => {
|
|
52
|
+
try {
|
|
53
|
+
const deleted = sandboxManager.deleteSandbox(c.req.param("slug"));
|
|
54
|
+
if (!deleted) return c.json({ error: "Sandbox not found" }, 404);
|
|
55
|
+
return c.json({ ok: true });
|
|
56
|
+
} catch (e: unknown) {
|
|
57
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Test the init script of a sandbox in an ephemeral container.
|
|
62
|
+
// Accepts an optional `initScript` body param to test unsaved content
|
|
63
|
+
// without persisting it first. Falls back to the stored script.
|
|
64
|
+
api.post("/sandboxes/:slug/test-init", async (c) => {
|
|
65
|
+
const slug = c.req.param("slug");
|
|
66
|
+
const body = await c.req.json().catch(() => ({}));
|
|
67
|
+
const rawCwd = body.cwd;
|
|
68
|
+
|
|
69
|
+
const sandbox = sandboxManager.getSandbox(slug);
|
|
70
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
71
|
+
|
|
72
|
+
// Prefer body initScript (unsaved draft) over stored value
|
|
73
|
+
const initScript = (typeof body.initScript === "string" ? body.initScript : sandbox.initScript ?? "").trim();
|
|
74
|
+
if (!initScript) return c.json({ error: "No init script configured for this sandbox" }, 400);
|
|
75
|
+
if (!rawCwd) return c.json({ error: "Working directory (cwd) is required" }, 400);
|
|
76
|
+
|
|
77
|
+
// Require an absolute path from the caller, then normalize
|
|
78
|
+
const cwdStr = String(rawCwd);
|
|
79
|
+
if (!cwdStr.startsWith("/")) return c.json({ error: "Working directory must be an absolute path" }, 400);
|
|
80
|
+
const cwd = resolve(cwdStr);
|
|
81
|
+
|
|
82
|
+
if (!containerManager.checkDocker()) return c.json({ error: "Docker is not available" }, 503);
|
|
83
|
+
|
|
84
|
+
const effectiveImage = "the-companion:latest";
|
|
85
|
+
if (!imagePullManager.isReady(effectiveImage)) {
|
|
86
|
+
return c.json({ error: `Docker image ${effectiveImage} is not available. Pull it first.` }, 503);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tempId = `t${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
90
|
+
let containerId: string | undefined;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const config: ContainerConfig = {
|
|
94
|
+
image: effectiveImage,
|
|
95
|
+
ports: [],
|
|
96
|
+
};
|
|
97
|
+
const containerInfo = containerManager.createContainer(tempId, cwd, config);
|
|
98
|
+
containerId = containerInfo.containerId;
|
|
99
|
+
|
|
100
|
+
await containerManager.copyWorkspaceToContainer(containerId, cwd);
|
|
101
|
+
|
|
102
|
+
const initTimeout = Number(process.env.HEYHANK_INIT_SCRIPT_TIMEOUT || process.env.COMPANION_INIT_SCRIPT_TIMEOUT) || 120_000;
|
|
103
|
+
const result = await containerManager.execInContainerAsync(
|
|
104
|
+
containerId,
|
|
105
|
+
["sh", "-lc", initScript],
|
|
106
|
+
{ timeout: initTimeout },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const output = result.output.length > 2000
|
|
110
|
+
? result.output.slice(0, 500) + "\n...[truncated]...\n" + result.output.slice(-1500)
|
|
111
|
+
: result.output;
|
|
112
|
+
|
|
113
|
+
return c.json({
|
|
114
|
+
success: result.exitCode === 0,
|
|
115
|
+
exitCode: result.exitCode,
|
|
116
|
+
output,
|
|
117
|
+
});
|
|
118
|
+
} catch (e: unknown) {
|
|
119
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
120
|
+
return c.json({ success: false, exitCode: -1, output: msg }, 500);
|
|
121
|
+
} finally {
|
|
122
|
+
if (containerId) {
|
|
123
|
+
try { containerManager.removeContainer(tempId); } catch { /* best effort cleanup */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import { DEFAULT_ANTHROPIC_MODEL, getSettings, updateSettings, type UpdateChannel } from "../settings-manager.js";
|
|
3
|
+
import { hasContainerCodexAuth } from "../codex-container-auth.js";
|
|
4
|
+
import { hasContainerClaudeAuth } from "../claude-container-auth.js";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
/** Detect Claude CLI auth method */
|
|
11
|
+
function detectClaudeAuthStatus(): { authenticated: boolean; method: string } {
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
13
|
+
|
|
14
|
+
// Check for credentials file (claude login)
|
|
15
|
+
const credFiles = [
|
|
16
|
+
join(home, ".claude", ".credentials.json"),
|
|
17
|
+
join(home, ".claude", "auth.json"),
|
|
18
|
+
join(home, ".claude", ".auth.json"),
|
|
19
|
+
join(home, ".claude", "credentials.json"),
|
|
20
|
+
];
|
|
21
|
+
if (credFiles.some((p) => existsSync(p))) {
|
|
22
|
+
return { authenticated: true, method: "cli_login" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check env vars
|
|
26
|
+
if (process.env.ANTHROPIC_API_KEY) return { authenticated: true, method: "env_api_key" };
|
|
27
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return { authenticated: true, method: "env_oauth" };
|
|
28
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) return { authenticated: true, method: "env_auth_token" };
|
|
29
|
+
|
|
30
|
+
return { authenticated: false, method: "none" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Detect Codex CLI auth method */
|
|
34
|
+
function detectCodexAuthStatus(): { authenticated: boolean; method: string } {
|
|
35
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
36
|
+
|
|
37
|
+
// Check for codex auth file
|
|
38
|
+
const codexAuth = join(home, ".codex", "auth.json");
|
|
39
|
+
if (existsSync(codexAuth)) {
|
|
40
|
+
return { authenticated: true, method: "cli_login" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check env var
|
|
44
|
+
if (process.env.OPENAI_API_KEY) return { authenticated: true, method: "env_api_key" };
|
|
45
|
+
|
|
46
|
+
return { authenticated: false, method: "none" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Detect Claude CLI version */
|
|
50
|
+
function detectClaudeVersion(): string | null {
|
|
51
|
+
try {
|
|
52
|
+
return execSync("claude --version 2>/dev/null", { timeout: 3000 }).toString().trim().split("\n")[0] || null;
|
|
53
|
+
} catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Detect Codex CLI version */
|
|
57
|
+
function detectCodexVersion(): string | null {
|
|
58
|
+
try {
|
|
59
|
+
return execSync("codex --version 2>/dev/null", { timeout: 3000 }).toString().trim().split("\n")[0] || null;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerSettingsRoutes(api: Hono): void {
|
|
64
|
+
api.get("/settings", (c) => {
|
|
65
|
+
const settings = getSettings();
|
|
66
|
+
const claudeAuth = detectClaudeAuthStatus();
|
|
67
|
+
const codexAuth = detectCodexAuthStatus();
|
|
68
|
+
return c.json({
|
|
69
|
+
anthropicApiKeyConfigured: !!settings.anthropicApiKey.trim(),
|
|
70
|
+
anthropicModel: settings.anthropicModel || DEFAULT_ANTHROPIC_MODEL,
|
|
71
|
+
claudeCodeOAuthTokenConfigured: !!settings.claudeCodeOAuthToken.trim(),
|
|
72
|
+
openaiApiKeyConfigured: !!settings.openaiApiKey.trim(),
|
|
73
|
+
codexDeviceAuthConfigured: hasContainerCodexAuth(),
|
|
74
|
+
// Enhanced auth detection
|
|
75
|
+
claudeCliAuth: { ...claudeAuth, oauthTokenConfigured: !!settings.claudeCodeOAuthToken.trim(), cliVersion: detectClaudeVersion() },
|
|
76
|
+
codexCliAuth: { ...codexAuth, apiKeyConfigured: !!settings.openaiApiKey.trim(), cliVersion: detectCodexVersion() },
|
|
77
|
+
onboardingCompleted: settings.onboardingCompleted,
|
|
78
|
+
geminiApiKeyConfigured: !!settings.geminiApiKey.trim(),
|
|
79
|
+
geminiVoice: settings.geminiVoice || "Kore",
|
|
80
|
+
assistantName: settings.assistantName || "",
|
|
81
|
+
userName: settings.userName || "",
|
|
82
|
+
editorTabEnabled: settings.editorTabEnabled,
|
|
83
|
+
internalAiProvider: settings.internalAiProvider || "",
|
|
84
|
+
aiValidationEnabled: settings.aiValidationEnabled,
|
|
85
|
+
aiValidationAutoApprove: settings.aiValidationAutoApprove,
|
|
86
|
+
aiValidationAutoDeny: settings.aiValidationAutoDeny,
|
|
87
|
+
publicUrl: settings.publicUrl,
|
|
88
|
+
updateChannel: settings.updateChannel,
|
|
89
|
+
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
api.put("/settings", async (c) => {
|
|
94
|
+
const body = await c.req.json().catch(() => ({}));
|
|
95
|
+
if (body.anthropicApiKey !== undefined && typeof body.anthropicApiKey !== "string") {
|
|
96
|
+
return c.json({ error: "anthropicApiKey must be a string" }, 400);
|
|
97
|
+
}
|
|
98
|
+
if (body.anthropicModel !== undefined && typeof body.anthropicModel !== "string") {
|
|
99
|
+
return c.json({ error: "anthropicModel must be a string" }, 400);
|
|
100
|
+
}
|
|
101
|
+
if (body.geminiApiKey !== undefined && typeof body.geminiApiKey !== "string") {
|
|
102
|
+
return c.json({ error: "geminiApiKey must be a string" }, 400);
|
|
103
|
+
}
|
|
104
|
+
if (body.geminiVoice !== undefined && typeof body.geminiVoice !== "string") {
|
|
105
|
+
return c.json({ error: "geminiVoice must be a string" }, 400);
|
|
106
|
+
}
|
|
107
|
+
if (body.editorTabEnabled !== undefined && typeof body.editorTabEnabled !== "boolean") {
|
|
108
|
+
return c.json({ error: "editorTabEnabled must be a boolean" }, 400);
|
|
109
|
+
}
|
|
110
|
+
if (body.aiValidationEnabled !== undefined && typeof body.aiValidationEnabled !== "boolean") {
|
|
111
|
+
return c.json({ error: "aiValidationEnabled must be a boolean" }, 400);
|
|
112
|
+
}
|
|
113
|
+
if (body.aiValidationAutoApprove !== undefined && typeof body.aiValidationAutoApprove !== "boolean") {
|
|
114
|
+
return c.json({ error: "aiValidationAutoApprove must be a boolean" }, 400);
|
|
115
|
+
}
|
|
116
|
+
if (body.aiValidationAutoDeny !== undefined && typeof body.aiValidationAutoDeny !== "boolean") {
|
|
117
|
+
return c.json({ error: "aiValidationAutoDeny must be a boolean" }, 400);
|
|
118
|
+
}
|
|
119
|
+
if (body.publicUrl !== undefined) {
|
|
120
|
+
if (typeof body.publicUrl !== "string") {
|
|
121
|
+
return c.json({ error: "publicUrl must be a string" }, 400);
|
|
122
|
+
}
|
|
123
|
+
const trimmed = body.publicUrl.trim().replace(/\/+$/, "");
|
|
124
|
+
if (trimmed !== "" && !/^https?:\/\/.+/.test(trimmed)) {
|
|
125
|
+
return c.json({ error: "publicUrl must be a valid http/https URL" }, 400);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (body.updateChannel !== undefined && body.updateChannel !== "stable" && body.updateChannel !== "prerelease") {
|
|
129
|
+
return c.json({ error: "updateChannel must be 'stable' or 'prerelease'" }, 400);
|
|
130
|
+
}
|
|
131
|
+
if (body.claudeCodeOAuthToken !== undefined && typeof body.claudeCodeOAuthToken !== "string") {
|
|
132
|
+
return c.json({ error: "claudeCodeOAuthToken must be a string" }, 400);
|
|
133
|
+
}
|
|
134
|
+
if (body.openaiApiKey !== undefined && typeof body.openaiApiKey !== "string") {
|
|
135
|
+
return c.json({ error: "openaiApiKey must be a string" }, 400);
|
|
136
|
+
}
|
|
137
|
+
if (body.onboardingCompleted !== undefined && typeof body.onboardingCompleted !== "boolean") {
|
|
138
|
+
return c.json({ error: "onboardingCompleted must be a boolean" }, 400);
|
|
139
|
+
}
|
|
140
|
+
if (body.dockerAutoUpdate !== undefined && typeof body.dockerAutoUpdate !== "boolean") {
|
|
141
|
+
return c.json({ error: "dockerAutoUpdate must be a boolean" }, 400);
|
|
142
|
+
}
|
|
143
|
+
const hasAnyField = body.anthropicApiKey !== undefined || body.anthropicModel !== undefined
|
|
144
|
+
|| body.claudeCodeOAuthToken !== undefined || body.openaiApiKey !== undefined
|
|
145
|
+
|| body.onboardingCompleted !== undefined
|
|
146
|
+
|| body.geminiApiKey !== undefined || body.geminiVoice !== undefined || body.assistantName !== undefined || body.userName !== undefined
|
|
147
|
+
|| body.editorTabEnabled !== undefined
|
|
148
|
+
|| body.internalAiProvider !== undefined
|
|
149
|
+
|| body.aiValidationEnabled !== undefined || body.aiValidationAutoApprove !== undefined
|
|
150
|
+
|| body.aiValidationAutoDeny !== undefined
|
|
151
|
+
|| body.publicUrl !== undefined
|
|
152
|
+
|| body.updateChannel !== undefined
|
|
153
|
+
|| body.dockerAutoUpdate !== undefined;
|
|
154
|
+
if (!hasAnyField) {
|
|
155
|
+
return c.json({ error: "At least one settings field is required" }, 400);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const settings = updateSettings({
|
|
159
|
+
anthropicApiKey:
|
|
160
|
+
typeof body.anthropicApiKey === "string"
|
|
161
|
+
? body.anthropicApiKey.trim()
|
|
162
|
+
: undefined,
|
|
163
|
+
anthropicModel:
|
|
164
|
+
typeof body.anthropicModel === "string"
|
|
165
|
+
? (body.anthropicModel.trim() || DEFAULT_ANTHROPIC_MODEL)
|
|
166
|
+
: undefined,
|
|
167
|
+
claudeCodeOAuthToken:
|
|
168
|
+
typeof body.claudeCodeOAuthToken === "string"
|
|
169
|
+
? body.claudeCodeOAuthToken.trim()
|
|
170
|
+
: undefined,
|
|
171
|
+
openaiApiKey:
|
|
172
|
+
typeof body.openaiApiKey === "string"
|
|
173
|
+
? body.openaiApiKey.trim()
|
|
174
|
+
: undefined,
|
|
175
|
+
onboardingCompleted:
|
|
176
|
+
typeof body.onboardingCompleted === "boolean"
|
|
177
|
+
? body.onboardingCompleted
|
|
178
|
+
: undefined,
|
|
179
|
+
geminiApiKey:
|
|
180
|
+
typeof body.geminiApiKey === "string"
|
|
181
|
+
? body.geminiApiKey.trim()
|
|
182
|
+
: undefined,
|
|
183
|
+
geminiVoice:
|
|
184
|
+
typeof body.geminiVoice === "string"
|
|
185
|
+
? body.geminiVoice.trim()
|
|
186
|
+
: undefined,
|
|
187
|
+
assistantName:
|
|
188
|
+
typeof body.assistantName === "string"
|
|
189
|
+
? body.assistantName.trim()
|
|
190
|
+
: undefined,
|
|
191
|
+
userName:
|
|
192
|
+
typeof body.userName === "string"
|
|
193
|
+
? body.userName.trim()
|
|
194
|
+
: undefined,
|
|
195
|
+
editorTabEnabled:
|
|
196
|
+
typeof body.editorTabEnabled === "boolean"
|
|
197
|
+
? body.editorTabEnabled
|
|
198
|
+
: undefined,
|
|
199
|
+
internalAiProvider:
|
|
200
|
+
typeof body.internalAiProvider === "string"
|
|
201
|
+
? body.internalAiProvider.trim()
|
|
202
|
+
: undefined,
|
|
203
|
+
aiValidationEnabled:
|
|
204
|
+
typeof body.aiValidationEnabled === "boolean"
|
|
205
|
+
? body.aiValidationEnabled
|
|
206
|
+
: undefined,
|
|
207
|
+
aiValidationAutoApprove:
|
|
208
|
+
typeof body.aiValidationAutoApprove === "boolean"
|
|
209
|
+
? body.aiValidationAutoApprove
|
|
210
|
+
: undefined,
|
|
211
|
+
aiValidationAutoDeny:
|
|
212
|
+
typeof body.aiValidationAutoDeny === "boolean"
|
|
213
|
+
? body.aiValidationAutoDeny
|
|
214
|
+
: undefined,
|
|
215
|
+
publicUrl:
|
|
216
|
+
typeof body.publicUrl === "string"
|
|
217
|
+
? body.publicUrl.trim().replace(/\/+$/, "")
|
|
218
|
+
: undefined,
|
|
219
|
+
updateChannel:
|
|
220
|
+
body.updateChannel === "stable" || body.updateChannel === "prerelease"
|
|
221
|
+
? (body.updateChannel as UpdateChannel)
|
|
222
|
+
: undefined,
|
|
223
|
+
dockerAutoUpdate:
|
|
224
|
+
typeof body.dockerAutoUpdate === "boolean"
|
|
225
|
+
? body.dockerAutoUpdate
|
|
226
|
+
: undefined,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const claudeAuthAfter = detectClaudeAuthStatus();
|
|
230
|
+
const codexAuthAfter = detectCodexAuthStatus();
|
|
231
|
+
return c.json({
|
|
232
|
+
anthropicApiKeyConfigured: !!settings.anthropicApiKey.trim(),
|
|
233
|
+
anthropicModel: settings.anthropicModel || DEFAULT_ANTHROPIC_MODEL,
|
|
234
|
+
claudeCodeOAuthTokenConfigured: !!settings.claudeCodeOAuthToken.trim(),
|
|
235
|
+
openaiApiKeyConfigured: !!settings.openaiApiKey.trim(),
|
|
236
|
+
codexDeviceAuthConfigured: hasContainerCodexAuth(),
|
|
237
|
+
claudeCliAuth: { ...claudeAuthAfter, oauthTokenConfigured: !!settings.claudeCodeOAuthToken.trim(), cliVersion: detectClaudeVersion() },
|
|
238
|
+
codexCliAuth: { ...codexAuthAfter, apiKeyConfigured: !!settings.openaiApiKey.trim(), cliVersion: detectCodexVersion() },
|
|
239
|
+
onboardingCompleted: settings.onboardingCompleted,
|
|
240
|
+
geminiApiKeyConfigured: !!settings.geminiApiKey.trim(),
|
|
241
|
+
geminiVoice: settings.geminiVoice || "Kore",
|
|
242
|
+
assistantName: settings.assistantName || "",
|
|
243
|
+
userName: settings.userName || "",
|
|
244
|
+
editorTabEnabled: settings.editorTabEnabled,
|
|
245
|
+
internalAiProvider: settings.internalAiProvider || "",
|
|
246
|
+
aiValidationEnabled: settings.aiValidationEnabled,
|
|
247
|
+
aiValidationAutoApprove: settings.aiValidationAutoApprove,
|
|
248
|
+
aiValidationAutoDeny: settings.aiValidationAutoDeny,
|
|
249
|
+
publicUrl: settings.publicUrl,
|
|
250
|
+
updateChannel: settings.updateChannel,
|
|
251
|
+
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
api.post("/settings/anthropic/verify", async (c) => {
|
|
256
|
+
const body = await c.req.json().catch(() => ({} as { apiKey?: string }));
|
|
257
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
258
|
+
if (!apiKey) {
|
|
259
|
+
return c.json({ valid: false, error: "API key is required" }, 400);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
267
|
+
headers: {
|
|
268
|
+
"x-api-key": apiKey,
|
|
269
|
+
"anthropic-version": "2023-06-01",
|
|
270
|
+
},
|
|
271
|
+
signal: controller.signal,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (res.ok) {
|
|
275
|
+
return c.json({ valid: true });
|
|
276
|
+
}
|
|
277
|
+
return c.json({ valid: false, error: `API returned ${res.status}` });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
280
|
+
return c.json({ valid: false, error: isAbort ? "Request timed out" : "Request failed" });
|
|
281
|
+
} finally {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile, writeFile, rm, mkdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { Hono } from "hono";
|
|
6
|
+
|
|
7
|
+
const SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
8
|
+
|
|
9
|
+
export function registerSkillRoutes(api: Hono): void {
|
|
10
|
+
api.get("/skills", async (c) => {
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(SKILLS_DIR)) return c.json([]);
|
|
13
|
+
const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
14
|
+
const skills = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.isDirectory()) continue;
|
|
17
|
+
const skillMdPath = join(SKILLS_DIR, entry.name, "SKILL.md");
|
|
18
|
+
if (!existsSync(skillMdPath)) continue;
|
|
19
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
20
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
21
|
+
let name = entry.name;
|
|
22
|
+
let description = "";
|
|
23
|
+
if (fmMatch) {
|
|
24
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
25
|
+
const nameMatch = line.match(/^name:\s*(.+)/);
|
|
26
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
27
|
+
const descMatch = line.match(/^description:\s*["']?(.+?)["']?\s*$/);
|
|
28
|
+
if (descMatch) description = descMatch[1];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
skills.push({ slug: entry.name, name, description, path: skillMdPath });
|
|
32
|
+
}
|
|
33
|
+
return c.json(skills);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return c.json({ error: String(e) }, 500);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
api.get("/skills/:slug", async (c) => {
|
|
40
|
+
const slug = c.req.param("slug");
|
|
41
|
+
if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
42
|
+
return c.json({ error: "Invalid slug" }, 400);
|
|
43
|
+
}
|
|
44
|
+
const skillMdPath = join(SKILLS_DIR, slug, "SKILL.md");
|
|
45
|
+
if (!existsSync(skillMdPath)) return c.json({ error: "Skill not found" }, 404);
|
|
46
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
47
|
+
return c.json({ slug, path: skillMdPath, content });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
api.post("/skills", async (c) => {
|
|
51
|
+
const body = await c.req.json().catch(() => ({}));
|
|
52
|
+
const { name, description, content } = body;
|
|
53
|
+
if (!name || typeof name !== "string") {
|
|
54
|
+
return c.json({ error: "name is required" }, 400);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
58
|
+
if (!slug) return c.json({ error: "Invalid name" }, 400);
|
|
59
|
+
|
|
60
|
+
const skillDir = join(SKILLS_DIR, slug);
|
|
61
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
62
|
+
if (existsSync(skillMdPath)) {
|
|
63
|
+
return c.json({ error: `Skill "${slug}" already exists` }, 409);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await mkdir(SKILLS_DIR, { recursive: true });
|
|
67
|
+
await mkdir(skillDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
const md = `---\nname: ${slug}\ndescription: ${JSON.stringify(description || `Skill: ${name}`)}\n---\n\n${content || `# ${name}\n\nDescribe what this skill does and how to use it.\n`}`;
|
|
70
|
+
await writeFile(skillMdPath, md, "utf-8");
|
|
71
|
+
|
|
72
|
+
return c.json({ slug, name, description: description || `Skill: ${name}`, path: skillMdPath });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
api.put("/skills/:slug", async (c) => {
|
|
76
|
+
const slug = c.req.param("slug");
|
|
77
|
+
if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
78
|
+
return c.json({ error: "Invalid slug" }, 400);
|
|
79
|
+
}
|
|
80
|
+
const skillMdPath = join(SKILLS_DIR, slug, "SKILL.md");
|
|
81
|
+
if (!existsSync(skillMdPath)) return c.json({ error: "Skill not found" }, 404);
|
|
82
|
+
const body = await c.req.json().catch(() => ({}));
|
|
83
|
+
if (typeof body.content !== "string") {
|
|
84
|
+
return c.json({ error: "content is required" }, 400);
|
|
85
|
+
}
|
|
86
|
+
await writeFile(skillMdPath, body.content, "utf-8");
|
|
87
|
+
return c.json({ ok: true, slug, path: skillMdPath });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
api.delete("/skills/:slug", async (c) => {
|
|
91
|
+
const slug = c.req.param("slug");
|
|
92
|
+
if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
93
|
+
return c.json({ error: "Invalid slug" }, 400);
|
|
94
|
+
}
|
|
95
|
+
const skillDir = join(SKILLS_DIR, slug);
|
|
96
|
+
if (!existsSync(skillDir)) return c.json({ error: "Skill not found" }, 404);
|
|
97
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
98
|
+
return c.json({ ok: true, slug });
|
|
99
|
+
});
|
|
100
|
+
}
|