@vellumai/assistant 0.4.50 → 0.4.51
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/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +19 -18
- package/src/__tests__/credential-vault.test.ts +17 -17
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +121 -32
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/oauth-cli.test.ts +286 -60
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +243 -11
- package/src/__tests__/relay-server.test.ts +9 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +71 -16
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +58 -9
- package/src/calls/call-domain.ts +3 -4
- package/src/calls/relay-server.ts +1 -1
- package/src/calls/twilio-config.ts +4 -3
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/oauth/apps.ts +40 -11
- package/src/cli/commands/oauth/connections.ts +66 -30
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +3 -3
- package/src/cli.ts +16 -12
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/loader.ts +6 -42
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/daemon/config-watcher.ts +8 -13
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/message-types/sessions.ts +0 -42
- package/src/daemon/server.ts +0 -6
- package/src/daemon/session-slash.ts +3 -5
- package/src/email/providers/index.ts +2 -2
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/conversation-queries.ts +3 -80
- package/src/memory/db-init.ts +4 -0
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +1 -1
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +32 -24
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +83 -17
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +208 -8
- package/src/oauth/token-persistence.ts +20 -13
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +71 -29
- package/src/runtime/invite-service.ts +40 -22
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +47 -10
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/workspace-routes.ts +7 -3
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +19 -16
- package/src/security/token-manager.ts +13 -6
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/tools/credentials/vault.ts +22 -5
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +2 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
|
@@ -16,7 +16,7 @@ import { getConnectionByProvider } from "../../oauth/oauth-store.js";
|
|
|
16
16
|
import { credentialKey } from "../../security/credential-key.js";
|
|
17
17
|
import {
|
|
18
18
|
deleteSecureKeyAsync,
|
|
19
|
-
|
|
19
|
+
getSecureKeyAsync,
|
|
20
20
|
setSecureKeyAsync,
|
|
21
21
|
} from "../../security/secure-keys.js";
|
|
22
22
|
import { getTelegramBotUsername } from "../../telegram/bot-username.js";
|
|
@@ -65,11 +65,13 @@ export type TelegramConfigResult = Omit<TelegramConfigResponse, "type">;
|
|
|
65
65
|
|
|
66
66
|
// -- Extracted business logic functions --
|
|
67
67
|
|
|
68
|
-
export function getTelegramConfig(): TelegramConfigResult {
|
|
69
|
-
const hasBotToken = !!
|
|
70
|
-
|
|
68
|
+
export async function getTelegramConfig(): Promise<TelegramConfigResult> {
|
|
69
|
+
const hasBotToken = !!(await getSecureKeyAsync(
|
|
70
|
+
credentialKey("telegram", "bot_token"),
|
|
71
|
+
));
|
|
72
|
+
const hasWebhookSecret = !!(await getSecureKeyAsync(
|
|
71
73
|
credentialKey("telegram", "webhook_secret"),
|
|
72
|
-
);
|
|
74
|
+
));
|
|
73
75
|
const conn = getConnectionByProvider("telegram");
|
|
74
76
|
const connected = !!(conn && conn.status === "active");
|
|
75
77
|
const botUsername = getTelegramBotUsername();
|
|
@@ -89,7 +91,8 @@ export async function setTelegramConfig(
|
|
|
89
91
|
// Track provenance so we only rollback tokens that were freshly provided.
|
|
90
92
|
const isNewToken = !!botToken;
|
|
91
93
|
const resolvedToken =
|
|
92
|
-
botToken ||
|
|
94
|
+
botToken ||
|
|
95
|
+
(await getSecureKeyAsync(credentialKey("telegram", "bot_token")));
|
|
93
96
|
if (!resolvedToken) {
|
|
94
97
|
return {
|
|
95
98
|
success: false,
|
|
@@ -166,9 +169,9 @@ export async function setTelegramConfig(
|
|
|
166
169
|
invalidateConfigCache();
|
|
167
170
|
|
|
168
171
|
// Ensure webhook secret exists (generate if missing)
|
|
169
|
-
let hasWebhookSecret = !!
|
|
172
|
+
let hasWebhookSecret = !!(await getSecureKeyAsync(
|
|
170
173
|
credentialKey("telegram", "webhook_secret"),
|
|
171
|
-
);
|
|
174
|
+
));
|
|
172
175
|
if (!hasWebhookSecret) {
|
|
173
176
|
const { randomUUID } = await import("node:crypto");
|
|
174
177
|
const webhookSecret = randomUUID();
|
|
@@ -241,7 +244,9 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
|
|
|
241
244
|
// The gateway reconcile short-circuits when credentials are absent,
|
|
242
245
|
// so we must call the Telegram API directly while the token is still
|
|
243
246
|
// available.
|
|
244
|
-
const botToken =
|
|
247
|
+
const botToken = await getSecureKeyAsync(
|
|
248
|
+
credentialKey("telegram", "bot_token"),
|
|
249
|
+
);
|
|
245
250
|
if (botToken) {
|
|
246
251
|
try {
|
|
247
252
|
await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`);
|
|
@@ -260,10 +265,12 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
|
|
|
260
265
|
|
|
261
266
|
if (r1 === "error" || r2 === "error") {
|
|
262
267
|
// Check each key individually so partial deletions report accurate status.
|
|
263
|
-
const hasBotToken = !!
|
|
264
|
-
|
|
268
|
+
const hasBotToken = !!(await getSecureKeyAsync(
|
|
269
|
+
credentialKey("telegram", "bot_token"),
|
|
270
|
+
));
|
|
271
|
+
const hasWebhookSecret = !!(await getSecureKeyAsync(
|
|
265
272
|
credentialKey("telegram", "webhook_secret"),
|
|
266
|
-
);
|
|
273
|
+
));
|
|
267
274
|
return {
|
|
268
275
|
success: false,
|
|
269
276
|
hasBotToken,
|
|
@@ -297,7 +304,9 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
|
|
|
297
304
|
export async function setTelegramCommands(
|
|
298
305
|
commands?: Array<{ command: string; description: string }>,
|
|
299
306
|
): Promise<TelegramConfigResult> {
|
|
300
|
-
const storedToken =
|
|
307
|
+
const storedToken = await getSecureKeyAsync(
|
|
308
|
+
credentialKey("telegram", "bot_token"),
|
|
309
|
+
);
|
|
301
310
|
if (!storedToken) {
|
|
302
311
|
return {
|
|
303
312
|
success: false,
|
|
@@ -398,7 +407,7 @@ export async function handleTelegramConfig(
|
|
|
398
407
|
let result: TelegramConfigResult;
|
|
399
408
|
|
|
400
409
|
if (msg.action === "get") {
|
|
401
|
-
result = getTelegramConfig();
|
|
410
|
+
result = await getTelegramConfig();
|
|
402
411
|
} else if (msg.action === "set") {
|
|
403
412
|
result = await setTelegramConfig(msg.botToken);
|
|
404
413
|
} else if (msg.action === "clear") {
|
|
@@ -1,324 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getAttachmentsForMessage,
|
|
3
|
-
getFilePathForAttachment,
|
|
4
|
-
setAttachmentThumbnail,
|
|
5
|
-
} from "../../memory/attachments-store.js";
|
|
6
1
|
import { getMessageById } from "../../memory/conversation-crud.js";
|
|
7
2
|
import {
|
|
8
|
-
getMessagesPaginated,
|
|
9
3
|
listConversations,
|
|
10
4
|
searchConversations,
|
|
11
5
|
} from "../../memory/conversation-queries.js";
|
|
12
|
-
import {
|
|
13
|
-
import { truncate } from "../../util/truncate.js";
|
|
14
|
-
import type {
|
|
15
|
-
ConversationSearchRequest,
|
|
16
|
-
HistoryRequest,
|
|
17
|
-
MessageContentRequest,
|
|
18
|
-
UserMessageAttachment,
|
|
19
|
-
} from "../message-protocol.js";
|
|
20
|
-
import { generateVideoThumbnail } from "../video-thumbnail.js";
|
|
21
|
-
import {
|
|
22
|
-
type HandlerContext,
|
|
23
|
-
type HistorySurface,
|
|
24
|
-
type HistoryToolCall,
|
|
25
|
-
log,
|
|
26
|
-
type ParsedHistoryMessage,
|
|
27
|
-
renderHistoryContent,
|
|
28
|
-
} from "./shared.js";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* In light mode, strip heavy payloads (e.g. full HTML) from surface data
|
|
32
|
-
* but preserve the fields the client needs to parse and render the surface.
|
|
33
|
-
*/
|
|
34
|
-
function lightModeSurfaceData(s: HistorySurface): Record<string, unknown> {
|
|
35
|
-
switch (s.surfaceType) {
|
|
36
|
-
case "dynamic_page":
|
|
37
|
-
return {
|
|
38
|
-
...(s.data.preview ? { preview: s.data.preview } : {}),
|
|
39
|
-
...(s.data.appId ? { appId: s.data.appId } : {}),
|
|
40
|
-
};
|
|
41
|
-
case "card":
|
|
42
|
-
return {
|
|
43
|
-
...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
|
|
44
|
-
...(typeof s.data.body === "string" ? { body: s.data.body } : {}),
|
|
45
|
-
...(typeof s.data.template === "string"
|
|
46
|
-
? { template: s.data.template }
|
|
47
|
-
: {}),
|
|
48
|
-
...(s.data.templateData ? { templateData: s.data.templateData } : {}),
|
|
49
|
-
};
|
|
50
|
-
case "document_preview":
|
|
51
|
-
return {
|
|
52
|
-
...(typeof s.data.surfaceId === "string"
|
|
53
|
-
? { surfaceId: s.data.surfaceId }
|
|
54
|
-
: {}),
|
|
55
|
-
...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
|
|
56
|
-
...(typeof s.data.content === "string"
|
|
57
|
-
? { content: s.data.content }
|
|
58
|
-
: {}),
|
|
59
|
-
};
|
|
60
|
-
default:
|
|
61
|
-
// For other types (list, table, form, confirmation, etc.),
|
|
62
|
-
// preserve the full data — these are generally small.
|
|
63
|
-
return s.data;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function handleHistoryRequest(
|
|
68
|
-
msg: HistoryRequest,
|
|
69
|
-
ctx: HandlerContext,
|
|
70
|
-
): void {
|
|
71
|
-
// No limit means return all messages.
|
|
72
|
-
const limit = msg.limit;
|
|
73
|
-
|
|
74
|
-
// Resolve include flags: explicit flags override mode, mode provides defaults.
|
|
75
|
-
// Default mode is 'light' when no mode and no include flags are specified.
|
|
76
|
-
const isFullMode = msg.mode === "full";
|
|
77
|
-
const includeAttachments = msg.includeAttachments ?? isFullMode;
|
|
78
|
-
const includeToolImages = msg.includeToolImages ?? isFullMode;
|
|
79
|
-
const includeSurfaceData = msg.includeSurfaceData ?? isFullMode;
|
|
80
|
-
|
|
81
|
-
const { messages: dbMessages, hasMore } = getMessagesPaginated(
|
|
82
|
-
msg.sessionId,
|
|
83
|
-
limit,
|
|
84
|
-
msg.beforeTimestamp,
|
|
85
|
-
msg.beforeMessageId,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const parsed: ParsedHistoryMessage[] = dbMessages.map((m) => {
|
|
89
|
-
let text = "";
|
|
90
|
-
let toolCalls: HistoryToolCall[] = [];
|
|
91
|
-
let toolCallsBeforeText = false;
|
|
92
|
-
let textSegments: string[] = [];
|
|
93
|
-
let contentOrder: string[] = [];
|
|
94
|
-
let surfaces: HistorySurface[] = [];
|
|
95
|
-
try {
|
|
96
|
-
const content = JSON.parse(m.content);
|
|
97
|
-
const rendered = renderHistoryContent(content);
|
|
98
|
-
text = rendered.text;
|
|
99
|
-
toolCalls = rendered.toolCalls;
|
|
100
|
-
toolCallsBeforeText = rendered.toolCallsBeforeText;
|
|
101
|
-
textSegments = rendered.textSegments;
|
|
102
|
-
contentOrder = rendered.contentOrder;
|
|
103
|
-
surfaces = rendered.surfaces;
|
|
104
|
-
if (m.role === "assistant" && toolCalls.length > 0) {
|
|
105
|
-
log.info(
|
|
106
|
-
{
|
|
107
|
-
messageId: m.id,
|
|
108
|
-
toolCallCount: toolCalls.length,
|
|
109
|
-
text: truncate(text, 100, ""),
|
|
110
|
-
},
|
|
111
|
-
"History message with tool calls",
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
} catch (err) {
|
|
115
|
-
log.debug(
|
|
116
|
-
{ err, messageId: m.id },
|
|
117
|
-
"Failed to parse message content as JSON, using raw text",
|
|
118
|
-
);
|
|
119
|
-
text = m.content;
|
|
120
|
-
textSegments = text ? [text] : [];
|
|
121
|
-
contentOrder = text ? ["text:0"] : [];
|
|
122
|
-
surfaces = [];
|
|
123
|
-
}
|
|
124
|
-
let subagentNotification: ParsedHistoryMessage["subagentNotification"];
|
|
125
|
-
if (m.metadata) {
|
|
126
|
-
try {
|
|
127
|
-
subagentNotification = (
|
|
128
|
-
JSON.parse(m.metadata) as {
|
|
129
|
-
subagentNotification?: ParsedHistoryMessage["subagentNotification"];
|
|
130
|
-
}
|
|
131
|
-
).subagentNotification;
|
|
132
|
-
} catch (err) {
|
|
133
|
-
log.debug(
|
|
134
|
-
{ err, messageId: m.id },
|
|
135
|
-
"Failed to parse message metadata as JSON, ignoring",
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
id: m.id,
|
|
141
|
-
role: m.role,
|
|
142
|
-
text,
|
|
143
|
-
timestamp: m.createdAt,
|
|
144
|
-
toolCalls,
|
|
145
|
-
toolCallsBeforeText,
|
|
146
|
-
textSegments,
|
|
147
|
-
contentOrder,
|
|
148
|
-
surfaces,
|
|
149
|
-
...(subagentNotification ? { subagentNotification } : {}),
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const historyMessages = parsed.map((m) => {
|
|
154
|
-
let attachments: UserMessageAttachment[] | undefined;
|
|
155
|
-
if (m.role === "assistant" && m.id) {
|
|
156
|
-
const linked = getAttachmentsForMessage(m.id);
|
|
157
|
-
if (linked.length > 0) {
|
|
158
|
-
if (includeAttachments) {
|
|
159
|
-
// Full attachment data: same behavior as before
|
|
160
|
-
const MAX_INLINE_B64_SIZE = 512 * 1024;
|
|
161
|
-
attachments = linked.map((a) => {
|
|
162
|
-
const isFileBacked = !a.dataBase64;
|
|
163
|
-
const omit =
|
|
164
|
-
isFileBacked ||
|
|
165
|
-
(a.mimeType.startsWith("video/") &&
|
|
166
|
-
a.dataBase64.length > MAX_INLINE_B64_SIZE);
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
a.mimeType.startsWith("video/") &&
|
|
170
|
-
!a.thumbnailBase64 &&
|
|
171
|
-
a.dataBase64
|
|
172
|
-
) {
|
|
173
|
-
const attachmentId = a.id;
|
|
174
|
-
const base64 = a.dataBase64;
|
|
175
|
-
silentlyWithLog(
|
|
176
|
-
generateVideoThumbnail(base64).then((thumb) => {
|
|
177
|
-
if (thumb) setAttachmentThumbnail(attachmentId, thumb);
|
|
178
|
-
}),
|
|
179
|
-
"video thumbnail generation",
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const fp = getFilePathForAttachment(a.id);
|
|
184
|
-
return {
|
|
185
|
-
id: a.id,
|
|
186
|
-
filename: a.originalFilename,
|
|
187
|
-
mimeType: a.mimeType,
|
|
188
|
-
data: omit ? "" : a.dataBase64,
|
|
189
|
-
...(omit ? { sizeBytes: a.sizeBytes } : {}),
|
|
190
|
-
...(a.thumbnailBase64
|
|
191
|
-
? { thumbnailData: a.thumbnailBase64 }
|
|
192
|
-
: {}),
|
|
193
|
-
...(fp ? { filePath: fp } : {}),
|
|
194
|
-
};
|
|
195
|
-
});
|
|
196
|
-
} else {
|
|
197
|
-
// Light mode: metadata only, strip base64 data
|
|
198
|
-
attachments = linked.map((a) => {
|
|
199
|
-
const fp = getFilePathForAttachment(a.id);
|
|
200
|
-
return {
|
|
201
|
-
id: a.id,
|
|
202
|
-
filename: a.originalFilename,
|
|
203
|
-
mimeType: a.mimeType,
|
|
204
|
-
data: "",
|
|
205
|
-
sizeBytes: a.sizeBytes,
|
|
206
|
-
...(a.thumbnailBase64
|
|
207
|
-
? { thumbnailData: a.thumbnailBase64 }
|
|
208
|
-
: {}),
|
|
209
|
-
...(fp ? { filePath: fp } : {}),
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// In light mode, strip imageData from tool calls
|
|
217
|
-
const filteredToolCalls =
|
|
218
|
-
m.toolCalls.length > 0
|
|
219
|
-
? includeToolImages
|
|
220
|
-
? m.toolCalls
|
|
221
|
-
: m.toolCalls.map((tc) => {
|
|
222
|
-
if (tc.imageData) {
|
|
223
|
-
const { imageData: _, ...rest } = tc;
|
|
224
|
-
return rest;
|
|
225
|
-
}
|
|
226
|
-
return tc;
|
|
227
|
-
})
|
|
228
|
-
: m.toolCalls;
|
|
229
|
-
|
|
230
|
-
// In light mode, strip heavy payloads but keep essential fields so
|
|
231
|
-
// the client can still parse and render surfaces (e.g. card title/body,
|
|
232
|
-
// dynamic_page preview, document_preview metadata).
|
|
233
|
-
const filteredSurfaces =
|
|
234
|
-
m.surfaces.length > 0
|
|
235
|
-
? includeSurfaceData
|
|
236
|
-
? m.surfaces
|
|
237
|
-
: m.surfaces.map((s) => ({
|
|
238
|
-
surfaceId: s.surfaceId,
|
|
239
|
-
surfaceType: s.surfaceType,
|
|
240
|
-
title: s.title,
|
|
241
|
-
data: lightModeSurfaceData(s),
|
|
242
|
-
...(s.actions ? { actions: s.actions } : {}),
|
|
243
|
-
...(s.display ? { display: s.display } : {}),
|
|
244
|
-
}))
|
|
245
|
-
: m.surfaces;
|
|
246
|
-
|
|
247
|
-
// Apply text truncation when maxTextChars is set
|
|
248
|
-
let wasTruncated = false;
|
|
249
|
-
let textWasTruncated = false;
|
|
250
|
-
let text = m.text;
|
|
251
|
-
if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
|
|
252
|
-
text = text.slice(0, msg.maxTextChars) + " \u2026 [truncated]";
|
|
253
|
-
wasTruncated = true;
|
|
254
|
-
textWasTruncated = true;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Apply tool result truncation when maxToolResultChars is set
|
|
258
|
-
const truncatedToolCalls =
|
|
259
|
-
msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
|
|
260
|
-
? filteredToolCalls.map((tc) => {
|
|
261
|
-
if (
|
|
262
|
-
tc.result !== undefined &&
|
|
263
|
-
tc.result.length > msg.maxToolResultChars!
|
|
264
|
-
) {
|
|
265
|
-
wasTruncated = true;
|
|
266
|
-
return {
|
|
267
|
-
...tc,
|
|
268
|
-
result:
|
|
269
|
-
tc.result.slice(0, msg.maxToolResultChars!) +
|
|
270
|
-
" \u2026 [truncated]",
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return tc;
|
|
274
|
-
})
|
|
275
|
-
: filteredToolCalls;
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
...(m.id ? { id: m.id } : {}),
|
|
279
|
-
role: m.role,
|
|
280
|
-
text,
|
|
281
|
-
timestamp: m.timestamp,
|
|
282
|
-
...(truncatedToolCalls.length > 0
|
|
283
|
-
? {
|
|
284
|
-
toolCalls: truncatedToolCalls,
|
|
285
|
-
toolCallsBeforeText: m.toolCallsBeforeText,
|
|
286
|
-
}
|
|
287
|
-
: {}),
|
|
288
|
-
...(attachments ? { attachments } : {}),
|
|
289
|
-
...(!textWasTruncated && m.textSegments.length > 0
|
|
290
|
-
? { textSegments: m.textSegments }
|
|
291
|
-
: {}),
|
|
292
|
-
...(!textWasTruncated && m.contentOrder.length > 0
|
|
293
|
-
? { contentOrder: m.contentOrder }
|
|
294
|
-
: {}),
|
|
295
|
-
...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
|
|
296
|
-
...(m.subagentNotification
|
|
297
|
-
? { subagentNotification: m.subagentNotification }
|
|
298
|
-
: {}),
|
|
299
|
-
...(wasTruncated ? { wasTruncated: true } : {}),
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
const oldestTimestamp =
|
|
304
|
-
historyMessages.length > 0 ? historyMessages[0].timestamp : undefined;
|
|
305
|
-
// Provide the oldest message ID as a tie-breaker cursor so clients can
|
|
306
|
-
// paginate without skipping same-millisecond messages at page boundaries.
|
|
307
|
-
const oldestMessageId =
|
|
308
|
-
historyMessages.length > 0 ? historyMessages[0].id : undefined;
|
|
309
|
-
|
|
310
|
-
ctx.send({
|
|
311
|
-
type: "history_response",
|
|
312
|
-
sessionId: msg.sessionId,
|
|
313
|
-
messages: historyMessages,
|
|
314
|
-
hasMore,
|
|
315
|
-
...(oldestTimestamp !== undefined ? { oldestTimestamp } : {}),
|
|
316
|
-
...(oldestMessageId ? { oldestMessageId } : {}),
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Surfaces are now included directly in the history_response message (in the surfaces array),
|
|
320
|
-
// so we no longer emit separate ui_surface_show messages during history loading.
|
|
321
|
-
}
|
|
6
|
+
import { renderHistoryContent } from "./shared.js";
|
|
322
7
|
|
|
323
8
|
// ---------------------------------------------------------------------------
|
|
324
9
|
// Shared business logic (transport-agnostic)
|
|
@@ -400,45 +85,3 @@ export function getMessageContent(
|
|
|
400
85
|
...(toolCalls ? { toolCalls } : {}),
|
|
401
86
|
};
|
|
402
87
|
}
|
|
403
|
-
|
|
404
|
-
// ---------------------------------------------------------------------------
|
|
405
|
-
// HTTP handlers (delegate to shared logic)
|
|
406
|
-
// ---------------------------------------------------------------------------
|
|
407
|
-
|
|
408
|
-
export function handleConversationSearch(
|
|
409
|
-
msg: ConversationSearchRequest,
|
|
410
|
-
ctx: HandlerContext,
|
|
411
|
-
): void {
|
|
412
|
-
const results = performConversationSearch({
|
|
413
|
-
query: msg.query,
|
|
414
|
-
limit: msg.limit,
|
|
415
|
-
maxMessagesPerConversation: msg.maxMessagesPerConversation,
|
|
416
|
-
});
|
|
417
|
-
ctx.send({
|
|
418
|
-
type: "conversation_search_response",
|
|
419
|
-
query: msg.query,
|
|
420
|
-
results,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function handleMessageContentRequest(
|
|
425
|
-
msg: MessageContentRequest,
|
|
426
|
-
ctx: HandlerContext,
|
|
427
|
-
): void {
|
|
428
|
-
const result = getMessageContent(msg.messageId, msg.sessionId);
|
|
429
|
-
if (!result) {
|
|
430
|
-
ctx.send({
|
|
431
|
-
type: "error",
|
|
432
|
-
message: `Message ${msg.messageId} not found in session ${msg.sessionId}`,
|
|
433
|
-
});
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
ctx.send({
|
|
438
|
-
type: "message_content_response",
|
|
439
|
-
sessionId: msg.sessionId,
|
|
440
|
-
messageId: msg.messageId,
|
|
441
|
-
...(result.text !== undefined ? { text: result.text } : {}),
|
|
442
|
-
...(result.toolCalls ? { toolCalls: result.toolCalls } : {}),
|
|
443
|
-
});
|
|
444
|
-
}
|
|
@@ -176,22 +176,6 @@ function clampAttachmentText(text: string): string {
|
|
|
176
176
|
return `${text.slice(0, HISTORY_ATTACHMENT_TEXT_LIMIT)}<truncated />`;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function renderImageBlockForHistory(block: Record<string, unknown>): string {
|
|
180
|
-
const source = isRecord(block.source) ? block.source : null;
|
|
181
|
-
const mediaType =
|
|
182
|
-
source && typeof source.media_type === "string"
|
|
183
|
-
? source.media_type
|
|
184
|
-
: "image/*";
|
|
185
|
-
const sizeBytes =
|
|
186
|
-
source && typeof source.data === "string"
|
|
187
|
-
? estimateBase64Bytes(source.data)
|
|
188
|
-
: 0;
|
|
189
|
-
if (sizeBytes <= 0) {
|
|
190
|
-
return `[Image attachment] ${mediaType}`;
|
|
191
|
-
}
|
|
192
|
-
return `[Image attachment] ${mediaType}, ${formatBytes(sizeBytes)}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
179
|
function renderFileBlockForHistory(block: Record<string, unknown>): string {
|
|
196
180
|
const source = isRecord(block.source) ? block.source : null;
|
|
197
181
|
const mediaType =
|
|
@@ -328,7 +312,9 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
328
312
|
continue;
|
|
329
313
|
}
|
|
330
314
|
if (block.type === "image") {
|
|
331
|
-
|
|
315
|
+
// Image data is sent as a separate attachment — skip the placeholder
|
|
316
|
+
// text so the client doesn't render both "[Image attachment]" and the
|
|
317
|
+
// actual image thumbnail.
|
|
332
318
|
continue;
|
|
333
319
|
}
|
|
334
320
|
if (block.type === "tool_use") {
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -167,7 +167,14 @@ export async function runDaemon(): Promise<void> {
|
|
|
167
167
|
// Backfill oauth_connection rows for manual-token providers (Telegram,
|
|
168
168
|
// Slack channel) that already have keychain credentials from before the
|
|
169
169
|
// oauth_connection migration. Safe to call on every startup.
|
|
170
|
-
|
|
170
|
+
try {
|
|
171
|
+
await backfillManualTokenConnections();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
log.warn(
|
|
174
|
+
{ err },
|
|
175
|
+
"Manual-token connection backfill failed — continuing startup",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
171
178
|
log.info("Daemon startup: DB initialized");
|
|
172
179
|
|
|
173
180
|
// Expire any pending canonical guardian requests left over from before
|
|
@@ -85,35 +85,6 @@ export interface ImageGenModelSetRequest {
|
|
|
85
85
|
model: string;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export interface HistoryRequest {
|
|
89
|
-
type: "history_request";
|
|
90
|
-
sessionId: string;
|
|
91
|
-
/** Max messages to return. When omitted, all messages are returned (unlimited). */
|
|
92
|
-
limit?: number;
|
|
93
|
-
/** Pagination cursor: return messages with timestamp before this value. */
|
|
94
|
-
beforeTimestamp?: number;
|
|
95
|
-
/** Pagination cursor tie-breaker: exclude this message ID when beforeTimestamp matches. */
|
|
96
|
-
beforeMessageId?: string;
|
|
97
|
-
/** Include attachment base64 data. Defaults to false in light mode. */
|
|
98
|
-
includeAttachments?: boolean;
|
|
99
|
-
/** Include tool screenshot base64 data. Defaults to false in light mode. */
|
|
100
|
-
includeToolImages?: boolean;
|
|
101
|
-
/** Include surface HTML payloads. Defaults to false in light mode. */
|
|
102
|
-
includeSurfaceData?: boolean;
|
|
103
|
-
/** Shorthand: 'light' = all include flags false (default), 'full' = all include flags true. */
|
|
104
|
-
mode?: "light" | "full";
|
|
105
|
-
/** Truncate message text fields beyond this character limit. When omitted, full text is returned. */
|
|
106
|
-
maxTextChars?: number;
|
|
107
|
-
/** Truncate tool result strings beyond this character limit. When omitted, full results are returned. */
|
|
108
|
-
maxToolResultChars?: number;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface MessageContentRequest {
|
|
112
|
-
type: "message_content_request";
|
|
113
|
-
sessionId: string;
|
|
114
|
-
messageId: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
88
|
export interface MessageContentResponse {
|
|
118
89
|
type: "message_content_response";
|
|
119
90
|
sessionId: string;
|
|
@@ -145,16 +116,6 @@ export interface SessionsClearRequest {
|
|
|
145
116
|
type: "sessions_clear";
|
|
146
117
|
}
|
|
147
118
|
|
|
148
|
-
export interface ConversationSearchRequest {
|
|
149
|
-
type: "conversation_search";
|
|
150
|
-
/** The search query string. */
|
|
151
|
-
query: string;
|
|
152
|
-
/** Maximum number of conversations to return. Defaults to 20. */
|
|
153
|
-
limit?: number;
|
|
154
|
-
/** Maximum number of matching messages to return per conversation. Defaults to 3. */
|
|
155
|
-
maxMessagesPerConversation?: number;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
119
|
export interface ReorderThreadsRequest {
|
|
159
120
|
type: "reorder_threads";
|
|
160
121
|
updates: Array<{
|
|
@@ -431,7 +392,6 @@ export type _SessionsClientMessages =
|
|
|
431
392
|
| ModelGetRequest
|
|
432
393
|
| ModelSetRequest
|
|
433
394
|
| ImageGenModelSetRequest
|
|
434
|
-
| HistoryRequest
|
|
435
395
|
| UndoRequest
|
|
436
396
|
| RegenerateRequest
|
|
437
397
|
| UsageRequest
|
|
@@ -440,8 +400,6 @@ export type _SessionsClientMessages =
|
|
|
440
400
|
| SessionSwitchRequest
|
|
441
401
|
| SessionRenameRequest
|
|
442
402
|
| SessionsClearRequest
|
|
443
|
-
| ConversationSearchRequest
|
|
444
|
-
| MessageContentRequest
|
|
445
403
|
| ReorderThreadsRequest;
|
|
446
404
|
|
|
447
405
|
export type _SessionsServerMessages =
|
package/src/daemon/server.ts
CHANGED
|
@@ -58,7 +58,6 @@ import type { SkillOperationContext } from "./handlers/skills.js";
|
|
|
58
58
|
import { HostBashProxy } from "./host-bash-proxy.js";
|
|
59
59
|
import { HostCuProxy } from "./host-cu-proxy.js";
|
|
60
60
|
import { HostFileProxy } from "./host-file-proxy.js";
|
|
61
|
-
import { reloadMcpServers } from "./mcp-reload-service.js";
|
|
62
61
|
import type { ServerMessage } from "./message-protocol.js";
|
|
63
62
|
import {
|
|
64
63
|
DEFAULT_MEMORY_POLICY,
|
|
@@ -393,11 +392,6 @@ export class DaemonServer {
|
|
|
393
392
|
this.configWatcher.start(
|
|
394
393
|
() => this.evictSessionsForReload(),
|
|
395
394
|
() => this.broadcastIdentityChanged(),
|
|
396
|
-
() => {
|
|
397
|
-
reloadMcpServers().catch((err: unknown) => {
|
|
398
|
-
log.error({ err }, "MCP reload triggered by config change failed");
|
|
399
|
-
});
|
|
400
|
-
},
|
|
401
395
|
);
|
|
402
396
|
|
|
403
397
|
// Broadcast contacts_changed to all clients when any contact mutation occurs.
|
|
@@ -166,7 +166,7 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
|
|
|
166
166
|
if (provider !== "ollama" && !config.apiKeys[provider]) {
|
|
167
167
|
return {
|
|
168
168
|
kind: "unknown",
|
|
169
|
-
message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`
|
|
169
|
+
message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`keys set ${provider} <your-key>\``,
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -216,9 +216,7 @@ function resolveModelList(): SlashResolution {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
lines.push("\n✓ = API key configured, ✗ = not configured");
|
|
219
|
-
lines.push(
|
|
220
|
-
"\nTip: Configure a provider with `config set apiKeys.<provider> <key>`",
|
|
221
|
-
);
|
|
219
|
+
lines.push("\nTip: Configure a provider with `keys set <provider> <key>`");
|
|
222
220
|
|
|
223
221
|
return {
|
|
224
222
|
kind: "unknown",
|
|
@@ -286,7 +284,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
|
|
|
286
284
|
const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
|
|
287
285
|
return {
|
|
288
286
|
kind: "unknown",
|
|
289
|
-
message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`
|
|
287
|
+
message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`keys set anthropic <your-key>\``,
|
|
290
288
|
};
|
|
291
289
|
}
|
|
292
290
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { getNestedValue, loadRawConfig } from "../../config/loader.js";
|
|
8
8
|
import { credentialKey } from "../../security/credential-key.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
10
10
|
import { ConfigError } from "../../util/errors.js";
|
|
11
11
|
import type { EmailProvider } from "../provider.js";
|
|
12
12
|
|
|
@@ -47,7 +47,7 @@ export async function createProvider(
|
|
|
47
47
|
const candidates = PROVIDER_KEY_MAP.agentmail;
|
|
48
48
|
let apiKey: string | undefined;
|
|
49
49
|
for (const account of candidates) {
|
|
50
|
-
apiKey =
|
|
50
|
+
apiKey = await getSecureKeyAsync(account);
|
|
51
51
|
if (apiKey) break;
|
|
52
52
|
}
|
|
53
53
|
if (!apiKey) {
|
|
@@ -32,7 +32,7 @@ export async function generateAvatar(
|
|
|
32
32
|
|
|
33
33
|
if (!credentials) {
|
|
34
34
|
throw new ConfigError(
|
|
35
|
-
"Gemini API key is not configured. Set it via `
|
|
35
|
+
"Gemini API key is not configured. Set it via `keys set gemini <key>` or the GEMINI_API_KEY environment variable.",
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|