causly-server 0.0.0-stage → 1.5.1

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 (91) hide show
  1. package/.env.example +120 -0
  2. package/CHANGELOG.md +126 -0
  3. package/LICENSE +21 -0
  4. package/README.md +377 -3
  5. package/bin/causly.js +192 -0
  6. package/index.js +229 -0
  7. package/package.json +59 -6
  8. package/packs/calendar.js +141 -0
  9. package/packs/command.js +22 -0
  10. package/packs/database.js +95 -0
  11. package/packs/directory.js +63 -0
  12. package/packs/discord.js +63 -0
  13. package/packs/docker.js +202 -0
  14. package/packs/files.js +106 -0
  15. package/packs/git.js +211 -0
  16. package/packs/github.js +321 -0
  17. package/packs/gmail.js +98 -0
  18. package/packs/leads.js +46 -0
  19. package/packs/linear.js +104 -0
  20. package/packs/loader.js +247 -0
  21. package/packs/manifest.json +1152 -0
  22. package/packs/meetings.js +141 -0
  23. package/packs/notion.js +185 -0
  24. package/packs/pipeline.js +151 -0
  25. package/packs/project.js +70 -0
  26. package/packs/registry.js +149 -0
  27. package/packs/reminders.js +57 -0
  28. package/packs/research.js +34 -0
  29. package/packs/secrets.js +77 -0
  30. package/packs/sentry.js +97 -0
  31. package/packs/seo.js +70 -0
  32. package/packs/slack.js +95 -0
  33. package/packs/stripe.js +127 -0
  34. package/packs/supabase.js +73 -0
  35. package/packs/telegram.js +70 -0
  36. package/packs/terraform.js +247 -0
  37. package/packs/vercel.js +114 -0
  38. package/packs/workflow.js +84 -0
  39. package/prompts/cofounder.js +66 -0
  40. package/prompts/playbooks.js +75 -0
  41. package/tools/backupOps.js +87 -0
  42. package/tools/boundaryAttest.js +184 -0
  43. package/tools/calendarOps.js +563 -0
  44. package/tools/cliOps.js +233 -0
  45. package/tools/commandOps.js +198 -0
  46. package/tools/dbOps.js +117 -0
  47. package/tools/directoryOps.js +96 -0
  48. package/tools/discordOps.js +133 -0
  49. package/tools/dockerOps.js +259 -0
  50. package/tools/envFile.js +89 -0
  51. package/tools/envLoader.js +85 -0
  52. package/tools/fileOps.js +125 -0
  53. package/tools/gitOps.js +170 -0
  54. package/tools/githubOps.js +357 -0
  55. package/tools/gmailOps.js +221 -0
  56. package/tools/googleAuth.js +51 -0
  57. package/tools/googleLogin.js +230 -0
  58. package/tools/leadOps.js +447 -0
  59. package/tools/linearOps.js +304 -0
  60. package/tools/logger.js +47 -0
  61. package/tools/meetingOps.js +697 -0
  62. package/tools/notionOps.js +215 -0
  63. package/tools/paths.js +36 -0
  64. package/tools/pipelineOps.js +511 -0
  65. package/tools/projectOps.js +192 -0
  66. package/tools/reminderAlerts.js +48 -0
  67. package/tools/reminderOps.js +129 -0
  68. package/tools/scheduler.js +46 -0
  69. package/tools/secretsOps.js +133 -0
  70. package/tools/security.js +488 -0
  71. package/tools/sentryOps.js +130 -0
  72. package/tools/seoOps.js +561 -0
  73. package/tools/services.js +295 -0
  74. package/tools/setupOps.js +245 -0
  75. package/tools/slackOps.js +151 -0
  76. package/tools/statusOps.js +104 -0
  77. package/tools/store.js +111 -0
  78. package/tools/stripeOps.js +299 -0
  79. package/tools/supabaseOps.js +94 -0
  80. package/tools/telegramOps.js +153 -0
  81. package/tools/terminal.js +124 -0
  82. package/tools/terraformOps.js +171 -0
  83. package/tools/toolWrapper.js +60 -0
  84. package/tools/toolsetOps.js +123 -0
  85. package/tools/transcribeOps.js +221 -0
  86. package/tools/updateCheck.js +58 -0
  87. package/tools/vercelOps.js +198 -0
  88. package/tools/verifyBoundaryAttest.js +25 -0
  89. package/tools/webOps.js +474 -0
  90. package/tools/workflowOps.js +371 -0
  91. package/tools/workspace.js +85 -0
@@ -0,0 +1,488 @@
1
+ // Security subsystem: secret redaction, permission levels, and command
2
+ // risk classification. Single source of truth so every tool call, log
3
+ // line, and approval check uses the same rules.
4
+
5
+ // ---------------- Secret redaction ----------------
6
+
7
+ // Key names (case-insensitive substring match) whose values must never
8
+ // appear in logs, error messages, or tool responses.
9
+ const SECRET_KEY_PATTERNS = [
10
+ "token",
11
+ "password",
12
+ "passwd",
13
+ "api_key",
14
+ "apikey",
15
+ "secret",
16
+ "authorization",
17
+ "access_token",
18
+ "refresh_token",
19
+ "db_pass",
20
+ "credential",
21
+ "private_key",
22
+ "client_secret",
23
+ "auth_token",
24
+ ];
25
+
26
+ const REDACTED = "[REDACTED]";
27
+
28
+ function isSecretKey(key) {
29
+ const lower = key.toLowerCase();
30
+ return SECRET_KEY_PATTERNS.some((pattern) => lower.includes(pattern));
31
+ }
32
+
33
+ /**
34
+ * Recursively walk an object/array and replace the value of any key that
35
+ * looks like a secret with "[REDACTED]". Never mutates the input — returns
36
+ * a deep copy. Safe against circular references (bails out to "[Circular]").
37
+ */
38
+ export function redactSecrets(value, seen = new WeakSet()) {
39
+ if (value === null || value === undefined) return value;
40
+
41
+ if (Array.isArray(value)) {
42
+ if (seen.has(value)) return "[Circular]";
43
+ seen.add(value);
44
+ return value.map((item) => redactSecrets(item, seen));
45
+ }
46
+
47
+ if (typeof value === "object") {
48
+ if (seen.has(value)) return "[Circular]";
49
+ seen.add(value);
50
+ const out = {};
51
+ for (const [key, val] of Object.entries(value)) {
52
+ out[key] = isSecretKey(key) ? REDACTED : redactSecrets(val, seen);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ return value;
58
+ }
59
+
60
+ /**
61
+ * Redact secret-looking substrings out of a plain string (e.g. an error
62
+ * message that happens to include "token=abc123..."). Best-effort — key/
63
+ * value pairs like `token=xxx`, `token: xxx`, `"token":"xxx"` are caught;
64
+ * this is not a substitute for redactSecrets() on structured data.
65
+ */
66
+ export function redactSecretsInString(str) {
67
+ if (typeof str !== "string") return str;
68
+ const pattern = new RegExp(
69
+ `\\b(${SECRET_KEY_PATTERNS.join("|")})\\b(\\s*[:=]\\s*)("[^"]*"|'[^']*'|\\S+)`,
70
+ "gi"
71
+ );
72
+ return str.replace(pattern, (_match, key, sep) => `${key}${sep}${REDACTED}`);
73
+ }
74
+
75
+ // ---------------- Permission levels ----------------
76
+
77
+ // READ — safe, read-only, no side effects
78
+ // LOW — normal dev actions, low blast radius, easily reversible
79
+ // MEDIUM — modifies project/repo state, reversible with effort
80
+ // HIGH — production-impacting or hard to reverse (pushes, deploys,
81
+ // merges, arbitrary shell/SQL execution)
82
+ // DESTRUCTIVE — deletion / irreversible
83
+ //
84
+ // Approval policy: READ and LOW run automatically. MEDIUM runs
85
+ // automatically too in this single-user trusted-local-machine mode, but
86
+ // is classified so that policy can be tightened later without touching
87
+ // every tool. HIGH and DESTRUCTIVE always require the caller to pass
88
+ // `confirm: true` — this is the approval gate.
89
+ export const PERMISSION_LEVELS = {
90
+ // Files
91
+ read_file: "READ",
92
+ read_multiple_files: "READ",
93
+ create_file: "MEDIUM",
94
+ write_file: "MEDIUM",
95
+ edit_file: "MEDIUM",
96
+ delete_file: "DESTRUCTIVE",
97
+ move_file: "MEDIUM",
98
+ copy_file: "LOW",
99
+ get_file_info: "READ",
100
+
101
+ // Directory
102
+ list_directory: "READ",
103
+ directory_tree: "READ",
104
+ create_directory: "LOW",
105
+ delete_directory: "DESTRUCTIVE",
106
+ search_files: "READ",
107
+
108
+ // Git
109
+ git_init: "LOW",
110
+ git_status: "READ",
111
+ git_add: "LOW",
112
+ git_commit: "MEDIUM",
113
+ git_push: "MEDIUM",
114
+ git_pull: "MEDIUM",
115
+ git_log: "READ",
116
+ git_diff: "READ",
117
+ git_branch: "READ",
118
+ git_create_branch: "LOW",
119
+ git_checkout: "MEDIUM",
120
+ git_merge: "MEDIUM",
121
+ git_reset: "HIGH",
122
+ git_stash: "LOW",
123
+ git_show: "READ",
124
+ git_remote: "MEDIUM",
125
+ git_tag: "LOW",
126
+ git_changed_files: "READ",
127
+ git_diff_stat: "READ",
128
+ git_check_clean: "READ",
129
+
130
+ // Command
131
+ run_command: "HIGH",
132
+
133
+ // GitHub
134
+ github_get_authenticated_user: "READ",
135
+ github_create_repo: "MEDIUM",
136
+ github_delete_repo: "DESTRUCTIVE",
137
+ github_list_repos: "READ",
138
+ github_create_issue: "LOW",
139
+ github_list_issues: "READ",
140
+ github_create_pull_request: "MEDIUM",
141
+ github_list_pull_requests: "READ",
142
+ github_add_comment: "LOW",
143
+ github_get_repo: "READ",
144
+ github_get_issue: "READ",
145
+ github_update_issue: "MEDIUM",
146
+ github_get_pull_request: "READ",
147
+ github_update_pull_request: "MEDIUM",
148
+ github_merge_pull_request: "HIGH",
149
+ github_get_pull_request_files: "READ",
150
+ github_get_pull_request_diff: "READ",
151
+ github_get_pull_request_comments: "READ",
152
+ github_get_pull_request_reviews: "READ",
153
+ github_get_branch: "READ",
154
+ github_list_branches: "READ",
155
+ github_list_workflows: "READ",
156
+ github_list_workflow_runs: "READ",
157
+ github_get_workflow_run: "READ",
158
+ github_get_workflow_run_jobs: "READ",
159
+ github_get_job_logs: "READ",
160
+ github_rerun_workflow: "MEDIUM",
161
+
162
+ // Vercel
163
+ vercel_get_authenticated_user: "READ",
164
+ vercel_list_projects: "READ",
165
+ vercel_get_project: "READ",
166
+ vercel_list_deployments: "READ",
167
+ vercel_get_deployment: "READ",
168
+ vercel_create_deployment: "HIGH",
169
+ vercel_delete_project: "DESTRUCTIVE",
170
+ vercel_get_deployment_logs: "READ",
171
+ vercel_get_deployment_events: "READ",
172
+ vercel_cancel_deployment: "MEDIUM",
173
+ http_check: "READ",
174
+
175
+ // Project intelligence
176
+ project_detect: "READ",
177
+ project_info: "READ",
178
+ project_health: "READ",
179
+ run_tests: "LOW",
180
+ run_lint: "LOW",
181
+ run_typecheck: "LOW",
182
+ run_build: "LOW",
183
+
184
+ // Workflows
185
+ ship_change: "HIGH",
186
+ fix_ci: "READ",
187
+ verify_ci_fix: "HIGH",
188
+ deploy_project: "HIGH",
189
+
190
+ // Slack
191
+ slack_get_user: "READ",
192
+ slack_list_channels: "READ",
193
+ slack_get_channel: "READ",
194
+ slack_read_messages: "READ",
195
+ slack_search_messages: "READ",
196
+ slack_send_message: "HIGH",
197
+ slack_reply_thread: "HIGH",
198
+ slack_create_channel: "HIGH",
199
+
200
+ // Gmail
201
+ gmail_get_profile: "READ",
202
+ gmail_search: "READ",
203
+ gmail_list_messages: "READ",
204
+ gmail_get_message: "READ",
205
+ gmail_get_thread: "READ",
206
+ gmail_send: "HIGH",
207
+ gmail_reply: "HIGH",
208
+ gmail_forward: "HIGH",
209
+
210
+ // Supabase
211
+ supabase_list_organizations: "READ",
212
+ supabase_list_projects: "READ",
213
+ supabase_get_project: "READ",
214
+ supabase_create_project: "MEDIUM",
215
+ supabase_delete_project: "DESTRUCTIVE",
216
+ supabase_run_sql: "HIGH",
217
+
218
+ // Database
219
+ postgres_query: "HIGH",
220
+ postgres_list_tables: "READ",
221
+ postgres_describe_table: "READ",
222
+ postgres_test_connection: "READ",
223
+ mysql_query: "HIGH",
224
+ mysql_list_tables: "READ",
225
+ mysql_describe_table: "READ",
226
+ mysql_test_connection: "READ",
227
+
228
+ // Secrets
229
+ secrets_set: "HIGH",
230
+ secrets_get: "MEDIUM",
231
+ secrets_list: "READ",
232
+ secrets_delete: "DESTRUCTIVE",
233
+ secrets_rotate_key: "HIGH",
234
+ secrets_generate_key: "LOW",
235
+
236
+ // Sentry
237
+ sentry_list_projects: "READ",
238
+ sentry_list_issues: "READ",
239
+ sentry_search_issues: "READ",
240
+ sentry_get_issue: "READ",
241
+ sentry_get_project_stats: "READ",
242
+ sentry_resolve_issue: "HIGH",
243
+ sentry_ignore_issue: "HIGH",
244
+ sentry_add_comment: "HIGH",
245
+
246
+ // Notion
247
+ notion_search: "READ",
248
+ notion_get_page: "READ",
249
+ notion_get_database: "READ",
250
+ notion_query_database: "READ",
251
+ notion_get_block_children: "READ",
252
+ notion_get_comments: "READ",
253
+ notion_list_users: "READ",
254
+ notion_get_user: "READ",
255
+ notion_create_page: "HIGH",
256
+ notion_update_page: "HIGH",
257
+ notion_create_database: "HIGH",
258
+ notion_append_block_children: "HIGH",
259
+ notion_update_block: "HIGH",
260
+ notion_add_comment: "HIGH",
261
+ notion_delete_block: "HIGH",
262
+
263
+ // Terraform
264
+ terraform_init: "LOW",
265
+ terraform_validate: "READ",
266
+ terraform_fmt: "MEDIUM",
267
+ terraform_plan: "LOW",
268
+ terraform_apply: "HIGH",
269
+ terraform_destroy: "DESTRUCTIVE",
270
+ terraform_show: "READ",
271
+ terraform_output: "READ",
272
+ terraform_workspace: "HIGH",
273
+ terraform_state_list: "READ",
274
+ terraform_state_show: "READ",
275
+ terraform_state_pull: "READ",
276
+ terraform_state_mv: "HIGH",
277
+ terraform_state_rm: "HIGH",
278
+ terraform_import: "HIGH",
279
+ terraform_taint: "HIGH",
280
+ terraform_untaint: "MEDIUM",
281
+ terraform_graph: "READ",
282
+ terraform_providers: "READ",
283
+ terraform_plan_comment: "MEDIUM",
284
+
285
+ // Docker
286
+ docker_version: "READ",
287
+ docker_ps: "READ",
288
+ docker_images: "READ",
289
+ docker_logs: "READ",
290
+ docker_inspect: "READ",
291
+ docker_stats: "READ",
292
+ docker_start: "LOW",
293
+ docker_pull: "MEDIUM",
294
+ docker_build: "HIGH",
295
+ docker_run: "HIGH",
296
+ docker_stop: "HIGH",
297
+ docker_restart: "HIGH",
298
+ docker_exec: "HIGH",
299
+ docker_push: "HIGH",
300
+ docker_compose_up: "HIGH",
301
+ docker_compose_down: "HIGH",
302
+ docker_remove: "DESTRUCTIVE",
303
+ docker_remove_image: "DESTRUCTIVE",
304
+
305
+ // Toolset management
306
+ search_tools: "READ",
307
+ load_toolset: "LOW",
308
+ use_tool: "READ", // only forwards; the called tool applies its own risk level and approvals
309
+
310
+ // Reminders
311
+ reminder_create: "LOW",
312
+ reminder_list: "READ",
313
+ reminder_update: "LOW",
314
+
315
+ // Research
316
+ web_search: "READ",
317
+ web_fetch: "MEDIUM",
318
+
319
+ // Leads
320
+ lead_hn_hiring: "READ",
321
+ lead_remote_jobs: "READ",
322
+ lead_company_contacts: "MEDIUM",
323
+
324
+ // SEO and content
325
+ seo_keyword_ideas: "READ",
326
+ seo_page_audit: "MEDIUM",
327
+ seo_content_check: "READ",
328
+ seo_search_console: "READ",
329
+
330
+ // Pipeline
331
+ pipeline_add_lead: "LOW",
332
+ pipeline_list: "READ",
333
+ pipeline_get: "READ",
334
+ pipeline_update: "LOW",
335
+ pipeline_log_activity: "LOW",
336
+ pipeline_add_contact: "LOW",
337
+ pipeline_report: "READ",
338
+ pipeline_export: "MEDIUM",
339
+ pipeline_delete_deal: "DESTRUCTIVE",
340
+
341
+ // Google Calendar (inviting guests is gated separately inside the tools)
342
+ calendar_list_calendars: "READ",
343
+ calendar_list_events: "READ",
344
+ calendar_get_event: "READ",
345
+ calendar_free_busy: "READ",
346
+ calendar_find_slots: "READ",
347
+ calendar_create_event: "MEDIUM",
348
+ calendar_update_event: "MEDIUM",
349
+ calendar_delete_event: "DESTRUCTIVE",
350
+
351
+ // Meeting notes (transcribing uploads audio to Groq, so it needs confirm)
352
+ meeting_transcribe: "HIGH",
353
+ meeting_save: "MEDIUM",
354
+ meeting_list: "READ",
355
+ meeting_get: "READ",
356
+ meeting_search: "READ",
357
+ meeting_actions: "READ",
358
+ meeting_complete_action: "LOW",
359
+ meeting_prep: "READ",
360
+ meeting_delete: "DESTRUCTIVE",
361
+
362
+ // Telegram (notify only reaches the owner's own chat)
363
+ telegram_get_me: "READ",
364
+ telegram_get_updates: "READ",
365
+ telegram_get_chat: "READ",
366
+ telegram_notify: "LOW",
367
+ telegram_send_message: "HIGH",
368
+
369
+ // Linear
370
+ linear_get_viewer: "READ",
371
+ linear_list_teams: "READ",
372
+ linear_list_issues: "READ",
373
+ linear_get_issue: "READ",
374
+ linear_create_issue: "MEDIUM",
375
+ linear_update_issue: "MEDIUM",
376
+ linear_add_comment: "MEDIUM",
377
+
378
+ // Stripe (live keys are refused unless STRIPE_ALLOW_LIVE=true; anything that emails or moves money is HIGH)
379
+ stripe_get_account: "READ",
380
+ stripe_list_customers: "READ",
381
+ stripe_create_customer: "MEDIUM",
382
+ stripe_list_invoices: "READ",
383
+ stripe_create_invoice: "MEDIUM",
384
+ stripe_send_invoice: "HIGH",
385
+ stripe_create_payment_link: "HIGH",
386
+ stripe_list_payments: "READ",
387
+ stripe_refund: "HIGH",
388
+
389
+ // Discord
390
+ discord_get_me: "READ",
391
+ discord_list_guilds: "READ",
392
+ discord_list_channels: "READ",
393
+ discord_read_messages: "READ",
394
+ discord_send_message: "HIGH",
395
+ };
396
+
397
+ const APPROVAL_REQUIRED_LEVELS = new Set(["HIGH", "DESTRUCTIVE"]);
398
+
399
+ /**
400
+ * Checks whether a tool call is allowed to proceed. HIGH and DESTRUCTIVE
401
+ * tools require the caller to explicitly pass `confirm: true` in their
402
+ * input — this is the approval gate for this single-user trusted-machine
403
+ * server (no separate human-approval UI exists at the MCP layer, so the
404
+ * explicit flag stands in for it: the AI must consciously decide to
405
+ * proceed, typically after confirming with the person).
406
+ *
407
+ * Returns { allowed, level, reason }. Never throws.
408
+ */
409
+ export function checkApproval(toolName, input) {
410
+ const level = PERMISSION_LEVELS[toolName] || "MEDIUM";
411
+ if (!APPROVAL_REQUIRED_LEVELS.has(level)) {
412
+ return { allowed: true, level };
413
+ }
414
+ if (input && input.confirm === true) {
415
+ return { allowed: true, level };
416
+ }
417
+ return {
418
+ allowed: false,
419
+ level,
420
+ reason: `'${toolName}' is a ${level}-risk action and requires explicit confirmation. Re-call with confirm: true to proceed.`,
421
+ };
422
+ }
423
+
424
+ // ---------------- Command risk classification ----------------
425
+
426
+ // Beyond the hard BLOCKED_PATTERNS in commandOps.js (which stop a call
427
+ // outright), this gives a *graded* risk read on a command string so it can
428
+ // be surfaced in logs/results for auditability — per the roadmap's own
429
+ // warning: "Do not rely only on regex-based destructive command blocking."
430
+ const RISKY_COMMAND_SIGNALS = [
431
+ { pattern: /\b(rm|del|erase)\b/i, reason: "deletes files" },
432
+ { pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "drops a database object" },
433
+ { pattern: /\btruncate\s+table\b/i, reason: "truncates a table" },
434
+ { pattern: /\bgit\s+push\s+.*--force/i, reason: "force-pushes (can overwrite remote history)" },
435
+ { pattern: /\bgit\s+reset\s+--hard/i, reason: "hard-resets (can discard uncommitted work)" },
436
+ { pattern: /\bcurl\b.*\|\s*(bash|sh|powershell)/i, reason: "pipes a remote script directly into a shell" },
437
+ { pattern: /\biwr\b.*\|\s*iex\b/i, reason: "downloads and executes a remote script (PowerShell)" },
438
+ { pattern: /\bchmod\s+-R\s+777\b/i, reason: "recursively opens permissions" },
439
+ { pattern: /\bsudo\b|\brunas\b/i, reason: "elevates privileges" },
440
+ { pattern: />\s*\/dev\/(sd|nvme|hd)/i, reason: "writes directly to a disk device" },
441
+ { pattern: /\bnpm\s+publish\b/i, reason: "publishes a package publicly" },
442
+ { pattern: /\bgh\s+repo\s+delete\b/i, reason: "deletes a GitHub repository" },
443
+ ];
444
+
445
+ /**
446
+ * Returns a risk read on a shell command string: { risk: "LOW"|"ELEVATED",
447
+ * signals: [reasons] }. Purely informational (does not block) — the hard
448
+ * BLOCKED_PATTERNS check in commandOps.js still runs separately and does
449
+ * block outright. This exists so risky-but-legitimate commands (force
450
+ * push, drop table, etc.) are visible in the audit log even when allowed.
451
+ */
452
+ export function classifyCommandRisk(command) {
453
+ const signals = RISKY_COMMAND_SIGNALS.filter((s) => s.pattern.test(command)).map((s) => s.reason);
454
+ return { risk: signals.length ? "ELEVATED" : "LOW", signals };
455
+ }
456
+
457
+ // ---------------- Path security ----------------
458
+
459
+ // System-critical directories that must never be targeted by delete/write
460
+ // operations, even on a trusted single-user machine — a wrong path here
461
+ // is the difference between "oops, undo" and "reinstall Windows". Matched
462
+ // case-insensitively against the resolved absolute path prefix.
463
+ const DENIED_PATH_PREFIXES = [
464
+ "C:\\Windows",
465
+ "C:\\Program Files",
466
+ "C:\\Program Files (x86)",
467
+ "C:\\ProgramData",
468
+ "C:\\Users\\Default",
469
+ "C:\\Users\\Public",
470
+ "C:\\System Volume Information",
471
+ "C:\\Recovery",
472
+ "C:\\Boot",
473
+ ];
474
+
475
+ /**
476
+ * Checks whether a path falls inside a denied system directory. Compares
477
+ * the normalized, lowercased path against known-dangerous prefixes.
478
+ * Returns { denied, reason }.
479
+ */
480
+ export function isPathDenied(targetPath) {
481
+ if (!targetPath || typeof targetPath !== "string") return { denied: false };
482
+ const normalized = targetPath.replace(/\//g, "\\").toLowerCase();
483
+ const hit = DENIED_PATH_PREFIXES.find((prefix) => normalized.startsWith(prefix.toLowerCase()));
484
+ if (hit) {
485
+ return { denied: true, reason: `'${targetPath}' is inside a protected system directory (${hit}) and cannot be modified through this tool.` };
486
+ }
487
+ return { denied: false };
488
+ }
@@ -0,0 +1,130 @@
1
+ const SENTRY_HOSTS = ["https://sentry.io/api/0", "https://de.sentry.io/api/0"];
2
+
3
+ function getToken() {
4
+ const token = process.env.SENTRY_AUTH_TOKEN;
5
+ if (!token) {
6
+ throw new Error(
7
+ "SENTRY_AUTH_TOKEN not set. Create one at https://sentry.io/settings/account/api/auth-tokens/ and add it to your .env file: SENTRY_AUTH_TOKEN=..."
8
+ );
9
+ }
10
+ return token;
11
+ }
12
+
13
+ async function sentryFetchOnHost(host, path, { method = "GET", body } = {}) {
14
+ const token = getToken();
15
+ const res = await fetch(`${host}${path}`, {
16
+ method,
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ "Content-Type": "application/json",
20
+ },
21
+ body: body ? JSON.stringify(body) : undefined,
22
+ });
23
+ const text = await res.text();
24
+ const data = text ? JSON.parse(text) : {};
25
+ return { ok: res.ok, status: res.status, data };
26
+ }
27
+
28
+ /**
29
+ * Some Sentry endpoints (notably bare /issues/{id}/ lookups) only resolve on
30
+ * the organization's actual data-residency host (e.g. de.sentry.io for EU
31
+ * orgs), even though org/project-scoped endpoints work fine on sentry.io.
32
+ * We try the default host first, and on a 404 transparently retry on the
33
+ * other known region host before giving up.
34
+ */
35
+ async function sentryFetch(path, opts = {}) {
36
+ let last;
37
+ for (const host of SENTRY_HOSTS) {
38
+ const result = await sentryFetchOnHost(host, path, opts);
39
+ if (result.ok) return result.data;
40
+ last = result;
41
+ if (result.status !== 404) break;
42
+ }
43
+ throw new Error(`Sentry API error (${path}): ${last.data.detail || last.status}`);
44
+ }
45
+
46
+ function simplifyIssue(i) {
47
+ return {
48
+ id: i.id,
49
+ shortId: i.shortId,
50
+ title: i.title,
51
+ culprit: i.culprit,
52
+ level: i.level,
53
+ status: i.status,
54
+ count: i.count,
55
+ userCount: i.userCount,
56
+ firstSeen: i.firstSeen,
57
+ lastSeen: i.lastSeen,
58
+ permalink: i.permalink,
59
+ };
60
+ }
61
+
62
+ /** List projects in the organization. */
63
+ export async function sentryListProjects({ org_slug }) {
64
+ const data = await sentryFetch(`/organizations/${org_slug}/projects/`);
65
+ return {
66
+ projects: data.map((p) => ({ id: p.id, slug: p.slug, name: p.name, platform: p.platform })),
67
+ };
68
+ }
69
+
70
+ /** List recent issues (errors) for a project. */
71
+ export async function sentryListIssues({ org_slug, project_slug, query, limit = 25 }) {
72
+ const params = new URLSearchParams({ limit: String(limit) });
73
+ if (query) params.set("query", query);
74
+ const data = await sentryFetch(`/projects/${org_slug}/${project_slug}/issues/?${params}`);
75
+ return { issues: data.map(simplifyIssue) };
76
+ }
77
+
78
+ /** Search issues with a Sentry query string (e.g. "is:unresolved level:error"). */
79
+ export async function sentrySearchIssues({ org_slug, project_slug, query, limit = 25 }) {
80
+ return sentryListIssues({ org_slug, project_slug, query, limit });
81
+ }
82
+
83
+ /** Get a single issue's full detail (stack trace context, occurrence counts). */
84
+ export async function sentryGetIssue({ issue_id }) {
85
+ const data = await sentryFetch(`/issues/${issue_id}/`);
86
+ return {
87
+ ...simplifyIssue(data),
88
+ metadata: data.metadata,
89
+ type: data.type,
90
+ platform: data.platform,
91
+ };
92
+ }
93
+
94
+ /** Mark an issue as resolved. HIGH risk — requires confirm: true. */
95
+ export async function sentryResolveIssue({ issue_id, confirm }) {
96
+ const data = await sentryFetch(`/issues/${issue_id}/`, {
97
+ method: "PUT",
98
+ body: { status: "resolved" },
99
+ });
100
+ return { id: data.id, status: data.status };
101
+ }
102
+
103
+ /** Mute/ignore an issue so it stops notifying. HIGH risk — requires confirm: true. */
104
+ export async function sentryIgnoreIssue({ issue_id, confirm }) {
105
+ const data = await sentryFetch(`/issues/${issue_id}/`, {
106
+ method: "PUT",
107
+ body: { status: "ignored" },
108
+ });
109
+ return { id: data.id, status: data.status };
110
+ }
111
+
112
+ /** Get error-count stats/trends for a project over a time period. */
113
+ export async function sentryGetProjectStats({ org_slug, project_slug, stat = "received", period = "24h" }) {
114
+ const params = new URLSearchParams({ stat, resolution: "1h" });
115
+ const data = await sentryFetch(`/projects/${org_slug}/${project_slug}/stats/?${params}`);
116
+ return {
117
+ period,
118
+ stat,
119
+ points: data.map(([timestamp, value]) => ({ timestamp, value })),
120
+ };
121
+ }
122
+
123
+ /** Add a comment/note to an issue. HIGH risk — requires confirm: true. */
124
+ export async function sentryAddComment({ issue_id, text, confirm }) {
125
+ const data = await sentryFetch(`/issues/${issue_id}/comments/`, {
126
+ method: "POST",
127
+ body: { text },
128
+ });
129
+ return { id: data.id, posted: true };
130
+ }