@vellumai/assistant 0.4.37 → 0.4.41

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 (169) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/README.md +13 -13
  3. package/bun.lock +80 -24
  4. package/docs/architecture/integrations.md +126 -128
  5. package/docs/runbook-trusted-contacts.md +1 -1
  6. package/docs/trusted-contact-access.md +12 -12
  7. package/package.json +3 -1
  8. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
  9. package/src/__tests__/app-bundler.test.ts +209 -0
  10. package/src/__tests__/app-compiler.test.ts +279 -0
  11. package/src/__tests__/app-executors.test.ts +293 -483
  12. package/src/__tests__/app-migration.test.ts +148 -0
  13. package/src/__tests__/app-routes-csp.test.ts +202 -0
  14. package/src/__tests__/avatar-e2e.test.ts +452 -0
  15. package/src/__tests__/avatar-generator.test.ts +193 -0
  16. package/src/__tests__/avatar-router.test.ts +186 -0
  17. package/src/__tests__/browser-download-timeout.test.ts +28 -0
  18. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
  19. package/src/__tests__/call-domain.test.ts +3 -7
  20. package/src/__tests__/credential-security-e2e.test.ts +19 -12
  21. package/src/__tests__/credentials-cli.test.ts +30 -4
  22. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
  23. package/src/__tests__/handlers-slack-config.test.ts +0 -72
  24. package/src/__tests__/handlers-telegram-config.test.ts +19 -12
  25. package/src/__tests__/handlers-twitter-config.test.ts +105 -48
  26. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  27. package/src/__tests__/integration-status.test.ts +15 -5
  28. package/src/__tests__/integrations-cli.test.ts +1 -1
  29. package/src/__tests__/invite-redemption-service.test.ts +62 -7
  30. package/src/__tests__/ipc-snapshot.test.ts +0 -8
  31. package/src/__tests__/managed-avatar-client.test.ts +280 -0
  32. package/src/__tests__/mcp-cli.test.ts +3 -3
  33. package/src/__tests__/oauth-cli.test.ts +203 -0
  34. package/src/__tests__/relay-server.test.ts +3 -3
  35. package/src/__tests__/secret-onetime-send.test.ts +19 -12
  36. package/src/__tests__/secure-keys.test.ts +78 -0
  37. package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
  38. package/src/__tests__/slack-channel-config.test.ts +23 -16
  39. package/src/__tests__/slack-share-routes.test.ts +263 -0
  40. package/src/__tests__/sms-messaging-provider.test.ts +3 -1
  41. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
  42. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  43. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  44. package/src/__tests__/twilio-config.test.ts +15 -36
  45. package/src/__tests__/twilio-provider.test.ts +4 -0
  46. package/src/__tests__/twitter-auth-handler.test.ts +27 -14
  47. package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
  48. package/src/__tests__/twitter-cli-routing.test.ts +38 -53
  49. package/src/__tests__/twitter-oauth-client.test.ts +18 -47
  50. package/src/__tests__/voice-invite-redemption.test.ts +27 -3
  51. package/src/amazon/cart.ts +1 -1
  52. package/src/amazon/client.ts +89 -7
  53. package/src/approvals/guardian-request-resolvers.ts +2 -2
  54. package/src/bundler/app-bundler.ts +77 -32
  55. package/src/bundler/app-compiler.ts +195 -0
  56. package/src/bundler/manifest.ts +1 -1
  57. package/src/bundler/package-resolver.ts +185 -0
  58. package/src/calls/call-domain.ts +4 -14
  59. package/src/calls/relay-server.ts +2 -2
  60. package/src/calls/twilio-config.ts +5 -24
  61. package/src/calls/twilio-rest.ts +19 -5
  62. package/src/cli/amazon.ts +74 -249
  63. package/src/cli/audit.ts +2 -2
  64. package/src/cli/autonomy.ts +9 -9
  65. package/src/cli/channels.ts +5 -5
  66. package/src/cli/completions.ts +27 -27
  67. package/src/cli/config.ts +14 -14
  68. package/src/cli/contacts.ts +27 -27
  69. package/src/cli/credentials.ts +28 -28
  70. package/src/cli/dev.ts +2 -2
  71. package/src/cli/doctor.ts +2 -2
  72. package/src/cli/email.ts +82 -82
  73. package/src/cli/influencer.ts +13 -13
  74. package/src/cli/integrations.ts +19 -144
  75. package/src/cli/keys.ts +10 -10
  76. package/src/cli/map.ts +4 -4
  77. package/src/cli/mcp.ts +17 -17
  78. package/src/cli/memory.ts +18 -18
  79. package/src/cli/notifications.ts +13 -13
  80. package/src/cli/oauth.ts +77 -0
  81. package/src/cli/program.ts +2 -0
  82. package/src/cli/sequence.ts +27 -27
  83. package/src/cli/sessions.ts +12 -12
  84. package/src/cli/trust.ts +8 -8
  85. package/src/cli/twitter.ts +124 -70
  86. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  87. package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
  88. package/src/config/bundled-skills/amazon/SKILL.md +54 -54
  89. package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
  90. package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
  91. package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
  92. package/src/config/bundled-skills/contacts/SKILL.md +12 -12
  93. package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
  94. package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
  95. package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
  96. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
  97. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
  98. package/src/config/bundled-skills/influencer/SKILL.md +13 -13
  99. package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
  101. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  102. package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
  103. package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
  104. package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
  105. package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
  106. package/src/config/bundled-skills/twitter/SKILL.md +68 -44
  107. package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
  108. package/src/config/core-schema.ts +26 -0
  109. package/src/config/env.ts +4 -0
  110. package/src/config/feature-flag-registry.json +9 -1
  111. package/src/config/schema.ts +8 -0
  112. package/src/config/system-prompt.ts +6 -3
  113. package/src/config/templates/BOOTSTRAP.md +7 -5
  114. package/src/contacts/contacts-write.ts +5 -1
  115. package/src/daemon/handlers/apps.ts +31 -4
  116. package/src/daemon/handlers/config-ingress.ts +3 -3
  117. package/src/daemon/handlers/config-integrations.ts +120 -49
  118. package/src/daemon/handlers/config-slack-channel.ts +26 -7
  119. package/src/daemon/handlers/config-slack.ts +1 -54
  120. package/src/daemon/handlers/config-telegram.ts +28 -10
  121. package/src/daemon/handlers/config.ts +1 -4
  122. package/src/daemon/handlers/twitter-auth.ts +11 -4
  123. package/src/daemon/ipc-contract/apps.ts +0 -13
  124. package/src/daemon/ipc-contract-inventory.json +0 -2
  125. package/src/daemon/lifecycle.ts +8 -1
  126. package/src/daemon/session-messaging.ts +2 -2
  127. package/src/daemon/tool-side-effects.ts +30 -0
  128. package/src/email/providers/agentmail.ts +1 -1
  129. package/src/email/providers/index.ts +1 -1
  130. package/src/email/service.ts +1 -1
  131. package/src/gallery/default-gallery.ts +538 -0
  132. package/src/gallery/gallery-manifest.ts +5 -1
  133. package/src/influencer/client.ts +8 -6
  134. package/src/mcp/client.ts +1 -1
  135. package/src/media/avatar-router.ts +99 -0
  136. package/src/media/avatar-types.ts +60 -0
  137. package/src/media/managed-avatar-client.ts +189 -0
  138. package/src/memory/app-migration.ts +114 -0
  139. package/src/memory/app-store.ts +11 -0
  140. package/src/memory/qdrant-client.ts +1 -1
  141. package/src/messaging/providers/slack/client.ts +12 -2
  142. package/src/messaging/providers/sms/adapter.ts +6 -10
  143. package/src/migrations/data-layout.ts +8 -1
  144. package/src/oauth/token-persistence.ts +9 -6
  145. package/src/runtime/assistant-scope.ts +5 -0
  146. package/src/runtime/auth/route-policy.ts +4 -0
  147. package/src/runtime/channel-readiness-service.ts +9 -4
  148. package/src/runtime/gateway-internal-client.ts +11 -3
  149. package/src/runtime/http-server.ts +2 -0
  150. package/src/runtime/invite-redemption-service.ts +23 -13
  151. package/src/runtime/middleware/twilio-validation.ts +2 -2
  152. package/src/runtime/routes/app-routes.ts +131 -3
  153. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
  154. package/src/runtime/routes/integration-routes.ts +2 -2
  155. package/src/runtime/routes/slack-share-routes.ts +235 -0
  156. package/src/runtime/routes/twilio-routes.ts +47 -34
  157. package/src/schedule/integration-status.ts +2 -3
  158. package/src/security/token-manager.ts +11 -3
  159. package/src/tools/apps/executors.ts +116 -8
  160. package/src/tools/browser/browser-manager.ts +30 -2
  161. package/src/tools/browser/chrome-cdp.ts +31 -3
  162. package/src/tools/credentials/vault.ts +9 -7
  163. package/src/tools/executor.ts +4 -0
  164. package/src/tools/system/avatar-generator.ts +55 -34
  165. package/src/twitter/client.ts +1 -1
  166. package/src/twitter/oauth-client.ts +31 -43
  167. package/src/twitter/router.ts +25 -23
  168. package/src/util/platform.ts +5 -0
  169. package/src/slack/slack-webhook.ts +0 -66
@@ -1,11 +1,10 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
3
4
 
4
- import { getConfig } from "../../config/loader.js";
5
- import {
6
- generateImage,
7
- mapGeminiError,
8
- } from "../../media/gemini-image-service.js";
5
+ import { routedGenerateAvatar } from "../../media/avatar-router.js";
6
+ import { ManagedAvatarError } from "../../media/avatar-types.js";
7
+ import { mapGeminiError } from "../../media/gemini-image-service.js";
9
8
  import { RiskLevel } from "../../permissions/types.js";
10
9
  import type { ToolDefinition } from "../../providers/types.js";
11
10
  import { getLogger } from "../../util/logger.js";
@@ -66,21 +65,8 @@ export const setAvatarTool: Tool = {
66
65
  };
67
66
  }
68
67
 
69
- const config = getConfig();
70
- const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
71
- if (!apiKey) {
72
- return {
73
- content:
74
- "No Gemini API key configured. Please add your Gemini API key in Settings → Models & Services, or set the GEMINI_API_KEY environment variable.",
75
- isError: true,
76
- };
77
- }
78
-
79
68
  try {
80
- log.info(
81
- { description: description.trim() },
82
- "Generating avatar via Gemini",
83
- );
69
+ log.info({ description: description.trim() }, "Generating avatar");
84
70
 
85
71
  const prompt =
86
72
  `Create an avatar image based on this description: ${description.trim()}\n\n` +
@@ -89,29 +75,31 @@ export const setAvatarTool: Tool = {
89
75
  "Circular or rounded composition filling the canvas. " +
90
76
  "Subtle background color (not white or transparent).";
91
77
 
92
- const result = await generateImage(apiKey, {
93
- prompt,
94
- mode: "generate",
95
- model: config.imageGenModel,
96
- });
97
-
98
- if (result.images.length === 0) {
78
+ const result = await routedGenerateAvatar(prompt);
79
+ if (!result.imageBase64) {
99
80
  return {
100
- content: "Error: Gemini returned no image data. Please try again.",
81
+ content: "Error: No image data returned. Please try again.",
101
82
  isError: true,
102
83
  };
103
84
  }
104
-
105
- const image = result.images[0];
106
- const pngBuffer = Buffer.from(image.dataBase64, "base64");
85
+ const pngBuffer = Buffer.from(result.imageBase64, "base64");
107
86
 
108
87
  const avatarPath = getAvatarPath();
109
88
  const avatarDir = dirname(avatarPath);
110
89
 
90
+ const tmpPath = `${avatarPath}.${randomUUID()}.tmp`;
111
91
  mkdirSync(avatarDir, { recursive: true });
112
- writeFileSync(avatarPath, pngBuffer);
92
+ writeFileSync(tmpPath, pngBuffer);
93
+ renameSync(tmpPath, avatarPath);
113
94
 
114
- log.info({ avatarPath }, "Avatar saved successfully");
95
+ log.info(
96
+ {
97
+ avatarPath,
98
+ pathUsed: result.pathUsed,
99
+ correlationId: result.correlationId,
100
+ },
101
+ "Avatar saved successfully",
102
+ );
115
103
 
116
104
  // Side-effect hook in tool-side-effects.ts broadcasts avatar_updated to all clients.
117
105
 
@@ -120,9 +108,42 @@ export const setAvatarTool: Tool = {
120
108
  isError: false,
121
109
  };
122
110
  } catch (error) {
111
+ if (error instanceof ManagedAvatarError) {
112
+ if (error.statusCode === 429) {
113
+ log.warn(
114
+ { correlationId: error.correlationId },
115
+ "Avatar generation rate limited",
116
+ );
117
+ return {
118
+ content:
119
+ "Avatar generation is rate limited. Please wait and try again.",
120
+ isError: true,
121
+ };
122
+ }
123
+ if (error.statusCode === 503) {
124
+ log.warn(
125
+ { correlationId: error.correlationId },
126
+ "Avatar generation service unavailable",
127
+ );
128
+ return {
129
+ content: "Avatar generation service is temporarily unavailable.",
130
+ isError: true,
131
+ };
132
+ }
133
+ const detail =
134
+ error.message || "Avatar generation failed. Please try again.";
135
+ log.error(
136
+ { error: detail, correlationId: error.correlationId },
137
+ "Managed avatar generation failed",
138
+ );
139
+ return {
140
+ content: `Avatar generation failed: ${detail}`,
141
+ isError: true,
142
+ };
143
+ }
144
+
123
145
  const message = mapGeminiError(error);
124
146
  log.error({ error: message }, "Avatar generation failed");
125
-
126
147
  return {
127
148
  content: `Avatar generation failed: ${message}`,
128
149
  isError: true,
@@ -93,7 +93,7 @@ async function findTwitterTab(): Promise<string> {
93
93
  const res = await fetch(`${CDP_BASE}/json/list`).catch(() => null);
94
94
  if (!res?.ok) {
95
95
  throw new SessionExpiredError(
96
- "Chrome CDP not available. Run `vellum twitter refresh` first.",
96
+ "Chrome CDP not available. Run `assistant twitter refresh` first.",
97
97
  );
98
98
  }
99
99
  const targets = (await res.json()) as Array<{
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * OAuth-backed Twitter API client.
3
3
  *
4
- * Uses stored OAuth2 Bearer tokens (via the token manager) to execute
4
+ * Accepts an OAuth2 Bearer token as a parameter and uses it to execute
5
5
  * Twitter API v2 operations directly, without requiring a browser session.
6
6
  * Currently supports post and reply; all other operations fall back to the
7
7
  * browser-based CDP client.
8
8
  */
9
9
 
10
- import { getSecureKey } from "../security/secure-keys.js";
11
- import { withValidToken } from "../security/token-manager.js";
12
-
13
10
  const TWITTER_API_BASE = "https://api.x.com/2";
14
- const SERVICE = "integration:twitter";
15
11
 
16
12
  /** Operations that the OAuth client can handle natively. */
17
13
  const SUPPORTED_OPERATIONS = new Set(["post", "reply"]);
@@ -45,55 +41,47 @@ export class UnsupportedOAuthOperationError extends Error {
45
41
  /**
46
42
  * Post a tweet (or reply) using OAuth2 Bearer token authentication.
47
43
  *
48
- * The token manager handles refresh transparently if the stored token
49
- * is expired it will be refreshed before (or after a 401) calling the API.
44
+ * The caller is responsible for providing a valid token (e.g. via
45
+ * `assistant oauth token twitter`).
50
46
  */
51
47
  export async function oauthPostTweet(
52
48
  text: string,
53
- opts?: { inReplyToTweetId?: string },
49
+ opts: { inReplyToTweetId?: string; oauthToken: string },
54
50
  ): Promise<OAuthPostResult> {
55
- return withValidToken(SERVICE, async (token) => {
56
- const body: Record<string, unknown> = { text };
57
- if (opts?.inReplyToTweetId) {
58
- body.reply = { in_reply_to_tweet_id: opts.inReplyToTweetId };
59
- }
51
+ const body: Record<string, unknown> = { text };
52
+ if (opts.inReplyToTweetId) {
53
+ body.reply = { in_reply_to_tweet_id: opts.inReplyToTweetId };
54
+ }
60
55
 
61
- const res = await fetch(`${TWITTER_API_BASE}/tweets`, {
62
- method: "POST",
63
- headers: {
64
- Authorization: `Bearer ${token}`,
65
- "Content-Type": "application/json",
66
- },
67
- body: JSON.stringify(body),
68
- });
56
+ const res = await fetch(`${TWITTER_API_BASE}/tweets`, {
57
+ method: "POST",
58
+ headers: {
59
+ Authorization: `Bearer ${opts.oauthToken}`,
60
+ "Content-Type": "application/json",
61
+ },
62
+ body: JSON.stringify(body),
63
+ });
69
64
 
70
- if (!res.ok) {
71
- const errorBody = await res.text().catch(() => "");
72
- const err = new Error(
73
- `Twitter API error (${res.status}): ${errorBody.slice(0, 500)}`,
74
- );
75
- // Attach status so the token manager's 401-retry logic can detect it.
76
- (err as Error & { status: number }).status = res.status;
77
- throw err;
78
- }
65
+ if (!res.ok) {
66
+ const errorBody = await res.text().catch(() => "");
67
+ throw new Error(
68
+ `Twitter API error (${res.status}): ${errorBody.slice(0, 500)}`,
69
+ );
70
+ }
79
71
 
80
- const json = (await res.json()) as { data: { id: string; text: string } };
81
- return {
82
- tweetId: json.data.id,
83
- text: json.data.text,
84
- };
85
- });
72
+ const json = (await res.json()) as { data: { id: string; text: string } };
73
+ return {
74
+ tweetId: json.data.id,
75
+ text: json.data.text,
76
+ };
86
77
  }
87
78
 
88
79
  /**
89
- * Check whether OAuth credentials are available for the Twitter integration.
90
- * Returns true if an access token has been stored (the token manager will
91
- * handle refresh if it's expired).
80
+ * Check whether an OAuth token is available.
81
+ * When the caller provides a token string, OAuth is available.
92
82
  */
93
- export function oauthIsAvailable(): boolean {
94
- return (
95
- getSecureKey("credential:integration:twitter:access_token") !== undefined
96
- );
83
+ export function oauthIsAvailable(oauthToken: string | undefined): boolean {
84
+ return oauthToken != null && oauthToken.length > 0;
97
85
  }
98
86
 
99
87
  /**
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Strategy router for Twitter operations.
3
- * Selects OAuth or browser path based on persisted preference and capability.
3
+ * Selects OAuth or browser path based on the caller-provided strategy.
4
4
  */
5
5
 
6
- import { loadRawConfig } from "../config/loader.js";
7
6
  import type { PostTweetResult } from "./client.js";
8
7
  import {
9
8
  postTweet as browserPostTweet,
@@ -29,30 +28,23 @@ export interface RoutedError {
29
28
  alternativeSetupHint?: string;
30
29
  }
31
30
 
32
- function getPreferredStrategy(): TwitterStrategy {
33
- try {
34
- const raw = loadRawConfig();
35
- const strategy = raw.twitterOperationStrategy as string | undefined;
36
- if (strategy === "oauth" || strategy === "browser") return strategy;
37
- } catch {
38
- /* fall through */
39
- }
40
- return "auto";
41
- }
42
-
43
31
  export async function routedPostTweet(
44
32
  text: string,
45
- opts?: { inReplyToTweetId?: string },
33
+ opts: {
34
+ inReplyToTweetId?: string;
35
+ strategy: TwitterStrategy;
36
+ oauthToken?: string;
37
+ },
46
38
  ): Promise<RoutedResult<PostTweetResult>> {
47
- const strategy = getPreferredStrategy();
48
- const operation = opts?.inReplyToTweetId ? "reply" : "post";
39
+ const strategy = opts.strategy;
40
+ const operation = opts.inReplyToTweetId ? "reply" : "post";
49
41
 
50
42
  if (strategy === "oauth") {
51
43
  // User explicitly wants OAuth
52
- if (!oauthIsAvailable()) {
44
+ if (!oauthIsAvailable(opts.oauthToken)) {
53
45
  throw Object.assign(
54
46
  new Error(
55
- "OAuth is not configured. Provide your X developer credentials here in the chat to set up OAuth, or switch to browser strategy: `vellum x strategy set browser`.",
47
+ "OAuth is not configured. Provide your X developer credentials here in the chat to set up OAuth, or switch to browser strategy: `assistant config set twitter.operationStrategy browser`.",
56
48
  ),
57
49
  {
58
50
  pathUsed: "oauth" as const,
@@ -60,7 +52,10 @@ export async function routedPostTweet(
60
52
  },
61
53
  );
62
54
  }
63
- const result = await oauthPostTweet(text, opts);
55
+ const result = await oauthPostTweet(text, {
56
+ inReplyToTweetId: opts.inReplyToTweetId,
57
+ oauthToken: opts.oauthToken!,
58
+ });
64
59
  return {
65
60
  result: {
66
61
  tweetId: result.tweetId,
@@ -74,7 +69,9 @@ export async function routedPostTweet(
74
69
  if (strategy === "browser") {
75
70
  // User explicitly wants browser
76
71
  try {
77
- const result = await browserPostTweet(text, opts);
72
+ const result = await browserPostTweet(text, {
73
+ inReplyToTweetId: opts.inReplyToTweetId,
74
+ });
78
75
  return { result, pathUsed: "browser" };
79
76
  } catch (err) {
80
77
  if (err instanceof SessionExpiredError) {
@@ -89,9 +86,12 @@ export async function routedPostTweet(
89
86
 
90
87
  // auto strategy: try OAuth first if available and supported, fallback to browser
91
88
  let oauthError: Error | undefined;
92
- if (oauthIsAvailable() && oauthSupportsOperation(operation)) {
89
+ if (oauthIsAvailable(opts.oauthToken) && oauthSupportsOperation(operation)) {
93
90
  try {
94
- const result = await oauthPostTweet(text, opts);
91
+ const result = await oauthPostTweet(text, {
92
+ inReplyToTweetId: opts.inReplyToTweetId,
93
+ oauthToken: opts.oauthToken!,
94
+ });
95
95
  return {
96
96
  result: {
97
97
  tweetId: result.tweetId,
@@ -108,7 +108,9 @@ export async function routedPostTweet(
108
108
 
109
109
  // Fallback to browser
110
110
  try {
111
- const result = await browserPostTweet(text, opts);
111
+ const result = await browserPostTweet(text, {
112
+ inReplyToTweetId: opts.inReplyToTweetId,
113
+ });
112
114
  return { result, pathUsed: "browser" };
113
115
  } catch (err) {
114
116
  if (err instanceof SessionExpiredError) {
@@ -85,6 +85,11 @@ export function readLockfile(): Record<string, unknown> | null {
85
85
  * inbound call path resolves phone numbers to config keys (typically
86
86
  * "self"). This function maps any known lockfile assistant ID to "self"
87
87
  * so both sides use a consistent DB key.
88
+ *
89
+ * Multi-instance safety: each daemon process runs with a scoped
90
+ * BASE_DATA_DIR, so readLockfile() only sees the lockfile for this
91
+ * instance. The mapping to "self" is correct because each daemon is
92
+ * single-tenant — it only manages its own instance's data.
88
93
  */
89
94
  export function normalizeAssistantId(assistantId: string): string {
90
95
  if (assistantId === "self") return "self";
@@ -1,66 +0,0 @@
1
- import { ProviderError } from "../util/errors.js";
2
- import { getLogger } from "../util/logger.js";
3
-
4
- const log = getLogger("slack-webhook");
5
-
6
- /**
7
- * Post a rich Block Kit message to a Slack Incoming Webhook URL.
8
- *
9
- * Uses the Block Kit format so the message renders nicely in Slack with
10
- * a header, description section, and context footer.
11
- */
12
- export async function postToSlackWebhook(
13
- webhookUrl: string,
14
- appName: string,
15
- appDescription: string,
16
- appIcon: string,
17
- ): Promise<void> {
18
- const blocks = [
19
- {
20
- type: "header",
21
- text: {
22
- type: "plain_text",
23
- text: `${appIcon} ${appName}`,
24
- emoji: true,
25
- },
26
- },
27
- {
28
- type: "section",
29
- text: {
30
- type: "mrkdwn",
31
- text: appDescription || "_No description_",
32
- },
33
- },
34
- {
35
- type: "context",
36
- elements: [
37
- {
38
- type: "mrkdwn",
39
- text: `Shared from Vellum Assistant`,
40
- },
41
- ],
42
- },
43
- ];
44
-
45
- const payload = {
46
- blocks,
47
- text: `${appIcon} ${appName}: ${appDescription || "No description"}`,
48
- };
49
-
50
- log.info({ appName }, "Posting app to Slack webhook");
51
-
52
- const response = await fetch(webhookUrl, {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify(payload),
56
- });
57
-
58
- if (!response.ok) {
59
- const body = await response.text();
60
- throw new ProviderError(
61
- `Slack webhook returned ${response.status}: ${body}`,
62
- "slack",
63
- response.status,
64
- );
65
- }
66
- }