heyhank 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +83 -10
- package/bin/cli.ts +7 -7
- package/bin/ctl.ts +42 -42
- package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-B-AAmsMK.js} +3 -3
- package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
- package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
- package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
- package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
- package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
- package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
- package/dist/assets/MediaPage-C48HTTrt.js +1 -0
- package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
- package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
- package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
- package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
- package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
- package/dist/assets/RunsPage-B9UOyO79.js +1 -0
- package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
- package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
- package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
- package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
- package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
- package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
- package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
- package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
- package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
- package/dist/assets/index-BkjSoVgn.css +32 -0
- package/dist/assets/sw-register-C7NOHtIu.js +1 -0
- package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +6 -1
- package/server/agent-executor.ts +37 -2
- package/server/agent-store.ts +3 -3
- package/server/agent-types.ts +11 -0
- package/server/assistant-store.ts +232 -6
- package/server/auth-manager.ts +9 -0
- package/server/cache-headers.ts +1 -1
- package/server/calendar-service.ts +10 -0
- package/server/ceo/document-store.ts +129 -0
- package/server/ceo/finance-store.ts +343 -0
- package/server/ceo/kpi-store.ts +208 -0
- package/server/ceo/memory-import.ts +277 -0
- package/server/ceo/news-store.ts +208 -0
- package/server/ceo/template-store.ts +134 -0
- package/server/ceo/time-tracking-store.ts +227 -0
- package/server/claude-auth-monitor.ts +128 -0
- package/server/claude-code-worker.ts +86 -0
- package/server/claude-session-discovery.ts +74 -1
- package/server/cli-launcher.ts +32 -10
- package/server/codex-adapter.ts +2 -2
- package/server/codex-ws-proxy.cjs +1 -1
- package/server/container-manager.ts +4 -4
- package/server/content-intelligence/content-engine.ts +1112 -0
- package/server/content-intelligence/platform-knowledge.ts +870 -0
- package/server/cron-store.ts +3 -3
- package/server/embedding-service.ts +49 -0
- package/server/event-bus-types.ts +13 -0
- package/server/federation/node-store.ts +5 -4
- package/server/fs-utils.ts +28 -1
- package/server/hank-notifications-store.ts +91 -0
- package/server/hank-tool-executor.ts +1835 -0
- package/server/hank-tools.ts +2107 -0
- package/server/image-pull-manager.ts +2 -2
- package/server/index.ts +25 -2
- package/server/llm-providers-streaming.ts +541 -0
- package/server/llm-providers.ts +12 -0
- package/server/marketplace.ts +249 -0
- package/server/mcp-registry.ts +158 -0
- package/server/memory-service.ts +296 -0
- package/server/obsidian-sync.ts +184 -0
- package/server/provider-manager.ts +5 -2
- package/server/provider-registry.ts +12 -0
- package/server/reminder-scheduler.ts +37 -1
- package/server/routes/agent-routes.ts +2 -1
- package/server/routes/assistant-routes.ts +198 -5
- package/server/routes/ceo-finance-kpi-routes.ts +167 -0
- package/server/routes/ceo-news-time-routes.ts +137 -0
- package/server/routes/ceo-routes.ts +99 -0
- package/server/routes/content-routes.ts +116 -0
- package/server/routes/email-routes.ts +147 -0
- package/server/routes/env-routes.ts +3 -3
- package/server/routes/fs-routes.ts +12 -9
- package/server/routes/hank-chat-routes.ts +592 -0
- package/server/routes/llm-routes.ts +12 -0
- package/server/routes/marketplace-routes.ts +63 -0
- package/server/routes/media-routes.ts +1 -1
- package/server/routes/memory-routes.ts +127 -0
- package/server/routes/platform-routes.ts +14 -675
- package/server/routes/sandbox-routes.ts +1 -1
- package/server/routes/settings-routes.ts +51 -1
- package/server/routes/socialmedia-routes.ts +152 -2
- package/server/routes/system-routes.ts +2 -2
- package/server/routes/team-routes.ts +71 -0
- package/server/routes/telephony-routes.ts +98 -18
- package/server/routes.ts +36 -9
- package/server/session-creation-service.ts +2 -2
- package/server/session-orchestrator.ts +54 -2
- package/server/session-types.ts +2 -0
- package/server/settings-manager.ts +50 -2
- package/server/skill-discovery.ts +68 -0
- package/server/socialmedia/adapters/browser-adapter.ts +179 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
- package/server/socialmedia/manager.ts +234 -15
- package/server/socialmedia/store.ts +51 -1
- package/server/socialmedia/types.ts +35 -2
- package/server/socialview/browser-manager.ts +150 -0
- package/server/socialview/extractors.ts +1298 -0
- package/server/socialview/image-describe.ts +188 -0
- package/server/socialview/library.ts +119 -0
- package/server/socialview/poster.ts +276 -0
- package/server/socialview/routes.ts +371 -0
- package/server/socialview/style-analyzer.ts +187 -0
- package/server/socialview/style-profiles.ts +67 -0
- package/server/socialview/types.ts +166 -0
- package/server/socialview/vision.ts +127 -0
- package/server/socialview/vnc-manager.ts +110 -0
- package/server/style-injector.ts +135 -0
- package/server/team-service.ts +239 -0
- package/server/team-store.ts +75 -0
- package/server/team-types.ts +52 -0
- package/server/telephony/audio-bridge.ts +281 -35
- package/server/telephony/audio-recorder.ts +132 -0
- package/server/telephony/call-manager.ts +803 -104
- package/server/telephony/call-types.ts +67 -1
- package/server/telephony/esl-client.ts +319 -0
- package/server/telephony/freeswitch-sync.ts +155 -0
- package/server/telephony/phone-utils.ts +63 -0
- package/server/telephony/telephony-store.ts +9 -8
- package/server/url-validator.ts +82 -0
- package/server/vault-markdown.ts +317 -0
- package/server/vault-migration.ts +121 -0
- package/server/vault-store.ts +466 -0
- package/server/vault-watcher.ts +59 -0
- package/server/vector-store.ts +210 -0
- package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
- package/server/voice-pipeline/greeting-cache.ts +200 -0
- package/server/voice-pipeline/manager.ts +249 -0
- package/server/voice-pipeline/pipeline.ts +335 -0
- package/server/voice-pipeline/providers/index.ts +47 -0
- package/server/voice-pipeline/providers/llm-internal.ts +527 -0
- package/server/voice-pipeline/providers/stt-google.ts +157 -0
- package/server/voice-pipeline/providers/tts-google.ts +126 -0
- package/server/voice-pipeline/types.ts +247 -0
- package/server/ws-bridge-types.ts +6 -1
- package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
- package/dist/assets/HelpPage-DMfkzERp.js +0 -1
- package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
- package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
- package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
- package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
- package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
- package/dist/assets/index-C8M_PUmX.css +0 -32
- package/dist/assets/sw-register-LSSpj6RU.js +0 -1
- package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ─── Content Engine Routes ──────────────────────────────────────────────────
|
|
2
|
+
// REST API for the Content Engine / Ad Creator system.
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
|
|
6
|
+
export function registerContentRoutes(api: Hono): void {
|
|
7
|
+
/** Analyze a website — extract brand identity, products, colors, tone */
|
|
8
|
+
api.post("/content/analyze", async (c) => {
|
|
9
|
+
try {
|
|
10
|
+
const body = await c.req.json();
|
|
11
|
+
const url = (body.url || "").trim();
|
|
12
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
13
|
+
|
|
14
|
+
const { analyzeWebsite } = await import("../content-intelligence/content-engine.js");
|
|
15
|
+
const intelligence = await analyzeWebsite(url);
|
|
16
|
+
return c.json(intelligence);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return c.json({ error: e instanceof Error ? e.message : "Analysis failed" }, 500);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Create a content strategy based on website analysis */
|
|
23
|
+
api.post("/content/strategy", async (c) => {
|
|
24
|
+
try {
|
|
25
|
+
const body = await c.req.json();
|
|
26
|
+
const url = (body.url || "").trim();
|
|
27
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
28
|
+
|
|
29
|
+
const platformsStr = (body.platforms as string | undefined) || "instagram,linkedin,facebook";
|
|
30
|
+
const platforms = Array.isArray(platformsStr)
|
|
31
|
+
? platformsStr
|
|
32
|
+
: platformsStr.split(",").map((p: string) => p.trim()).filter(Boolean);
|
|
33
|
+
|
|
34
|
+
const { analyzeWebsite, createContentStrategy } = await import("../content-intelligence/content-engine.js");
|
|
35
|
+
const intelligence = await analyzeWebsite(url);
|
|
36
|
+
const strategy = createContentStrategy(intelligence, platforms);
|
|
37
|
+
return c.json(strategy);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return c.json({ error: e instanceof Error ? e.message : "Strategy creation failed" }, 500);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Generate platform-optimized content pieces */
|
|
44
|
+
api.post("/content/generate", async (c) => {
|
|
45
|
+
try {
|
|
46
|
+
const body = await c.req.json();
|
|
47
|
+
const url = (body.url || "").trim();
|
|
48
|
+
const platform = (body.platform || "").trim();
|
|
49
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
50
|
+
if (!platform) return c.json({ error: "platform is required" }, 400);
|
|
51
|
+
|
|
52
|
+
const count = body.count || 5;
|
|
53
|
+
const journeyStage = body.journeyStage || undefined;
|
|
54
|
+
const styleProfileHandle =
|
|
55
|
+
typeof body.styleProfileHandle === "string" && body.styleProfileHandle.trim()
|
|
56
|
+
? body.styleProfileHandle.trim()
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
const { analyzeWebsite, createContentStrategy, generateSmartContent } = await import("../content-intelligence/content-engine.js");
|
|
60
|
+
const intelligence = await analyzeWebsite(url);
|
|
61
|
+
const strategy = createContentStrategy(intelligence, [platform]);
|
|
62
|
+
const pieces = await generateSmartContent({
|
|
63
|
+
intelligence,
|
|
64
|
+
strategy,
|
|
65
|
+
platform,
|
|
66
|
+
journeyStage,
|
|
67
|
+
count,
|
|
68
|
+
styleProfileHandle,
|
|
69
|
+
});
|
|
70
|
+
return c.json({ pieces, count: pieces.length });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return c.json({ error: e instanceof Error ? e.message : "Content generation failed" }, 500);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Generate ad creatives */
|
|
77
|
+
api.post("/content/ads", async (c) => {
|
|
78
|
+
try {
|
|
79
|
+
const body = await c.req.json();
|
|
80
|
+
const url = (body.url || "").trim();
|
|
81
|
+
const platform = (body.platform || "").trim();
|
|
82
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
83
|
+
if (!platform) return c.json({ error: "platform is required" }, 400);
|
|
84
|
+
|
|
85
|
+
const count = body.count || 3;
|
|
86
|
+
|
|
87
|
+
const { analyzeWebsite, generateAdCreatives } = await import("../content-intelligence/content-engine.js");
|
|
88
|
+
const intelligence = await analyzeWebsite(url);
|
|
89
|
+
const ads = await generateAdCreatives({ intelligence, platform, count });
|
|
90
|
+
return c.json({ ads, count: ads.length });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return c.json({ error: e instanceof Error ? e.message : "Ad generation failed" }, 500);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** Generate a complete multi-week content plan */
|
|
97
|
+
api.post("/content/plan", async (c) => {
|
|
98
|
+
try {
|
|
99
|
+
const body = await c.req.json();
|
|
100
|
+
const url = (body.url || "").trim();
|
|
101
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
102
|
+
|
|
103
|
+
const platformsStr = (body.platforms as string | undefined) || "instagram,linkedin,facebook";
|
|
104
|
+
const platforms = Array.isArray(platformsStr)
|
|
105
|
+
? platformsStr
|
|
106
|
+
: platformsStr.split(",").map((p: string) => p.trim()).filter(Boolean);
|
|
107
|
+
const weeks = body.weeks || 4;
|
|
108
|
+
|
|
109
|
+
const { generateContentPlan } = await import("../content-intelligence/content-engine.js");
|
|
110
|
+
const plan = await generateContentPlan({ url, platforms, weeks });
|
|
111
|
+
return c.json(plan);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return c.json({ error: e instanceof Error ? e.message : "Plan generation failed" }, 500);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ─── Email Routes ────────────────────────────────────────────────────────────
|
|
2
|
+
// REST API for multi-account email management (IMAP/SMTP).
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import * as email from "../email-service.js";
|
|
6
|
+
import type { EmailAccount } from "../email-service.js";
|
|
7
|
+
|
|
8
|
+
/** Strip auth.pass from an account object before returning it to the client. */
|
|
9
|
+
function sanitizeAccount(account: EmailAccount): Omit<EmailAccount, "auth"> & { auth: { user: string } } {
|
|
10
|
+
const { auth, ...rest } = account;
|
|
11
|
+
return { ...rest, auth: { user: auth.user } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerEmailRoutes(api: Hono): void {
|
|
15
|
+
// ─── Account Management ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
api.get("/assistant/email/accounts", (c) => {
|
|
18
|
+
const accounts = email.loadAccounts().map(sanitizeAccount);
|
|
19
|
+
return c.json({ accounts });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
api.post("/assistant/email/accounts", async (c) => {
|
|
23
|
+
const body = await c.req.json<{
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
imap: { host: string; port: number; secure: boolean };
|
|
27
|
+
smtp: { host: string; port: number; secure: boolean };
|
|
28
|
+
auth: { user: string; pass: string };
|
|
29
|
+
}>();
|
|
30
|
+
if (!body.name || !body.email || !body.imap || !body.smtp || !body.auth) {
|
|
31
|
+
return c.json({ error: "name, email, imap, smtp, and auth are required" }, 400);
|
|
32
|
+
}
|
|
33
|
+
const account = email.addAccount(body);
|
|
34
|
+
return c.json(sanitizeAccount(account));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
api.delete("/assistant/email/accounts/:id", (c) => {
|
|
38
|
+
const ok = email.removeAccount(c.req.param("id"));
|
|
39
|
+
return c.json({ ok });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
api.post("/assistant/email/accounts/:id/test", async (c) => {
|
|
43
|
+
const account = email.getAccount(c.req.param("id"));
|
|
44
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
45
|
+
try {
|
|
46
|
+
await email.listEmails(account, { limit: 1 });
|
|
47
|
+
return c.json({ ok: true, message: "IMAP connection successful" });
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
return c.json({ ok: false, error: err?.message || "connection failed" }, 500);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Unread Summary ─────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
api.get("/assistant/email/unread", async (c) => {
|
|
56
|
+
try {
|
|
57
|
+
const summary = await email.getUnreadSummary();
|
|
58
|
+
return c.json({ summary });
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
return c.json({ error: err?.message || "failed to get unread summary" }, 500);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── Per-Account Email Operations ───────────────────────────────────
|
|
65
|
+
|
|
66
|
+
api.get("/assistant/email/:accountId/messages", async (c) => {
|
|
67
|
+
const account = email.getAccount(c.req.param("accountId"));
|
|
68
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
69
|
+
|
|
70
|
+
const limit = c.req.query("limit") ? Number(c.req.query("limit")) : undefined;
|
|
71
|
+
const unseen = c.req.query("unseen") === "true" ? true : undefined;
|
|
72
|
+
const folder = c.req.query("folder") || undefined;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const messages = await email.listEmails(account, { limit, unseen, folder });
|
|
76
|
+
return c.json({ messages });
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
return c.json({ error: err?.message || "failed to list emails" }, 500);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
api.get("/assistant/email/:accountId/messages/:uid", async (c) => {
|
|
83
|
+
const account = email.getAccount(c.req.param("accountId"));
|
|
84
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
85
|
+
|
|
86
|
+
const uid = Number(c.req.param("uid"));
|
|
87
|
+
const folder = c.req.query("folder") || undefined;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const message = await email.readEmail(account, uid, folder);
|
|
91
|
+
if (!message) return c.json({ error: "message not found" }, 404);
|
|
92
|
+
return c.json({ message });
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
return c.json({ error: err?.message || "failed to read email" }, 500);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
api.get("/assistant/email/:accountId/search", async (c) => {
|
|
99
|
+
const account = email.getAccount(c.req.param("accountId"));
|
|
100
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
101
|
+
|
|
102
|
+
const q = c.req.query("q");
|
|
103
|
+
if (!q) return c.json({ error: "q query parameter is required" }, 400);
|
|
104
|
+
const limit = c.req.query("limit") ? Number(c.req.query("limit")) : undefined;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const messages = await email.searchEmails(account, q, limit);
|
|
108
|
+
return c.json({ messages });
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
return c.json({ error: err?.message || "search failed" }, 500);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
api.post("/assistant/email/:accountId/send", async (c) => {
|
|
115
|
+
const account = email.getAccount(c.req.param("accountId"));
|
|
116
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
117
|
+
|
|
118
|
+
const body = await c.req.json<{ to: string; subject: string; body: string }>();
|
|
119
|
+
if (!body.to || !body.subject || !body.body) {
|
|
120
|
+
return c.json({ error: "to, subject, and body are required" }, 400);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await email.sendEmail(account, body.to, body.subject, body.body);
|
|
125
|
+
return c.json(result);
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
return c.json({ error: err?.message || "failed to send email" }, 500);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
api.post("/assistant/email/:accountId/reply", async (c) => {
|
|
132
|
+
const account = email.getAccount(c.req.param("accountId"));
|
|
133
|
+
if (!account) return c.json({ error: "account not found" }, 404);
|
|
134
|
+
|
|
135
|
+
const body = await c.req.json<{ uid: number; body: string }>();
|
|
136
|
+
if (!body.uid || !body.body) {
|
|
137
|
+
return c.json({ error: "uid and body are required" }, 400);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const result = await email.replyToEmail(account, body.uid, body.body);
|
|
142
|
+
return c.json(result);
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
return c.json({ error: err?.message || "failed to reply" }, 500);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -65,7 +65,7 @@ export function registerEnvRoutes(
|
|
|
65
65
|
return c.json({ error: "Base Dockerfile not found at " + dockerfilePath }, 404);
|
|
66
66
|
}
|
|
67
67
|
try {
|
|
68
|
-
const log = containerManager.buildImage(dockerfilePath, "
|
|
68
|
+
const log = containerManager.buildImage(dockerfilePath, "heyhank:latest");
|
|
69
69
|
return c.json({ success: true, log });
|
|
70
70
|
} catch (e: unknown) {
|
|
71
71
|
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500);
|
|
@@ -73,8 +73,8 @@ export function registerEnvRoutes(
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
api.get("/docker/base-image", (c) => {
|
|
76
|
-
const exists = containerManager.imageExists("
|
|
77
|
-
return c.json({ exists, image: "
|
|
76
|
+
const exists = containerManager.imageExists("heyhank:latest");
|
|
77
|
+
return c.json({ exists, image: "heyhank:latest" });
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
api.get("/images/:tag/status", (c) => {
|
|
@@ -112,7 +112,7 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
112
112
|
const cwd = process.cwd();
|
|
113
113
|
// Only report cwd if the user launched heyhank from a real project directory
|
|
114
114
|
// (not from the package root or the home directory itself)
|
|
115
|
-
const packageRoot = process.env.__HEYHANK_PACKAGE_ROOT || process.env.__COMPANION_PACKAGE_ROOT
|
|
115
|
+
const packageRoot = process.env.__HEYHANK_PACKAGE_ROOT || process.env.__COMPANION_PACKAGE_ROOT /* legacy */;
|
|
116
116
|
const isProjectDir =
|
|
117
117
|
cwd !== home &&
|
|
118
118
|
(!packageRoot || !cwd.startsWith(packageRoot));
|
|
@@ -243,14 +243,15 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
243
243
|
const filePath = c.req.query("path");
|
|
244
244
|
if (!filePath) return c.json({ error: "path required" }, 400);
|
|
245
245
|
const base = c.req.query("base");
|
|
246
|
-
const absPath =
|
|
246
|
+
const absPath = guardPath(filePath, allowedBases());
|
|
247
|
+
if (!absPath) return c.json({ error: "Path outside allowed directories" }, 403);
|
|
247
248
|
try {
|
|
248
249
|
const repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
249
250
|
cwd: dirname(absPath),
|
|
250
251
|
encoding: "utf-8",
|
|
251
252
|
timeout: 5000,
|
|
252
253
|
}).trim();
|
|
253
|
-
const relPath = execSync(`git -C
|
|
254
|
+
const relPath = execSync(`git -C ${shellEscapeArg(repoRoot)} ls-files --full-name -- ${shellEscapeArg(absPath)}`, {
|
|
254
255
|
encoding: "utf-8",
|
|
255
256
|
timeout: 5000,
|
|
256
257
|
}).trim() || absPath;
|
|
@@ -261,7 +262,7 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
261
262
|
const diffBases = resolveBranchDiffBases(repoRoot);
|
|
262
263
|
for (const b of diffBases) {
|
|
263
264
|
try {
|
|
264
|
-
diff = execCaptureStdout(`git diff ${b} --
|
|
265
|
+
diff = execCaptureStdout(`git diff ${shellEscapeArg(b)} -- ${shellEscapeArg(relPath)}`, {
|
|
265
266
|
cwd: repoRoot,
|
|
266
267
|
encoding: "utf-8",
|
|
267
268
|
timeout: 5000,
|
|
@@ -273,7 +274,7 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
273
274
|
}
|
|
274
275
|
} else {
|
|
275
276
|
try {
|
|
276
|
-
diff = execCaptureStdout(`git diff HEAD --
|
|
277
|
+
diff = execCaptureStdout(`git diff HEAD -- ${shellEscapeArg(relPath)}`, {
|
|
277
278
|
cwd: repoRoot,
|
|
278
279
|
encoding: "utf-8",
|
|
279
280
|
timeout: 5000,
|
|
@@ -284,13 +285,13 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
if (!diff.trim()) {
|
|
287
|
-
const untracked = execSync(`git ls-files --others --exclude-standard --
|
|
288
|
+
const untracked = execSync(`git ls-files --others --exclude-standard -- ${shellEscapeArg(relPath)}`, {
|
|
288
289
|
cwd: repoRoot,
|
|
289
290
|
encoding: "utf-8",
|
|
290
291
|
timeout: 5000,
|
|
291
292
|
}).trim();
|
|
292
293
|
if (untracked) {
|
|
293
|
-
diff = execCaptureStdout(`git diff --no-index -- /dev/null
|
|
294
|
+
diff = execCaptureStdout(`git diff --no-index -- /dev/null ${shellEscapeArg(absPath)}`, {
|
|
294
295
|
cwd: repoRoot,
|
|
295
296
|
encoding: "utf-8",
|
|
296
297
|
timeout: 5000,
|
|
@@ -312,7 +313,8 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
312
313
|
const cwd = c.req.query("cwd");
|
|
313
314
|
if (!cwd) return c.json({ error: "cwd required" }, 400);
|
|
314
315
|
const base = c.req.query("base"); // "last-commit" | "default-branch" | undefined
|
|
315
|
-
const resolvedCwd =
|
|
316
|
+
const resolvedCwd = guardPath(cwd, allowedBases());
|
|
317
|
+
if (!resolvedCwd) return c.json({ error: "Path outside allowed directories" }, 403);
|
|
316
318
|
try {
|
|
317
319
|
const repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
318
320
|
cwd: resolvedCwd,
|
|
@@ -604,7 +606,8 @@ export function registerFsRoutes(api: Hono, opts?: { allowedBases?: string[] }):
|
|
|
604
606
|
if (base !== "CLAUDE.md") {
|
|
605
607
|
return c.json({ error: "Can only write CLAUDE.md files" }, 400);
|
|
606
608
|
}
|
|
607
|
-
const absPath =
|
|
609
|
+
const absPath = guardPath(filePath, allowedBases());
|
|
610
|
+
if (!absPath) return c.json({ error: "Path outside allowed directories" }, 403);
|
|
608
611
|
if (!absPath.endsWith("/CLAUDE.md") && !absPath.endsWith("/.claude/CLAUDE.md")) {
|
|
609
612
|
return c.json({ error: "Invalid CLAUDE.md path" }, 400);
|
|
610
613
|
}
|