@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.
- package/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -6,16 +6,15 @@ import { seedProviders } from "./oauth-store.js";
|
|
|
6
6
|
* These values are upserted into the `oauth_providers` SQLite table on
|
|
7
7
|
* every startup. Only Vellum implementation fields (authUrl, tokenUrl,
|
|
8
8
|
* tokenEndpointAuthMethod, userinfoUrl, extraParams, callbackTransport,
|
|
9
|
-
* pingUrl, pingMethod, pingHeaders, pingBody, managedServiceConfigKey
|
|
9
|
+
* pingUrl, pingMethod, pingHeaders, pingBody, managedServiceConfigKey,
|
|
10
|
+
* loopbackPort, injectionTemplates, appType, setupNotes,
|
|
11
|
+
* identityUrl, identityMethod, identityHeaders, identityBody,
|
|
12
|
+
* identityResponsePaths, identityFormat, identityOkField)
|
|
10
13
|
* and display metadata (displayName,
|
|
11
14
|
* description, dashboardUrl, clientIdPlaceholder, requiresClientSecret)
|
|
12
15
|
* are overwritten on subsequent startups — user-customizable
|
|
13
|
-
* fields (defaultScopes, scopePolicy
|
|
16
|
+
* fields (defaultScopes, scopePolicy) are only
|
|
14
17
|
* written on initial insert and preserved across restarts.
|
|
15
|
-
*
|
|
16
|
-
* Code-side behavioral fields (identityVerifier, injectionTemplates,
|
|
17
|
-
* setup, etc.) live in `provider-behaviors.ts` and are never persisted
|
|
18
|
-
* to the DB.
|
|
19
18
|
*/
|
|
20
19
|
const PROVIDER_SEED_DATA: Record<
|
|
21
20
|
string,
|
|
@@ -44,10 +43,26 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
44
43
|
dashboardUrl: string | null;
|
|
45
44
|
clientIdPlaceholder: string | null;
|
|
46
45
|
requiresClientSecret?: boolean;
|
|
46
|
+
loopbackPort?: number;
|
|
47
|
+
injectionTemplates?: Array<{
|
|
48
|
+
hostPattern: string;
|
|
49
|
+
injectionType: string;
|
|
50
|
+
headerName: string;
|
|
51
|
+
valuePrefix: string;
|
|
52
|
+
}>;
|
|
53
|
+
appType?: string;
|
|
54
|
+
setupNotes?: string[];
|
|
55
|
+
identityUrl?: string;
|
|
56
|
+
identityMethod?: string;
|
|
57
|
+
identityHeaders?: Record<string, string>;
|
|
58
|
+
identityBody?: unknown;
|
|
59
|
+
identityResponsePaths?: string[];
|
|
60
|
+
identityFormat?: string;
|
|
61
|
+
identityOkField?: string;
|
|
47
62
|
}
|
|
48
63
|
> = {
|
|
49
|
-
|
|
50
|
-
providerKey: "
|
|
64
|
+
google: {
|
|
65
|
+
providerKey: "google",
|
|
51
66
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
52
67
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
53
68
|
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
@@ -77,10 +92,33 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
77
92
|
extraParams: { access_type: "offline", prompt: "consent" },
|
|
78
93
|
callbackTransport: "loopback",
|
|
79
94
|
managedServiceConfigKey: "google-oauth",
|
|
95
|
+
injectionTemplates: [
|
|
96
|
+
{
|
|
97
|
+
hostPattern: "gmail.googleapis.com",
|
|
98
|
+
injectionType: "header",
|
|
99
|
+
headerName: "Authorization",
|
|
100
|
+
valuePrefix: "Bearer ",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
hostPattern: "www.googleapis.com",
|
|
104
|
+
injectionType: "header",
|
|
105
|
+
headerName: "Authorization",
|
|
106
|
+
valuePrefix: "Bearer ",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
hostPattern: "people.googleapis.com",
|
|
110
|
+
injectionType: "header",
|
|
111
|
+
headerName: "Authorization",
|
|
112
|
+
valuePrefix: "Bearer ",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
appType: "Desktop app",
|
|
116
|
+
identityUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
117
|
+
identityResponsePaths: ["email"],
|
|
80
118
|
},
|
|
81
119
|
|
|
82
|
-
|
|
83
|
-
providerKey: "
|
|
120
|
+
slack: {
|
|
121
|
+
providerKey: "slack",
|
|
84
122
|
authUrl: "https://slack.com/oauth/v2/authorize",
|
|
85
123
|
tokenUrl: "https://slack.com/api/oauth.v2.access",
|
|
86
124
|
pingUrl: "https://slack.com/api/auth.test",
|
|
@@ -114,10 +152,24 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
114
152
|
"channels:read,channels:history,groups:read,groups:history,im:read,im:history,im:write,mpim:read,mpim:history,users:read,chat:write,search:read,reactions:write",
|
|
115
153
|
},
|
|
116
154
|
callbackTransport: "loopback",
|
|
155
|
+
loopbackPort: 17322,
|
|
156
|
+
injectionTemplates: [
|
|
157
|
+
{
|
|
158
|
+
hostPattern: "slack.com",
|
|
159
|
+
injectionType: "header",
|
|
160
|
+
headerName: "Authorization",
|
|
161
|
+
valuePrefix: "Bearer ",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
appType: "Slack App",
|
|
165
|
+
identityUrl: "https://slack.com/api/auth.test",
|
|
166
|
+
identityOkField: "ok",
|
|
167
|
+
identityResponsePaths: ["user", "team"],
|
|
168
|
+
identityFormat: "@${user} (${team})",
|
|
117
169
|
},
|
|
118
170
|
|
|
119
|
-
|
|
120
|
-
providerKey: "
|
|
171
|
+
notion: {
|
|
172
|
+
providerKey: "notion",
|
|
121
173
|
authUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
122
174
|
tokenUrl: "https://api.notion.com/v1/oauth/token",
|
|
123
175
|
pingUrl: "https://api.notion.com/v1/users/me",
|
|
@@ -136,10 +188,23 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
136
188
|
extraParams: { owner: "user" },
|
|
137
189
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
138
190
|
callbackTransport: "loopback",
|
|
191
|
+
loopbackPort: 17323,
|
|
192
|
+
injectionTemplates: [
|
|
193
|
+
{
|
|
194
|
+
hostPattern: "api.notion.com",
|
|
195
|
+
injectionType: "header",
|
|
196
|
+
headerName: "Authorization",
|
|
197
|
+
valuePrefix: "Bearer ",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
appType: "Public integration",
|
|
201
|
+
identityUrl: "https://api.notion.com/v1/users/me",
|
|
202
|
+
identityHeaders: { "Notion-Version": "2022-06-28" },
|
|
203
|
+
identityResponsePaths: ["name", "person.email"],
|
|
139
204
|
},
|
|
140
205
|
|
|
141
|
-
|
|
142
|
-
providerKey: "
|
|
206
|
+
twitter: {
|
|
207
|
+
providerKey: "twitter",
|
|
143
208
|
authUrl: "https://twitter.com/i/oauth2/authorize",
|
|
144
209
|
tokenUrl: "https://api.x.com/2/oauth2/token",
|
|
145
210
|
pingUrl: "https://api.x.com/2/users/me",
|
|
@@ -161,10 +226,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
161
226
|
},
|
|
162
227
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
163
228
|
callbackTransport: "gateway",
|
|
229
|
+
injectionTemplates: [
|
|
230
|
+
{
|
|
231
|
+
hostPattern: "api.x.com",
|
|
232
|
+
injectionType: "header",
|
|
233
|
+
headerName: "Authorization",
|
|
234
|
+
valuePrefix: "Bearer ",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
appType: "App",
|
|
238
|
+
identityUrl: "https://api.x.com/2/users/me",
|
|
239
|
+
identityResponsePaths: ["data.username"],
|
|
240
|
+
identityFormat: "@${data.username}",
|
|
164
241
|
},
|
|
165
242
|
|
|
166
|
-
|
|
167
|
-
providerKey: "
|
|
243
|
+
github: {
|
|
244
|
+
providerKey: "github",
|
|
168
245
|
authUrl: "https://github.com/login/oauth/authorize",
|
|
169
246
|
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
170
247
|
pingUrl: "https://api.github.com/user",
|
|
@@ -185,10 +262,23 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
185
262
|
forbiddenScopes: ["delete_repo", "admin:org"],
|
|
186
263
|
},
|
|
187
264
|
callbackTransport: "loopback",
|
|
265
|
+
loopbackPort: 17332,
|
|
266
|
+
injectionTemplates: [
|
|
267
|
+
{
|
|
268
|
+
hostPattern: "api.github.com",
|
|
269
|
+
injectionType: "header",
|
|
270
|
+
headerName: "Authorization",
|
|
271
|
+
valuePrefix: "Bearer ",
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
appType: "OAuth App",
|
|
275
|
+
identityUrl: "https://api.github.com/user",
|
|
276
|
+
identityResponsePaths: ["login"],
|
|
277
|
+
identityFormat: "@${login}",
|
|
188
278
|
},
|
|
189
279
|
|
|
190
|
-
|
|
191
|
-
providerKey: "
|
|
280
|
+
linear: {
|
|
281
|
+
providerKey: "linear",
|
|
192
282
|
authUrl: "https://linear.app/oauth/authorize",
|
|
193
283
|
tokenUrl: "https://api.linear.app/oauth/token",
|
|
194
284
|
pingUrl: "https://api.linear.app/graphql",
|
|
@@ -208,10 +298,25 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
208
298
|
},
|
|
209
299
|
extraParams: { prompt: "consent" },
|
|
210
300
|
callbackTransport: "loopback",
|
|
301
|
+
loopbackPort: 17324,
|
|
302
|
+
injectionTemplates: [
|
|
303
|
+
{
|
|
304
|
+
hostPattern: "api.linear.app",
|
|
305
|
+
injectionType: "header",
|
|
306
|
+
headerName: "Authorization",
|
|
307
|
+
valuePrefix: "Bearer ",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
appType: "OAuth application",
|
|
311
|
+
identityUrl: "https://api.linear.app/graphql",
|
|
312
|
+
identityMethod: "POST",
|
|
313
|
+
identityHeaders: { "Content-Type": "application/json" },
|
|
314
|
+
identityBody: { query: "{ viewer { email name } }" },
|
|
315
|
+
identityResponsePaths: ["data.viewer.email", "data.viewer.name"],
|
|
211
316
|
},
|
|
212
317
|
|
|
213
|
-
|
|
214
|
-
providerKey: "
|
|
318
|
+
spotify: {
|
|
319
|
+
providerKey: "spotify",
|
|
215
320
|
authUrl: "https://accounts.spotify.com/authorize",
|
|
216
321
|
tokenUrl: "https://accounts.spotify.com/api/token",
|
|
217
322
|
pingUrl: "https://api.spotify.com/v1/me",
|
|
@@ -238,10 +343,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
238
343
|
},
|
|
239
344
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
240
345
|
callbackTransport: "loopback",
|
|
346
|
+
loopbackPort: 17333,
|
|
347
|
+
injectionTemplates: [
|
|
348
|
+
{
|
|
349
|
+
hostPattern: "api.spotify.com",
|
|
350
|
+
injectionType: "header",
|
|
351
|
+
headerName: "Authorization",
|
|
352
|
+
valuePrefix: "Bearer ",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
appType: "App",
|
|
356
|
+
identityUrl: "https://api.spotify.com/v1/me",
|
|
357
|
+
identityResponsePaths: ["display_name", "email"],
|
|
241
358
|
},
|
|
242
359
|
|
|
243
|
-
|
|
244
|
-
providerKey: "
|
|
360
|
+
todoist: {
|
|
361
|
+
providerKey: "todoist",
|
|
245
362
|
authUrl: "https://todoist.com/oauth/authorize",
|
|
246
363
|
tokenUrl: "https://todoist.com/oauth/access_token",
|
|
247
364
|
pingUrl: "https://api.todoist.com/rest/v2/projects",
|
|
@@ -257,10 +374,25 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
257
374
|
forbiddenScopes: ["data:delete"],
|
|
258
375
|
},
|
|
259
376
|
callbackTransport: "loopback",
|
|
377
|
+
loopbackPort: 17325,
|
|
378
|
+
injectionTemplates: [
|
|
379
|
+
{
|
|
380
|
+
hostPattern: "api.todoist.com",
|
|
381
|
+
injectionType: "header",
|
|
382
|
+
headerName: "Authorization",
|
|
383
|
+
valuePrefix: "Bearer ",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
appType: "App",
|
|
387
|
+
identityUrl: "https://api.todoist.com/sync/v9/sync",
|
|
388
|
+
identityMethod: "POST",
|
|
389
|
+
identityHeaders: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
390
|
+
identityBody: "sync_token=*&resource_types=[%22user%22]",
|
|
391
|
+
identityResponsePaths: ["user.full_name", "user.email"],
|
|
260
392
|
},
|
|
261
393
|
|
|
262
|
-
|
|
263
|
-
providerKey: "
|
|
394
|
+
discord: {
|
|
395
|
+
providerKey: "discord",
|
|
264
396
|
authUrl: "https://discord.com/oauth2/authorize",
|
|
265
397
|
tokenUrl: "https://discord.com/api/v10/oauth2/token",
|
|
266
398
|
pingUrl: "https://discord.com/api/v10/users/@me",
|
|
@@ -281,10 +413,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
281
413
|
forbiddenScopes: [],
|
|
282
414
|
},
|
|
283
415
|
callbackTransport: "loopback",
|
|
416
|
+
loopbackPort: 17326,
|
|
417
|
+
injectionTemplates: [
|
|
418
|
+
{
|
|
419
|
+
hostPattern: "discord.com",
|
|
420
|
+
injectionType: "header",
|
|
421
|
+
headerName: "Authorization",
|
|
422
|
+
valuePrefix: "Bearer ",
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
appType: "Application",
|
|
426
|
+
identityUrl: "https://discord.com/api/v10/users/@me",
|
|
427
|
+
identityResponsePaths: ["global_name", "username"],
|
|
284
428
|
},
|
|
285
429
|
|
|
286
|
-
|
|
287
|
-
providerKey: "
|
|
430
|
+
dropbox: {
|
|
431
|
+
providerKey: "dropbox",
|
|
288
432
|
authUrl: "https://www.dropbox.com/oauth2/authorize",
|
|
289
433
|
tokenUrl: "https://api.dropboxapi.com/oauth2/token",
|
|
290
434
|
pingUrl: "https://api.dropboxapi.com/2/users/get_current_account",
|
|
@@ -307,10 +451,29 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
307
451
|
},
|
|
308
452
|
extraParams: { token_access_type: "offline" },
|
|
309
453
|
callbackTransport: "loopback",
|
|
454
|
+
loopbackPort: 17327,
|
|
455
|
+
injectionTemplates: [
|
|
456
|
+
{
|
|
457
|
+
hostPattern: "api.dropboxapi.com",
|
|
458
|
+
injectionType: "header",
|
|
459
|
+
headerName: "Authorization",
|
|
460
|
+
valuePrefix: "Bearer ",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
hostPattern: "content.dropboxapi.com",
|
|
464
|
+
injectionType: "header",
|
|
465
|
+
headerName: "Authorization",
|
|
466
|
+
valuePrefix: "Bearer ",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
appType: "Scoped access app",
|
|
470
|
+
identityUrl: "https://api.dropboxapi.com/2/users/get_current_account",
|
|
471
|
+
identityMethod: "POST",
|
|
472
|
+
identityResponsePaths: ["name.display_name", "email"],
|
|
310
473
|
},
|
|
311
474
|
|
|
312
|
-
|
|
313
|
-
providerKey: "
|
|
475
|
+
asana: {
|
|
476
|
+
providerKey: "asana",
|
|
314
477
|
authUrl: "https://app.asana.com/-/oauth_authorize",
|
|
315
478
|
tokenUrl: "https://app.asana.com/-/oauth_token",
|
|
316
479
|
pingUrl: "https://app.asana.com/api/1.0/users/me",
|
|
@@ -326,10 +489,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
326
489
|
forbiddenScopes: [],
|
|
327
490
|
},
|
|
328
491
|
callbackTransport: "loopback",
|
|
492
|
+
loopbackPort: 17328,
|
|
493
|
+
injectionTemplates: [
|
|
494
|
+
{
|
|
495
|
+
hostPattern: "app.asana.com",
|
|
496
|
+
injectionType: "header",
|
|
497
|
+
headerName: "Authorization",
|
|
498
|
+
valuePrefix: "Bearer ",
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
appType: "App",
|
|
502
|
+
identityUrl: "https://app.asana.com/api/1.0/users/me",
|
|
503
|
+
identityResponsePaths: ["data.name", "data.email"],
|
|
329
504
|
},
|
|
330
505
|
|
|
331
|
-
|
|
332
|
-
providerKey: "
|
|
506
|
+
airtable: {
|
|
507
|
+
providerKey: "airtable",
|
|
333
508
|
authUrl: "https://airtable.com/oauth2/v1/authorize",
|
|
334
509
|
tokenUrl: "https://airtable.com/oauth2/v1/token",
|
|
335
510
|
pingUrl: "https://api.airtable.com/v0/meta/whoami",
|
|
@@ -350,10 +525,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
350
525
|
},
|
|
351
526
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
352
527
|
callbackTransport: "loopback",
|
|
528
|
+
loopbackPort: 17329,
|
|
529
|
+
injectionTemplates: [
|
|
530
|
+
{
|
|
531
|
+
hostPattern: "api.airtable.com",
|
|
532
|
+
injectionType: "header",
|
|
533
|
+
headerName: "Authorization",
|
|
534
|
+
valuePrefix: "Bearer ",
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
appType: "OAuth integration",
|
|
538
|
+
identityUrl: "https://api.airtable.com/v0/meta/whoami",
|
|
539
|
+
identityResponsePaths: ["email"],
|
|
353
540
|
},
|
|
354
541
|
|
|
355
|
-
|
|
356
|
-
providerKey: "
|
|
542
|
+
hubspot: {
|
|
543
|
+
providerKey: "hubspot",
|
|
357
544
|
authUrl: "https://app.hubspot.com/oauth/authorize",
|
|
358
545
|
tokenUrl: "https://api.hubapi.com/oauth/v1/token",
|
|
359
546
|
pingUrl: "https://api.hubapi.com/crm/v3/objects/contacts?limit=1",
|
|
@@ -378,10 +565,22 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
378
565
|
forbiddenScopes: [],
|
|
379
566
|
},
|
|
380
567
|
callbackTransport: "loopback",
|
|
568
|
+
loopbackPort: 17330,
|
|
569
|
+
injectionTemplates: [
|
|
570
|
+
{
|
|
571
|
+
hostPattern: "api.hubapi.com",
|
|
572
|
+
injectionType: "header",
|
|
573
|
+
headerName: "Authorization",
|
|
574
|
+
valuePrefix: "Bearer ",
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
appType: "App",
|
|
578
|
+
identityUrl: "https://api.hubapi.com/oauth/v1/access-tokens/${accessToken}",
|
|
579
|
+
identityResponsePaths: ["user", "hub_domain"],
|
|
381
580
|
},
|
|
382
581
|
|
|
383
|
-
|
|
384
|
-
providerKey: "
|
|
582
|
+
figma: {
|
|
583
|
+
providerKey: "figma",
|
|
385
584
|
authUrl: "https://www.figma.com/oauth",
|
|
386
585
|
tokenUrl: "https://api.figma.com/v1/oauth/token",
|
|
387
586
|
pingUrl: "https://api.figma.com/v1/me",
|
|
@@ -398,6 +597,18 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
398
597
|
},
|
|
399
598
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
400
599
|
callbackTransport: "loopback",
|
|
600
|
+
loopbackPort: 17331,
|
|
601
|
+
injectionTemplates: [
|
|
602
|
+
{
|
|
603
|
+
hostPattern: "api.figma.com",
|
|
604
|
+
injectionType: "header",
|
|
605
|
+
headerName: "Authorization",
|
|
606
|
+
valuePrefix: "Bearer ",
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
appType: "App",
|
|
610
|
+
identityUrl: "https://api.figma.com/v1/me",
|
|
611
|
+
identityResponsePaths: ["handle", "email"],
|
|
401
612
|
},
|
|
402
613
|
|
|
403
614
|
// Manual-token providers: these don't use OAuth2 flows but need provider
|
|
@@ -441,6 +652,8 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
441
652
|
},
|
|
442
653
|
};
|
|
443
654
|
|
|
655
|
+
export const SEEDED_PROVIDER_KEYS = new Set(Object.keys(PROVIDER_SEED_DATA));
|
|
656
|
+
|
|
444
657
|
/**
|
|
445
658
|
* Seed the oauth_providers table with well-known provider configurations.
|
|
446
659
|
* Uses INSERT … ON CONFLICT DO UPDATE so seed-data corrections propagate
|
|
@@ -146,7 +146,6 @@ const LOW_RISK_PROGRAMS = new Set([
|
|
|
146
146
|
"tree",
|
|
147
147
|
"du",
|
|
148
148
|
"df",
|
|
149
|
-
"assistant",
|
|
150
149
|
]);
|
|
151
150
|
|
|
152
151
|
// High-risk shell programs / patterns
|
|
@@ -200,6 +199,46 @@ const LOW_RISK_GIT_SUBCOMMANDS = new Set([
|
|
|
200
199
|
"reflog",
|
|
201
200
|
]);
|
|
202
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Classify risk for `assistant` CLI subcommands. Multi-word subcommands
|
|
204
|
+
* (e.g. `assistant oauth token`) are matched by walking the positional args.
|
|
205
|
+
*/
|
|
206
|
+
function classifyAssistantSubcommand(args: string[]): RiskLevel {
|
|
207
|
+
// `--help` on any subcommand is read-only, always Low risk.
|
|
208
|
+
// Only check args before `--` (option terminator) — after `--`, tokens
|
|
209
|
+
// are positional arguments, not flags.
|
|
210
|
+
const ddIndex = args.indexOf("--");
|
|
211
|
+
const flagArgs = ddIndex === -1 ? args : args.slice(0, ddIndex);
|
|
212
|
+
if (flagArgs.some((a) => a === "--help" || a === "-h")) return RiskLevel.Low;
|
|
213
|
+
|
|
214
|
+
const sub = firstPositionalArg(args);
|
|
215
|
+
if (!sub) return RiskLevel.Low;
|
|
216
|
+
|
|
217
|
+
if (sub === "oauth") {
|
|
218
|
+
const oauthSub = firstPositionalArg(args.slice(args.indexOf(sub) + 1));
|
|
219
|
+
if (oauthSub === "token") return RiskLevel.High;
|
|
220
|
+
if (oauthSub === "mode") {
|
|
221
|
+
// `oauth mode --set` is high risk; bare `oauth mode` (read) is low.
|
|
222
|
+
// Match both `--set value` (two tokens) and `--set=value` (one token).
|
|
223
|
+
if (args.some((a) => a === "--set" || a.startsWith("--set=")))
|
|
224
|
+
return RiskLevel.High;
|
|
225
|
+
return RiskLevel.Low;
|
|
226
|
+
}
|
|
227
|
+
if (oauthSub === "request") return RiskLevel.Medium;
|
|
228
|
+
if (oauthSub === "connect" || oauthSub === "disconnect")
|
|
229
|
+
return RiskLevel.Medium;
|
|
230
|
+
return RiskLevel.Low;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (sub === "credentials") {
|
|
234
|
+
const credSub = firstPositionalArg(args.slice(args.indexOf(sub) + 1));
|
|
235
|
+
if (credSub === "reveal") return RiskLevel.High;
|
|
236
|
+
return RiskLevel.Low;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return RiskLevel.Low;
|
|
240
|
+
}
|
|
241
|
+
|
|
203
242
|
// Commands that wrap another program — the real program appears as the first
|
|
204
243
|
// non-flag argument. When one of these is the segment program we look through
|
|
205
244
|
// its args to find the effective program (e.g. `env curl …` → curl).
|
|
@@ -222,6 +261,14 @@ const WRAPPER_PROGRAMS = new Set([
|
|
|
222
261
|
// value of -u) as the wrapped program instead of `echo`.
|
|
223
262
|
const ENV_VALUE_FLAGS = new Set(["-u", "--unset", "-C", "--chdir"]);
|
|
224
263
|
|
|
264
|
+
// `timeout` flags that consume the next positional argument as their value.
|
|
265
|
+
const TIMEOUT_VALUE_FLAGS = new Set(["-s", "--signal", "-k", "--kill-after"]);
|
|
266
|
+
|
|
267
|
+
// Wrapper programs where the first non-flag positional argument is a
|
|
268
|
+
// configuration value (duration, CPU mask), not the wrapped program name.
|
|
269
|
+
// For these wrappers, the second non-flag positional is the real program.
|
|
270
|
+
const WRAPPER_SKIP_FIRST_POSITIONAL = new Set(["timeout", "taskset"]);
|
|
271
|
+
|
|
225
272
|
// `git` global flags that consume the next positional argument as their value.
|
|
226
273
|
// Without this, `git -C status commit` would incorrectly identify `status`
|
|
227
274
|
// (the directory path) as the subcommand instead of `commit`.
|
|
@@ -280,24 +327,66 @@ function isRmOfKnownSafeFile(args: string[]): boolean {
|
|
|
280
327
|
*
|
|
281
328
|
* Handles `env` specially: skips `VAR=value` pairs and value-taking flags
|
|
282
329
|
* like `-u NAME` and `-C DIR`.
|
|
330
|
+
*
|
|
331
|
+
* Handles `timeout` and `taskset` specially: their first non-flag positional
|
|
332
|
+
* argument is a duration or CPU mask, not the wrapped program. The second
|
|
333
|
+
* non-flag positional is the real program.
|
|
283
334
|
*/
|
|
284
335
|
function getWrappedProgram(seg: {
|
|
285
336
|
program: string;
|
|
286
337
|
args: string[];
|
|
287
338
|
}): string | undefined {
|
|
288
339
|
const isEnv = seg.program === "env";
|
|
340
|
+
const isTimeout = seg.program === "timeout";
|
|
341
|
+
const skipFirst = WRAPPER_SKIP_FIRST_POSITIONAL.has(seg.program);
|
|
342
|
+
let skippedFirstPositional = false;
|
|
289
343
|
for (let i = 0; i < seg.args.length; i++) {
|
|
290
344
|
const arg = seg.args[i];
|
|
291
345
|
if (arg.startsWith("-")) {
|
|
292
346
|
if (isEnv && ENV_VALUE_FLAGS.has(arg)) i++; // skip the value argument
|
|
347
|
+
if (isTimeout && TIMEOUT_VALUE_FLAGS.has(arg)) i++; // skip the value argument
|
|
293
348
|
continue;
|
|
294
349
|
}
|
|
295
350
|
if (isEnv && arg.includes("=")) continue; // skip env VAR=value pairs
|
|
351
|
+
if (skipFirst && !skippedFirstPositional) {
|
|
352
|
+
skippedFirstPositional = true;
|
|
353
|
+
continue; // skip the duration/CPU mask
|
|
354
|
+
}
|
|
296
355
|
return arg;
|
|
297
356
|
}
|
|
298
357
|
return undefined;
|
|
299
358
|
}
|
|
300
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Like `getWrappedProgram`, but also returns the remaining args after the
|
|
362
|
+
* wrapped program name. This allows callers to propagate subcommand-aware
|
|
363
|
+
* classification (e.g. `env assistant oauth token` → classify `oauth token`).
|
|
364
|
+
*/
|
|
365
|
+
function getWrappedProgramWithArgs(seg: {
|
|
366
|
+
program: string;
|
|
367
|
+
args: string[];
|
|
368
|
+
}): { program: string; args: string[] } | undefined {
|
|
369
|
+
const isEnv = seg.program === "env";
|
|
370
|
+
const isTimeout = seg.program === "timeout";
|
|
371
|
+
const skipFirst = WRAPPER_SKIP_FIRST_POSITIONAL.has(seg.program);
|
|
372
|
+
let skippedFirstPositional = false;
|
|
373
|
+
for (let i = 0; i < seg.args.length; i++) {
|
|
374
|
+
const arg = seg.args[i];
|
|
375
|
+
if (arg.startsWith("-")) {
|
|
376
|
+
if (isEnv && ENV_VALUE_FLAGS.has(arg)) i++;
|
|
377
|
+
if (isTimeout && TIMEOUT_VALUE_FLAGS.has(arg)) i++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (isEnv && arg.includes("=")) continue;
|
|
381
|
+
if (skipFirst && !skippedFirstPositional) {
|
|
382
|
+
skippedFirstPositional = true;
|
|
383
|
+
continue; // skip the duration/CPU mask
|
|
384
|
+
}
|
|
385
|
+
return { program: arg, args: seg.args.slice(i + 1) };
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
301
390
|
function getStringField(
|
|
302
391
|
input: Record<string, unknown>,
|
|
303
392
|
...keys: string[]
|
|
@@ -731,6 +820,34 @@ async function classifyRiskUncached(
|
|
|
731
820
|
maxRisk = RiskLevel.Medium;
|
|
732
821
|
continue;
|
|
733
822
|
}
|
|
823
|
+
// Propagate subcommand-aware classification for wrapped git/assistant
|
|
824
|
+
if (wrapped === "git") {
|
|
825
|
+
const wrappedWithArgs = getWrappedProgramWithArgs(seg);
|
|
826
|
+
if (wrappedWithArgs) {
|
|
827
|
+
const subcommand = firstPositionalArg(
|
|
828
|
+
wrappedWithArgs.args,
|
|
829
|
+
GIT_VALUE_FLAGS,
|
|
830
|
+
);
|
|
831
|
+
if (subcommand && LOW_RISK_GIT_SUBCOMMANDS.has(subcommand)) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
maxRisk = RiskLevel.Medium;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (wrapped === "assistant") {
|
|
839
|
+
const wrappedWithArgs = getWrappedProgramWithArgs(seg);
|
|
840
|
+
if (wrappedWithArgs) {
|
|
841
|
+
const assistantRisk = classifyAssistantSubcommand(
|
|
842
|
+
wrappedWithArgs.args,
|
|
843
|
+
);
|
|
844
|
+
if (assistantRisk === RiskLevel.High) return RiskLevel.High;
|
|
845
|
+
if (assistantRisk === RiskLevel.Medium) {
|
|
846
|
+
maxRisk = RiskLevel.Medium;
|
|
847
|
+
}
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
734
851
|
}
|
|
735
852
|
|
|
736
853
|
if (prog === "git") {
|
|
@@ -744,6 +861,15 @@ async function classifyRiskUncached(
|
|
|
744
861
|
continue;
|
|
745
862
|
}
|
|
746
863
|
|
|
864
|
+
if (prog === "assistant") {
|
|
865
|
+
const assistantRisk = classifyAssistantSubcommand(seg.args);
|
|
866
|
+
if (assistantRisk === RiskLevel.High) return RiskLevel.High;
|
|
867
|
+
if (assistantRisk === RiskLevel.Medium) {
|
|
868
|
+
maxRisk = RiskLevel.Medium;
|
|
869
|
+
}
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
747
873
|
if (!LOW_RISK_PROGRAMS.has(prog)) {
|
|
748
874
|
// Unknown program → medium
|
|
749
875
|
if (maxRisk === RiskLevel.Low) {
|
|
@@ -78,7 +78,10 @@ export function buildJournalContext(
|
|
|
78
78
|
|
|
79
79
|
// Filter for .md files, excluding README.md (case-insensitive)
|
|
80
80
|
const mdFiles = files.filter(
|
|
81
|
-
(f) =>
|
|
81
|
+
(f) =>
|
|
82
|
+
f.endsWith(".md") &&
|
|
83
|
+
!f.startsWith(".") &&
|
|
84
|
+
f.toLowerCase() !== "readme.md",
|
|
82
85
|
);
|
|
83
86
|
|
|
84
87
|
// Collect file info with birthtime (creation time), skipping unreadable entries
|