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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -10
  3. package/bin/cli.ts +7 -7
  4. package/bin/ctl.ts +42 -42
  5. package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-B-AAmsMK.js} +3 -3
  6. package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
  7. package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
  8. package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
  9. package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
  10. package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
  11. package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
  12. package/dist/assets/MediaPage-C48HTTrt.js +1 -0
  13. package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
  14. package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
  15. package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
  16. package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
  17. package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
  18. package/dist/assets/RunsPage-B9UOyO79.js +1 -0
  19. package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
  20. package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
  21. package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
  22. package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
  23. package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
  24. package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
  25. package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
  26. package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
  27. package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
  28. package/dist/assets/index-BkjSoVgn.css +32 -0
  29. package/dist/assets/sw-register-C7NOHtIu.js +1 -0
  30. package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
  31. package/dist/index.html +2 -2
  32. package/dist/sw.js +1 -1
  33. package/package.json +6 -1
  34. package/server/agent-executor.ts +37 -2
  35. package/server/agent-store.ts +3 -3
  36. package/server/agent-types.ts +11 -0
  37. package/server/assistant-store.ts +232 -6
  38. package/server/auth-manager.ts +9 -0
  39. package/server/cache-headers.ts +1 -1
  40. package/server/calendar-service.ts +10 -0
  41. package/server/ceo/document-store.ts +129 -0
  42. package/server/ceo/finance-store.ts +343 -0
  43. package/server/ceo/kpi-store.ts +208 -0
  44. package/server/ceo/memory-import.ts +277 -0
  45. package/server/ceo/news-store.ts +208 -0
  46. package/server/ceo/template-store.ts +134 -0
  47. package/server/ceo/time-tracking-store.ts +227 -0
  48. package/server/claude-auth-monitor.ts +128 -0
  49. package/server/claude-code-worker.ts +86 -0
  50. package/server/claude-session-discovery.ts +74 -1
  51. package/server/cli-launcher.ts +32 -10
  52. package/server/codex-adapter.ts +2 -2
  53. package/server/codex-ws-proxy.cjs +1 -1
  54. package/server/container-manager.ts +4 -4
  55. package/server/content-intelligence/content-engine.ts +1112 -0
  56. package/server/content-intelligence/platform-knowledge.ts +870 -0
  57. package/server/cron-store.ts +3 -3
  58. package/server/embedding-service.ts +49 -0
  59. package/server/event-bus-types.ts +13 -0
  60. package/server/federation/node-store.ts +5 -4
  61. package/server/fs-utils.ts +28 -1
  62. package/server/hank-notifications-store.ts +91 -0
  63. package/server/hank-tool-executor.ts +1835 -0
  64. package/server/hank-tools.ts +2107 -0
  65. package/server/image-pull-manager.ts +2 -2
  66. package/server/index.ts +25 -2
  67. package/server/llm-providers-streaming.ts +541 -0
  68. package/server/llm-providers.ts +12 -0
  69. package/server/marketplace.ts +249 -0
  70. package/server/mcp-registry.ts +158 -0
  71. package/server/memory-service.ts +296 -0
  72. package/server/obsidian-sync.ts +184 -0
  73. package/server/provider-manager.ts +5 -2
  74. package/server/provider-registry.ts +12 -0
  75. package/server/reminder-scheduler.ts +37 -1
  76. package/server/routes/agent-routes.ts +2 -1
  77. package/server/routes/assistant-routes.ts +198 -5
  78. package/server/routes/ceo-finance-kpi-routes.ts +167 -0
  79. package/server/routes/ceo-news-time-routes.ts +137 -0
  80. package/server/routes/ceo-routes.ts +99 -0
  81. package/server/routes/content-routes.ts +116 -0
  82. package/server/routes/email-routes.ts +147 -0
  83. package/server/routes/env-routes.ts +3 -3
  84. package/server/routes/fs-routes.ts +12 -9
  85. package/server/routes/hank-chat-routes.ts +592 -0
  86. package/server/routes/llm-routes.ts +12 -0
  87. package/server/routes/marketplace-routes.ts +63 -0
  88. package/server/routes/media-routes.ts +1 -1
  89. package/server/routes/memory-routes.ts +127 -0
  90. package/server/routes/platform-routes.ts +14 -675
  91. package/server/routes/sandbox-routes.ts +1 -1
  92. package/server/routes/settings-routes.ts +51 -1
  93. package/server/routes/socialmedia-routes.ts +152 -2
  94. package/server/routes/system-routes.ts +2 -2
  95. package/server/routes/team-routes.ts +71 -0
  96. package/server/routes/telephony-routes.ts +98 -18
  97. package/server/routes.ts +36 -9
  98. package/server/session-creation-service.ts +2 -2
  99. package/server/session-orchestrator.ts +54 -2
  100. package/server/session-types.ts +2 -0
  101. package/server/settings-manager.ts +50 -2
  102. package/server/skill-discovery.ts +68 -0
  103. package/server/socialmedia/adapters/browser-adapter.ts +179 -0
  104. package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
  105. package/server/socialmedia/manager.ts +234 -15
  106. package/server/socialmedia/store.ts +51 -1
  107. package/server/socialmedia/types.ts +35 -2
  108. package/server/socialview/browser-manager.ts +150 -0
  109. package/server/socialview/extractors.ts +1298 -0
  110. package/server/socialview/image-describe.ts +188 -0
  111. package/server/socialview/library.ts +119 -0
  112. package/server/socialview/poster.ts +276 -0
  113. package/server/socialview/routes.ts +371 -0
  114. package/server/socialview/style-analyzer.ts +187 -0
  115. package/server/socialview/style-profiles.ts +67 -0
  116. package/server/socialview/types.ts +166 -0
  117. package/server/socialview/vision.ts +127 -0
  118. package/server/socialview/vnc-manager.ts +110 -0
  119. package/server/style-injector.ts +135 -0
  120. package/server/team-service.ts +239 -0
  121. package/server/team-store.ts +75 -0
  122. package/server/team-types.ts +52 -0
  123. package/server/telephony/audio-bridge.ts +281 -35
  124. package/server/telephony/audio-recorder.ts +132 -0
  125. package/server/telephony/call-manager.ts +803 -104
  126. package/server/telephony/call-types.ts +67 -1
  127. package/server/telephony/esl-client.ts +319 -0
  128. package/server/telephony/freeswitch-sync.ts +155 -0
  129. package/server/telephony/phone-utils.ts +63 -0
  130. package/server/telephony/telephony-store.ts +9 -8
  131. package/server/url-validator.ts +82 -0
  132. package/server/vault-markdown.ts +317 -0
  133. package/server/vault-migration.ts +121 -0
  134. package/server/vault-store.ts +466 -0
  135. package/server/vault-watcher.ts +59 -0
  136. package/server/vector-store.ts +210 -0
  137. package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
  138. package/server/voice-pipeline/greeting-cache.ts +200 -0
  139. package/server/voice-pipeline/manager.ts +249 -0
  140. package/server/voice-pipeline/pipeline.ts +335 -0
  141. package/server/voice-pipeline/providers/index.ts +47 -0
  142. package/server/voice-pipeline/providers/llm-internal.ts +527 -0
  143. package/server/voice-pipeline/providers/stt-google.ts +157 -0
  144. package/server/voice-pipeline/providers/tts-google.ts +126 -0
  145. package/server/voice-pipeline/types.ts +247 -0
  146. package/server/ws-bridge-types.ts +6 -1
  147. package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
  148. package/dist/assets/HelpPage-DMfkzERp.js +0 -1
  149. package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
  150. package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
  151. package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
  152. package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
  153. package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
  154. package/dist/assets/index-C8M_PUmX.css +0 -32
  155. package/dist/assets/sw-register-LSSpj6RU.js +0 -1
  156. 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, "the-companion:latest");
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("the-companion:latest");
77
- return c.json({ exists, image: "the-companion:latest" });
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 = resolve(filePath);
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 "${repoRoot}" ls-files --full-name -- "${absPath}"`, {
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} -- "${relPath}"`, {
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 -- "${relPath}"`, {
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 -- "${relPath}"`, {
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 "${absPath}"`, {
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 = resolve(cwd);
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 = resolve(filePath);
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
  }