@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -143,7 +143,10 @@ export function upsertJournalMemoriesFromDisk(
143
143
 
144
144
  // Filter for .md files, excluding readme.md (case-insensitive)
145
145
  const mdFiles = files.filter(
146
- (f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
146
+ (f) =>
147
+ f.endsWith(".md") &&
148
+ !f.startsWith(".") &&
149
+ f.toLowerCase() !== "readme.md",
147
150
  );
148
151
 
149
152
  let upserted = 0;
@@ -178,7 +181,10 @@ export function upsertJournalMemoriesFromDisk(
178
181
  if (!statSync(subdirPath).isDirectory()) continue;
179
182
 
180
183
  const subFiles = readdirSync(subdirPath).filter(
181
- (f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
184
+ (f) =>
185
+ f.endsWith(".md") &&
186
+ !f.startsWith(".") &&
187
+ f.toLowerCase() !== "readme.md",
182
188
  );
183
189
 
184
190
  for (const filename of subFiles) {
@@ -0,0 +1,9 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+
3
+ export function migrateMessagesConversationCreatedAtIndex(
4
+ database: DrizzleDb,
5
+ ): void {
6
+ database.run(
7
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_messages_conversation_created_at ON messages(conversation_id, created_at)`,
8
+ );
9
+ }
@@ -0,0 +1,186 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+ import { withCrashRecovery } from "./validate-migration-state.js";
4
+
5
+ /**
6
+ * One-shot migration: strip the `integration:` prefix from provider_key
7
+ * values across all three OAuth tables (oauth_providers, oauth_apps,
8
+ * oauth_connections).
9
+ *
10
+ * Historically provider keys were stored as `integration:google`,
11
+ * `integration:slack`, etc. The codebase is moving to bare-name keys
12
+ * (`google`, `slack`) for simplicity. Providers that were already stored
13
+ * with bare names (e.g. `slack_channel`, `telegram`) are unaffected.
14
+ *
15
+ * If a bare-name key already exists (runtime seed data created it), the
16
+ * old `integration:` rows are orphaned — we delete them instead of
17
+ * renaming to avoid UNIQUE constraint violations.
18
+ *
19
+ * FK constraints require us to update child tables (oauth_apps,
20
+ * oauth_connections) before the parent (oauth_providers), or disable FKs.
21
+ * We disable FKs for safety and update all three tables atomically.
22
+ */
23
+ export function migrateStripIntegrationPrefixFromProviderKeys(
24
+ database: DrizzleDb,
25
+ ): void {
26
+ withCrashRecovery(
27
+ database,
28
+ "migration_strip_integration_prefix_from_provider_keys_v1",
29
+ () => {
30
+ const raw = getSqliteFrom(database);
31
+
32
+ raw.exec("PRAGMA foreign_keys = OFF");
33
+ try {
34
+ // Find all provider keys with the integration: prefix.
35
+ const rows = raw
36
+ .prepare(
37
+ /*sql*/ `SELECT provider_key FROM oauth_providers WHERE provider_key LIKE 'integration:%'`,
38
+ )
39
+ .all() as Array<{ provider_key: string }>;
40
+
41
+ for (const { provider_key: oldKey } of rows) {
42
+ const newKey = oldKey.replace(/^integration:/, "");
43
+
44
+ // Check if the bare-name key already exists (seed data may have created it).
45
+ const bareExists = raw
46
+ .prepare(
47
+ /*sql*/ `SELECT 1 FROM oauth_providers WHERE provider_key = ?`,
48
+ )
49
+ .get(newKey);
50
+
51
+ if (bareExists) {
52
+ // Bare-name provider already exists — delete the old prefixed rows
53
+ // to avoid UNIQUE constraint violations.
54
+ raw
55
+ .prepare(
56
+ /*sql*/ `DELETE FROM oauth_connections WHERE provider_key = ?`,
57
+ )
58
+ .run(oldKey);
59
+ raw
60
+ .prepare(/*sql*/ `DELETE FROM oauth_apps WHERE provider_key = ?`)
61
+ .run(oldKey);
62
+ raw
63
+ .prepare(
64
+ /*sql*/ `DELETE FROM oauth_providers WHERE provider_key = ?`,
65
+ )
66
+ .run(oldKey);
67
+ } else {
68
+ // Rename: update child tables first, then parent.
69
+ raw
70
+ .prepare(
71
+ /*sql*/ `UPDATE oauth_connections SET provider_key = ? WHERE provider_key = ?`,
72
+ )
73
+ .run(newKey, oldKey);
74
+ raw
75
+ .prepare(
76
+ /*sql*/ `UPDATE oauth_apps SET provider_key = ? WHERE provider_key = ?`,
77
+ )
78
+ .run(newKey, oldKey);
79
+ raw
80
+ .prepare(
81
+ /*sql*/ `UPDATE oauth_providers SET provider_key = ? WHERE provider_key = ?`,
82
+ )
83
+ .run(newKey, oldKey);
84
+ }
85
+ }
86
+
87
+ // Also update the watchers table — credential_service stores provider
88
+ // keys like "integration:google" that feed into resolveOAuthConnection().
89
+ raw
90
+ .prepare(
91
+ /*sql*/ `UPDATE watchers SET credential_service = REPLACE(credential_service, 'integration:', '') WHERE credential_service LIKE 'integration:%'`,
92
+ )
93
+ .run();
94
+ } finally {
95
+ raw.exec("PRAGMA foreign_keys = ON");
96
+ }
97
+ },
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Reverse: re-add the `integration:` prefix to provider keys that don't
103
+ * already have one and aren't known bare-name providers.
104
+ *
105
+ * This is a best-effort rollback — we prefix all keys that look like they
106
+ * were originally `integration:` prefixed. Known bare-name keys
107
+ * (`slack_channel`, `telegram`) are left as-is because they never had the
108
+ * prefix.
109
+ */
110
+ export function migrateStripIntegrationPrefixFromProviderKeysDown(
111
+ database: DrizzleDb,
112
+ ): void {
113
+ const raw = getSqliteFrom(database);
114
+
115
+ // Keys that were always bare — never had an integration: prefix.
116
+ const ALWAYS_BARE = new Set(["slack_channel", "telegram"]);
117
+
118
+ raw.exec("PRAGMA foreign_keys = OFF");
119
+ try {
120
+ const rows = raw
121
+ .prepare(
122
+ /*sql*/ `SELECT provider_key FROM oauth_providers WHERE provider_key NOT LIKE 'integration:%'`,
123
+ )
124
+ .all() as Array<{ provider_key: string }>;
125
+
126
+ for (const { provider_key: bareKey } of rows) {
127
+ if (ALWAYS_BARE.has(bareKey)) continue;
128
+
129
+ const prefixedKey = `integration:${bareKey}`;
130
+
131
+ // If the prefixed key already exists, delete the bare rows.
132
+ const prefixedExists = raw
133
+ .prepare(/*sql*/ `SELECT 1 FROM oauth_providers WHERE provider_key = ?`)
134
+ .get(prefixedKey);
135
+
136
+ if (prefixedExists) {
137
+ raw
138
+ .prepare(
139
+ /*sql*/ `DELETE FROM oauth_connections WHERE provider_key = ?`,
140
+ )
141
+ .run(bareKey);
142
+ raw
143
+ .prepare(/*sql*/ `DELETE FROM oauth_apps WHERE provider_key = ?`)
144
+ .run(bareKey);
145
+ raw
146
+ .prepare(/*sql*/ `DELETE FROM oauth_providers WHERE provider_key = ?`)
147
+ .run(bareKey);
148
+ } else {
149
+ raw
150
+ .prepare(
151
+ /*sql*/ `UPDATE oauth_connections SET provider_key = ? WHERE provider_key = ?`,
152
+ )
153
+ .run(prefixedKey, bareKey);
154
+ raw
155
+ .prepare(
156
+ /*sql*/ `UPDATE oauth_apps SET provider_key = ? WHERE provider_key = ?`,
157
+ )
158
+ .run(prefixedKey, bareKey);
159
+ raw
160
+ .prepare(
161
+ /*sql*/ `UPDATE oauth_providers SET provider_key = ? WHERE provider_key = ?`,
162
+ )
163
+ .run(prefixedKey, bareKey);
164
+ }
165
+ }
166
+
167
+ // Reverse the watchers table update — re-add the prefix for keys that
168
+ // aren't known bare-name providers.
169
+ const watcherRows = raw
170
+ .prepare(
171
+ /*sql*/ `SELECT DISTINCT credential_service FROM watchers WHERE credential_service NOT LIKE 'integration:%'`,
172
+ )
173
+ .all() as Array<{ credential_service: string }>;
174
+
175
+ for (const { credential_service: bareKey } of watcherRows) {
176
+ if (ALWAYS_BARE.has(bareKey)) continue;
177
+ raw
178
+ .prepare(
179
+ /*sql*/ `UPDATE watchers SET credential_service = ? WHERE credential_service = ?`,
180
+ )
181
+ .run(`integration:${bareKey}`, bareKey);
182
+ }
183
+ } finally {
184
+ raw.exec("PRAGMA foreign_keys = ON");
185
+ }
186
+ }
@@ -0,0 +1,29 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateOAuthProvidersBehaviorColumns(
5
+ database: DrizzleDb,
6
+ ): void {
7
+ const raw = getSqliteFrom(database);
8
+ const columns = [
9
+ "loopback_port INTEGER",
10
+ "injection_templates TEXT",
11
+ "setup_skill_id TEXT",
12
+ "app_type TEXT",
13
+ "setup_notes TEXT",
14
+ "identity_url TEXT",
15
+ "identity_method TEXT",
16
+ "identity_headers TEXT",
17
+ "identity_body TEXT",
18
+ "identity_response_paths TEXT",
19
+ "identity_format TEXT",
20
+ "identity_ok_field TEXT",
21
+ ];
22
+ for (const col of columns) {
23
+ try {
24
+ raw.exec(`ALTER TABLE oauth_providers ADD COLUMN ${col}`);
25
+ } catch {
26
+ // Column already exists — nothing to do.
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateDropSetupSkillIdColumn(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ try {
7
+ raw.exec(/*sql*/ `ALTER TABLE oauth_providers DROP COLUMN setup_skill_id`);
8
+ } catch {
9
+ // Column already dropped or doesn't exist — nothing to do.
10
+ }
11
+ }
@@ -134,6 +134,10 @@ export { migrateContactsUserFileColumn } from "./192-contacts-user-file-column.j
134
134
  export { migrateAddSourceTypeColumns } from "./193-add-source-type-columns.js";
135
135
  export { migrateCreateMemoryRecallLogs } from "./194-memory-recall-logs.js";
136
136
  export { migrateOAuthProvidersPingConfig } from "./195-oauth-providers-ping-config.js";
137
+ export { migrateMessagesConversationCreatedAtIndex } from "./196-messages-conversation-created-at-index.js";
138
+ export { migrateStripIntegrationPrefixFromProviderKeys } from "./196-strip-integration-prefix-from-provider-keys.js";
139
+ export { migrateOAuthProvidersBehaviorColumns } from "./197-oauth-providers-behavior-columns.js";
140
+ export { migrateDropSetupSkillIdColumn } from "./198-drop-setup-skill-id-column.js";
137
141
  export {
138
142
  MIGRATION_REGISTRY,
139
143
  type MigrationRegistryEntry,
@@ -38,6 +38,7 @@ import { migrateBackfillInlineAttachmentsToDiskDown } from "./180-backfill-inlin
38
38
  import { migrateRenameThreadStartersCheckpointsDown } from "./181-rename-thread-starters-checkpoints.js";
39
39
  import { migrateBackfillAudioAttachmentMimeTypesDown } from "./191-backfill-audio-attachment-mime-types.js";
40
40
  import { migrateAddSourceTypeColumnsDown } from "./193-add-source-type-columns.js";
41
+ import { migrateStripIntegrationPrefixFromProviderKeysDown } from "./196-strip-integration-prefix-from-provider-keys.js";
41
42
 
42
43
  export interface MigrationRegistryEntry {
43
44
  /** The checkpoint key written to memory_checkpoints on completion. */
@@ -333,6 +334,13 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
333
334
  "Add source_type and source_message_role columns to memory_items with backfill from verification_state and source messages",
334
335
  down: migrateAddSourceTypeColumnsDown,
335
336
  },
337
+ {
338
+ key: "migration_strip_integration_prefix_from_provider_keys_v1",
339
+ version: 38,
340
+ description:
341
+ "Strip integration: prefix from provider_key across oauth_providers, oauth_apps, and oauth_connections",
342
+ down: migrateStripIntegrationPrefixFromProviderKeysDown,
343
+ },
336
344
  ];
337
345
 
338
346
  export function getMaxMigrationVersion(): number {
@@ -27,6 +27,17 @@ export const oauthProviders = sqliteTable("oauth_providers", {
27
27
  dashboardUrl: text("dashboard_url"),
28
28
  clientIdPlaceholder: text("client_id_placeholder"),
29
29
  requiresClientSecret: integer("requires_client_secret").notNull().default(1),
30
+ loopbackPort: integer("loopback_port"),
31
+ injectionTemplates: text("injection_templates"),
32
+ appType: text("app_type"),
33
+ setupNotes: text("setup_notes"),
34
+ identityUrl: text("identity_url"),
35
+ identityMethod: text("identity_method"),
36
+ identityHeaders: text("identity_headers"),
37
+ identityBody: text("identity_body"),
38
+ identityResponsePaths: text("identity_response_paths"),
39
+ identityFormat: text("identity_format"),
40
+ identityOkField: text("identity_ok_field"),
30
41
  createdAt: integer("created_at").notNull(),
31
42
  updatedAt: integer("updated_at").notNull(),
32
43
  });
@@ -30,25 +30,23 @@ export interface MessagingProvider {
30
30
 
31
31
  // ── Universal operations (every platform must implement) ──────────
32
32
 
33
- testConnection(
34
- connectionOrToken: OAuthConnection | string,
35
- ): Promise<ConnectionInfo>;
33
+ testConnection(connection?: OAuthConnection): Promise<ConnectionInfo>;
36
34
  listConversations(
37
- connectionOrToken: OAuthConnection | string,
35
+ connection: OAuthConnection | undefined,
38
36
  options?: ListOptions,
39
37
  ): Promise<Conversation[]>;
40
38
  getHistory(
41
- connectionOrToken: OAuthConnection | string,
39
+ connection: OAuthConnection | undefined,
42
40
  conversationId: string,
43
41
  options?: HistoryOptions,
44
42
  ): Promise<Message[]>;
45
43
  search(
46
- connectionOrToken: OAuthConnection | string,
44
+ connection: OAuthConnection | undefined,
47
45
  query: string,
48
46
  options?: SearchOptions,
49
47
  ): Promise<SearchResult>;
50
48
  sendMessage(
51
- connectionOrToken: OAuthConnection | string,
49
+ connection: OAuthConnection | undefined,
52
50
  conversationId: string,
53
51
  text: string,
54
52
  options?: SendOptions,
@@ -57,26 +55,26 @@ export interface MessagingProvider {
57
55
  // ── Optional operations (platforms implement what they support) ───
58
56
 
59
57
  getThreadReplies?(
60
- connectionOrToken: OAuthConnection | string,
58
+ connection: OAuthConnection | undefined,
61
59
  conversationId: string,
62
60
  threadId: string,
63
61
  options?: HistoryOptions,
64
62
  ): Promise<Message[]>;
65
63
  markRead?(
66
- connectionOrToken: OAuthConnection | string,
64
+ connection: OAuthConnection | undefined,
67
65
  conversationId: string,
68
66
  messageId?: string,
69
67
  ): Promise<void>;
70
68
 
71
69
  /** Scan messages and group by sender for bulk cleanup (e.g. newsletter decluttering). */
72
70
  senderDigest?(
73
- connectionOrToken: OAuthConnection | string,
71
+ connection: OAuthConnection | undefined,
74
72
  query: string,
75
73
  options?: { maxMessages?: number; maxSenders?: number; pageToken?: string },
76
74
  ): Promise<SenderDigestResult>;
77
75
  /** Archive messages matching a search query. */
78
76
  archiveByQuery?(
79
- connectionOrToken: OAuthConnection | string,
77
+ connection: OAuthConnection | undefined,
80
78
  query: string,
81
79
  ): Promise<ArchiveResult>;
82
80
 
@@ -95,8 +93,11 @@ export interface MessagingProvider {
95
93
  * than the OAuth provider key). When present, getProviderConnection() calls
96
94
  * this instead of resolveOAuthConnection(), giving the provider full control
97
95
  * over credential lookup including fallback strategies.
96
+ *
97
+ * Returns an OAuthConnection if the provider uses OAuth, or undefined if
98
+ * the provider manages credentials internally (e.g. raw bot tokens).
98
99
  */
99
- resolveConnection?(account?: string): Promise<OAuthConnection | string>;
100
+ resolveConnection?(account?: string): Promise<OAuthConnection | undefined>;
100
101
 
101
102
  /** Platform-specific capabilities for tool routing (e.g. 'reactions', 'threads', 'labels'). */
102
103
  capabilities: Set<string>;
@@ -24,6 +24,17 @@ import type {
24
24
  import * as gmail from "./client.js";
25
25
  import type { GmailMessage, GmailMessagePart } from "./types.js";
26
26
 
27
+ function requireConnection(
28
+ connection: OAuthConnection | undefined,
29
+ ): OAuthConnection {
30
+ if (!connection) {
31
+ throw new Error(
32
+ "Gmail requires an OAuth connection — is the account connected?",
33
+ );
34
+ }
35
+ return connection;
36
+ }
37
+
27
38
  function extractHeader(msg: GmailMessage, name: string): string {
28
39
  const lower = name.toLowerCase();
29
40
  return (
@@ -86,7 +97,7 @@ function mapGmailMessage(msg: GmailMessage): Message {
86
97
  export const gmailMessagingProvider: MessagingProvider = {
87
98
  id: "gmail",
88
99
  displayName: "Gmail",
89
- credentialService: "integration:google",
100
+ credentialService: "google",
90
101
  capabilities: new Set([
91
102
  "threads",
92
103
  "labels",
@@ -95,11 +106,9 @@ export const gmailMessagingProvider: MessagingProvider = {
95
106
  "unsubscribe",
96
107
  ]),
97
108
 
98
- async testConnection(
99
- connectionOrToken: OAuthConnection | string,
100
- ): Promise<ConnectionInfo> {
101
- const connection = connectionOrToken as OAuthConnection;
102
- const profile = await gmail.getProfile(connection);
109
+ async testConnection(connection?: OAuthConnection): Promise<ConnectionInfo> {
110
+ const conn = requireConnection(connection);
111
+ const profile = await gmail.getProfile(conn);
103
112
  return {
104
113
  connected: true,
105
114
  user: profile.emailAddress,
@@ -112,12 +121,12 @@ export const gmailMessagingProvider: MessagingProvider = {
112
121
  },
113
122
 
114
123
  async listConversations(
115
- connectionOrToken: OAuthConnection | string,
124
+ connection: OAuthConnection | undefined,
116
125
  _options?: ListOptions,
117
126
  ): Promise<Conversation[]> {
118
- const connection = connectionOrToken as OAuthConnection;
127
+ const conn = requireConnection(connection);
119
128
  // Gmail "conversations" are modeled as labels with unread counts
120
- const labels = await gmail.listLabels(connection);
129
+ const labels = await gmail.listLabels(conn);
121
130
  const conversations: Conversation[] = [];
122
131
 
123
132
  for (const label of labels) {
@@ -156,15 +165,15 @@ export const gmailMessagingProvider: MessagingProvider = {
156
165
  },
157
166
 
158
167
  async getHistory(
159
- connectionOrToken: OAuthConnection | string,
168
+ connection: OAuthConnection | undefined,
160
169
  conversationId: string,
161
170
  options?: HistoryOptions,
162
171
  ): Promise<Message[]> {
163
- const connection = connectionOrToken as OAuthConnection;
172
+ const conn = requireConnection(connection);
164
173
  // conversationId is a label ID — list messages in that label
165
174
  const limit = options?.limit ?? 50;
166
175
  const listResult = await gmail.listMessages(
167
- connection,
176
+ conn,
168
177
  undefined,
169
178
  limit,
170
179
  undefined,
@@ -174,7 +183,7 @@ export const gmailMessagingProvider: MessagingProvider = {
174
183
  if (!listResult.messages?.length) return [];
175
184
 
176
185
  const messages = await gmail.batchGetMessages(
177
- connection,
186
+ conn,
178
187
  listResult.messages.map((m) => m.id),
179
188
  "full",
180
189
  );
@@ -183,20 +192,20 @@ export const gmailMessagingProvider: MessagingProvider = {
183
192
  },
184
193
 
185
194
  async search(
186
- connectionOrToken: OAuthConnection | string,
195
+ connection: OAuthConnection | undefined,
187
196
  query: string,
188
197
  options?: SearchOptions,
189
198
  ): Promise<SearchResult> {
190
- const connection = connectionOrToken as OAuthConnection;
199
+ const conn = requireConnection(connection);
191
200
  const count = options?.count ?? 20;
192
- const listResult = await gmail.listMessages(connection, query, count);
201
+ const listResult = await gmail.listMessages(conn, query, count);
193
202
 
194
203
  if (!listResult.messages?.length) {
195
204
  return { total: 0, messages: [], hasMore: false };
196
205
  }
197
206
 
198
207
  const messages = await gmail.batchGetMessages(
199
- connection,
208
+ conn,
200
209
  listResult.messages.map((m) => m.id),
201
210
  "full",
202
211
  );
@@ -210,17 +219,17 @@ export const gmailMessagingProvider: MessagingProvider = {
210
219
  },
211
220
 
212
221
  async sendMessage(
213
- connectionOrToken: OAuthConnection | string,
222
+ connection: OAuthConnection | undefined,
214
223
  conversationId: string,
215
224
  text: string,
216
225
  options?: SendOptions,
217
226
  ): Promise<SendResult> {
218
- const connection = connectionOrToken as OAuthConnection;
227
+ const conn = requireConnection(connection);
219
228
  // conversationId is the recipient email for Gmail
220
229
  const to = conversationId;
221
230
  const subject = options?.subject ?? "";
222
231
  const msg = await gmail.sendMessage(
223
- connection,
232
+ conn,
224
233
  to,
225
234
  subject,
226
235
  text,
@@ -236,16 +245,16 @@ export const gmailMessagingProvider: MessagingProvider = {
236
245
  },
237
246
 
238
247
  async getThreadReplies(
239
- connectionOrToken: OAuthConnection | string,
248
+ connection: OAuthConnection | undefined,
240
249
  _conversationId: string,
241
250
  threadId: string,
242
251
  options?: HistoryOptions,
243
252
  ): Promise<Message[]> {
244
- const connection = connectionOrToken as OAuthConnection;
253
+ const conn = requireConnection(connection);
245
254
  // Get all messages in a Gmail thread
246
255
  const limit = options?.limit ?? 50;
247
256
  const listResult = await gmail.listMessages(
248
- connection,
257
+ conn,
249
258
  `thread:${threadId}`,
250
259
  limit,
251
260
  );
@@ -253,7 +262,7 @@ export const gmailMessagingProvider: MessagingProvider = {
253
262
  if (!listResult.messages?.length) return [];
254
263
 
255
264
  const messages = await gmail.batchGetMessages(
256
- connection,
265
+ conn,
257
266
  listResult.messages.map((m) => m.id),
258
267
  "full",
259
268
  );
@@ -262,23 +271,23 @@ export const gmailMessagingProvider: MessagingProvider = {
262
271
  },
263
272
 
264
273
  async markRead(
265
- connectionOrToken: OAuthConnection | string,
274
+ connection: OAuthConnection | undefined,
266
275
  _conversationId: string,
267
276
  messageId?: string,
268
277
  ): Promise<void> {
269
- const connection = connectionOrToken as OAuthConnection;
278
+ const conn = requireConnection(connection);
270
279
  if (!messageId) return;
271
- await gmail.modifyMessage(connection, messageId, {
280
+ await gmail.modifyMessage(conn, messageId, {
272
281
  removeLabelIds: ["UNREAD"],
273
282
  });
274
283
  },
275
284
 
276
285
  async senderDigest(
277
- connectionOrToken: OAuthConnection | string,
286
+ connection: OAuthConnection | undefined,
278
287
  query: string,
279
288
  options?: { maxMessages?: number; maxSenders?: number; pageToken?: string },
280
289
  ): Promise<SenderDigestResult> {
281
- const connection = connectionOrToken as OAuthConnection;
290
+ const conn = requireConnection(connection);
282
291
  const maxMessages = Math.min(options?.maxMessages ?? 5000, 5000);
283
292
  const maxSenders = options?.maxSenders ?? 30;
284
293
  const maxIdsPerSender = 5000;
@@ -298,7 +307,7 @@ export const gmailMessagingProvider: MessagingProvider = {
298
307
  }
299
308
  const pageSize = Math.min(100, maxMessages - allMessageIds.length);
300
309
  const listResp = await gmail.listMessages(
301
- connection,
310
+ conn,
302
311
  query,
303
312
  pageSize,
304
313
  pageToken,
@@ -308,7 +317,7 @@ export const gmailMessagingProvider: MessagingProvider = {
308
317
  allMessageIds.push(...ids);
309
318
  fetchPromises.push(
310
319
  gmail.batchGetMessages(
311
- connection,
320
+ conn,
312
321
  ids,
313
322
  "metadata",
314
323
  metadataHeaders,
@@ -423,10 +432,10 @@ export const gmailMessagingProvider: MessagingProvider = {
423
432
  },
424
433
 
425
434
  async archiveByQuery(
426
- connectionOrToken: OAuthConnection | string,
435
+ connection: OAuthConnection | undefined,
427
436
  query: string,
428
437
  ): Promise<ArchiveResult> {
429
- const connection = connectionOrToken as OAuthConnection;
438
+ const conn = requireConnection(connection);
430
439
  const maxMessages = 5000;
431
440
  const batchModifyLimit = 1000;
432
441
 
@@ -436,7 +445,7 @@ export const gmailMessagingProvider: MessagingProvider = {
436
445
 
437
446
  while (allMessageIds.length < maxMessages) {
438
447
  const listResp = await gmail.listMessages(
439
- connection,
448
+ conn,
440
449
  query,
441
450
  Math.min(500, maxMessages - allMessageIds.length),
442
451
  pageToken,
@@ -458,7 +467,7 @@ export const gmailMessagingProvider: MessagingProvider = {
458
467
 
459
468
  for (let i = 0; i < allMessageIds.length; i += batchModifyLimit) {
460
469
  const chunk = allMessageIds.slice(i, i + batchModifyLimit);
461
- await gmail.batchModifyMessages(connection, chunk, {
470
+ await gmail.batchModifyMessages(conn, chunk, {
462
471
  removeLabelIds: ["INBOX"],
463
472
  });
464
473
  }