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,371 @@
1
+ import * as gitOps from "./gitOps.js";
2
+ import * as githubOps from "./githubOps.js";
3
+ import * as projectOps from "./projectOps.js";
4
+ import * as vercelOps from "./vercelOps.js";
5
+
6
+ /**
7
+ * Extract "owner" and "repo" from a git remote URL like
8
+ * https://github.com/owner/repo.git or git@github.com:owner/repo.git
9
+ */
10
+ function parseOwnerRepoFromRemoteUrl(url) {
11
+ if (!url) return null;
12
+ const httpsMatch = url.match(/github\.com[/:]([^/]+)\/([^/.]+)(\.git)?$/);
13
+ if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
14
+ return null;
15
+ }
16
+
17
+ /**
18
+ * fix_ci — investigate a failed GitHub Actions run and prepare the fix loop.
19
+ *
20
+ * This does NOT write code fixes itself (that requires the AI's own
21
+ * reasoning + file tools). Instead it does the mechanical investigation
22
+ * work end-to-end — find the failing run, find the failing job(s), pull
23
+ * the actual log text — so the AI can read the logs, make the fix with
24
+ * file tools, then call `verify_ci_fix` (below) to commit/push and confirm
25
+ * the run goes green.
26
+ *
27
+ * Flow:
28
+ * find latest failing run on branch -> list its jobs -> find failed jobs
29
+ * -> pull logs for each failed job -> return structured diagnosis
30
+ */
31
+ export async function fixCi({ owner, repo, branch, run_id }) {
32
+ const steps = [];
33
+
34
+ // 1. Resolve the run to investigate
35
+ let run;
36
+ if (run_id) {
37
+ run = await githubOps.githubGetWorkflowRun({ owner, repo, run_id });
38
+ steps.push({ step: "get_workflow_run", run });
39
+ } else {
40
+ const runs = await githubOps.githubListWorkflowRuns({ owner, repo, branch, status: "failure", per_page: 5 });
41
+ steps.push({ step: "list_workflow_runs", found: runs.runs?.length || 0 });
42
+ if (!runs.runs || runs.runs.length === 0) {
43
+ return { ok: false, reason: `No failing workflow runs found${branch ? ` on branch '${branch}'` : ""}.`, steps };
44
+ }
45
+ run = runs.runs[0]; // most recent
46
+ }
47
+
48
+ if (run.conclusion && run.conclusion !== "failure" && run.status !== "in_progress") {
49
+ return { ok: false, reason: `Run ${run.id} is not currently failing (conclusion: ${run.conclusion}).`, steps, run };
50
+ }
51
+
52
+ // 2. Get jobs for that run
53
+ const jobsResult = await githubOps.githubGetWorkflowRunJobs({ owner, repo, run_id: run.id });
54
+ steps.push({ step: "get_workflow_run_jobs", job_count: jobsResult.jobs?.length || 0 });
55
+
56
+ const failedJobs = (jobsResult.jobs || []).filter((j) => j.conclusion === "failure");
57
+ if (failedJobs.length === 0) {
58
+ return { ok: false, reason: `No failed jobs found in run ${run.id} — it may still be in progress.`, steps, run, jobs: jobsResult.jobs };
59
+ }
60
+
61
+ // 3. Pull logs for each failed job
62
+ const diagnosis = [];
63
+ for (const job of failedJobs) {
64
+ try {
65
+ const logResult = await githubOps.githubGetJobLogs({ owner, repo, job_id: job.id });
66
+ const failedSteps = (job.steps || []).filter((s) => s.conclusion === "failure");
67
+ diagnosis.push({
68
+ job_id: job.id,
69
+ job_name: job.name,
70
+ failed_steps: failedSteps.map((s) => s.name),
71
+ html_url: job.html_url,
72
+ // Trim to the tail — that's where the actual error usually is, and
73
+ // full CI logs can be enormous.
74
+ logs_tail: logResult.logs.slice(-8000),
75
+ });
76
+ } catch (err) {
77
+ diagnosis.push({ job_id: job.id, job_name: job.name, error: `Could not fetch logs: ${err.message}` });
78
+ }
79
+ }
80
+ steps.push({ step: "fetch_job_logs", jobs_diagnosed: diagnosis.length });
81
+
82
+ return {
83
+ ok: true,
84
+ run: { id: run.id, branch: run.branch || run.head_branch, html_url: run.html_url },
85
+ failed_jobs: diagnosis,
86
+ steps,
87
+ next_step:
88
+ "Read logs_tail for each failed job, fix the code with file tools, then call verify_ci_fix to commit, push, and confirm CI passes.",
89
+ };
90
+ }
91
+
92
+ /**
93
+ * verify_ci_fix — after the AI has made a code fix on disk, commit + push
94
+ * it and poll the new workflow run until it finishes, reporting the result.
95
+ *
96
+ * Flow:
97
+ * commit -> push -> poll new run on branch until complete -> report pass/fail
98
+ */
99
+ export async function verifyCiFix({
100
+ repo_path,
101
+ owner,
102
+ repo,
103
+ branch,
104
+ commit_message,
105
+ poll_interval_ms = 15_000,
106
+ max_polls = 20,
107
+ }) {
108
+ const steps = [];
109
+
110
+ try {
111
+ await gitOps.gitAdd({ repo_path, files: "." });
112
+ const commitResult = await gitOps.gitCommit({ repo_path, message: commit_message });
113
+ steps.push({ step: "commit", ...commitResult });
114
+ } catch (err) {
115
+ return { ok: false, failed_step: "commit", reason: err.message, steps };
116
+ }
117
+
118
+ try {
119
+ const pushResult = await gitOps.gitPush({ repo_path, branch });
120
+ steps.push({ step: "push", ...pushResult });
121
+ } catch (err) {
122
+ return { ok: false, failed_step: "push", reason: err.message, steps };
123
+ }
124
+
125
+ // Poll for the new run to appear and finish
126
+ let latestRun = null;
127
+ for (let i = 0; i < max_polls; i++) {
128
+ await new Promise((resolve) => setTimeout(resolve, poll_interval_ms));
129
+ const runs = await githubOps.githubListWorkflowRuns({ owner, repo, branch, per_page: 1 });
130
+ latestRun = runs.runs?.[0] || null;
131
+ if (latestRun && latestRun.status === "completed") break;
132
+ }
133
+ steps.push({ step: "poll_workflow_run", final_status: latestRun?.status, conclusion: latestRun?.conclusion });
134
+
135
+ if (!latestRun) {
136
+ return { ok: false, failed_step: "poll_workflow_run", reason: "No workflow run found after push.", steps };
137
+ }
138
+ if (latestRun.status !== "completed") {
139
+ return { ok: null, reason: "Run still in progress after max_polls — check back later.", run: latestRun, steps };
140
+ }
141
+
142
+ return {
143
+ ok: latestRun.conclusion === "success",
144
+ run: latestRun,
145
+ steps,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * ship_change — take a set of already-made file edits from request to PR.
151
+ *
152
+ * Assumes the caller (the AI, using file/directory tools) has already made
153
+ * the code changes on disk. This workflow does NOT edit files; it takes the
154
+ * project from "dirty working tree" to "open pull request", running checks
155
+ * along the way and stopping early if anything fails.
156
+ *
157
+ * Flow:
158
+ * inspect changes -> create branch -> run checks -> commit -> push -> open PR
159
+ */
160
+ export async function shipChange({
161
+ repo_path,
162
+ branch_name,
163
+ commit_message,
164
+ pr_title,
165
+ pr_body = "",
166
+ base = "main",
167
+ owner,
168
+ repo,
169
+ run_checks = true,
170
+ skip_if_no_changes = true,
171
+ }) {
172
+ const steps = [];
173
+ const fail = (step, reason, extra = {}) => ({
174
+ ok: false,
175
+ failed_step: step,
176
+ reason,
177
+ steps,
178
+ ...extra,
179
+ });
180
+
181
+ // 1. Inspect current changes
182
+ const changed = await gitOps.gitChangedFiles({ repo_path });
183
+ steps.push({ step: "inspect_changes", changed_files: changed.changed_files });
184
+ if (skip_if_no_changes && changed.changed_files.length === 0) {
185
+ return fail("inspect_changes", "No changes detected in the working tree — nothing to ship.");
186
+ }
187
+
188
+ // 2. Create + checkout branch (fall back to checkout if it already exists)
189
+ try {
190
+ const branchResult = await gitOps.gitCreateBranch({ repo_path, branch_name, checkout: true });
191
+ steps.push({ step: "create_branch", ...branchResult });
192
+ } catch (err) {
193
+ if (/already exists/i.test(err.message)) {
194
+ try {
195
+ const checkoutResult = await gitOps.gitCheckout({ repo_path, branch: branch_name });
196
+ steps.push({ step: "checkout_existing_branch", ...checkoutResult });
197
+ } catch (checkoutErr) {
198
+ return fail("create_branch", checkoutErr.message);
199
+ }
200
+ } else {
201
+ return fail("create_branch", err.message);
202
+ }
203
+ }
204
+
205
+ // 3. Run checks (tests/lint/typecheck/build) — best-effort, non-fatal per check
206
+ const checks = {};
207
+ if (run_checks) {
208
+ for (const [name, fn] of Object.entries({
209
+ tests: projectOps.runTests,
210
+ lint: projectOps.runLint,
211
+ typecheck: projectOps.runTypecheck,
212
+ build: projectOps.runBuild,
213
+ })) {
214
+ const result = await fn({ repo_path });
215
+ checks[name] = result;
216
+ if (result.ran && result.exit_code !== 0) {
217
+ steps.push({ step: `check_${name}`, ...result });
218
+ return fail(`check_${name}`, `${name} failed (exit code ${result.exit_code}). Fix the issue and retry.`, { checks });
219
+ }
220
+ }
221
+ steps.push({ step: "run_checks", checks });
222
+ }
223
+
224
+ // 4. Stage + commit
225
+ try {
226
+ await gitOps.gitAdd({ repo_path, files: "." });
227
+ const commitResult = await gitOps.gitCommit({ repo_path, message: commit_message });
228
+ steps.push({ step: "commit", ...commitResult });
229
+ } catch (err) {
230
+ return fail("commit", err.message);
231
+ }
232
+
233
+ // 5. Push
234
+ try {
235
+ const pushResult = await gitOps.gitPush({ repo_path, branch: branch_name });
236
+ steps.push({ step: "push", ...pushResult });
237
+ } catch (err) {
238
+ return fail("push", err.message);
239
+ }
240
+
241
+ // 6. Resolve owner/repo if not given, from the git remote
242
+ let resolvedOwner = owner;
243
+ let resolvedRepo = repo;
244
+ if (!resolvedOwner || !resolvedRepo) {
245
+ const remotes = await gitOps.gitRemote({ repo_path, action: "list" });
246
+ const origin = remotes.result?.find((r) => r.name === "origin");
247
+ const parsed = parseOwnerRepoFromRemoteUrl(origin?.refs?.fetch);
248
+ if (parsed) {
249
+ resolvedOwner = resolvedOwner || parsed.owner;
250
+ resolvedRepo = resolvedRepo || parsed.repo;
251
+ }
252
+ }
253
+ if (!resolvedOwner || !resolvedRepo) {
254
+ return fail("create_pull_request", "Could not resolve owner/repo — pass them explicitly.", { checks });
255
+ }
256
+
257
+ // 7. Open PR
258
+ try {
259
+ const pr = await githubOps.githubCreatePullRequest({
260
+ owner: resolvedOwner,
261
+ repo: resolvedRepo,
262
+ title: pr_title || commit_message,
263
+ head: branch_name,
264
+ base,
265
+ body: pr_body,
266
+ });
267
+ steps.push({ step: "create_pull_request", ...pr });
268
+ return { ok: true, pull_request: pr, checks, steps };
269
+ } catch (err) {
270
+ return fail("create_pull_request", err.message, { checks });
271
+ }
272
+ }
273
+
274
+ /**
275
+ * deploy_project — safely deploy a project and verify it actually works.
276
+ *
277
+ * Flow:
278
+ * project health -> tests -> build -> trigger deployment -> poll until
279
+ * ready -> if ready, HTTP health check the live URL -> verified result
280
+ *
281
+ * Stops early (without deploying) if health/tests/build fail, so broken
282
+ * code never reaches a deployment.
283
+ */
284
+ export async function deployProject({
285
+ repo_path,
286
+ project,
287
+ git_source_repo,
288
+ git_source_ref = "main",
289
+ deployment_name,
290
+ health_check_url,
291
+ run_checks = true,
292
+ poll_interval_ms = 10_000,
293
+ max_polls = 30,
294
+ }) {
295
+ const steps = [];
296
+ const fail = (step, reason, extra = {}) => ({ ok: false, failed_step: step, reason, steps, ...extra });
297
+
298
+ // 1. Project health
299
+ const health = await projectOps.projectHealth({ repo_path });
300
+ steps.push({ step: "project_health", ...health });
301
+ if (health.git_status && health.git_status.is_clean === false) {
302
+ return fail("project_health", "Working tree has uncommitted changes — commit or stash before deploying.", { health });
303
+ }
304
+
305
+ // 2. Tests + build (best-effort skip if project has no such script)
306
+ const checks = {};
307
+ if (run_checks) {
308
+ for (const [name, fn] of Object.entries({ tests: projectOps.runTests, build: projectOps.runBuild })) {
309
+ const result = await fn({ repo_path });
310
+ checks[name] = result;
311
+ if (result.ran && result.exit_code !== 0) {
312
+ steps.push({ step: `check_${name}`, ...result });
313
+ return fail(`check_${name}`, `${name} failed (exit code ${result.exit_code}) — deployment aborted.`, { checks });
314
+ }
315
+ }
316
+ steps.push({ step: "run_checks", checks });
317
+ }
318
+
319
+ // 3. Trigger deployment
320
+ let deployment;
321
+ try {
322
+ deployment = await vercelOps.vercelCreateDeployment({
323
+ name: deployment_name || project,
324
+ project,
325
+ git_source_repo,
326
+ git_source_ref,
327
+ });
328
+ steps.push({ step: "trigger_deployment", ...deployment });
329
+ } catch (err) {
330
+ return fail("trigger_deployment", err.message, { checks });
331
+ }
332
+
333
+ // 4. Poll until ready or errored
334
+ let finalState = null;
335
+ for (let i = 0; i < max_polls; i++) {
336
+ await new Promise((resolve) => setTimeout(resolve, poll_interval_ms));
337
+ const events = await vercelOps.vercelGetDeploymentEvents({ deployment_id: deployment.uid });
338
+ finalState = events;
339
+ if (events.state === "READY" || events.state === "ERROR" || events.state === "CANCELED") break;
340
+ }
341
+ steps.push({ step: "poll_deployment", final_state: finalState?.state });
342
+
343
+ if (!finalState || finalState.state !== "READY") {
344
+ // Pull build logs to help diagnose the failure
345
+ let logs = null;
346
+ try {
347
+ logs = await vercelOps.vercelGetDeploymentLogs({ deployment_id: deployment.uid, limit: 200 });
348
+ } catch {
349
+ // best-effort — logs are a bonus, not required for the failure report
350
+ }
351
+ return fail("poll_deployment", `Deployment did not become ready (state: ${finalState?.state || "unknown"}).`, {
352
+ checks,
353
+ deployment,
354
+ build_errors: finalState?.build_errors,
355
+ logs,
356
+ });
357
+ }
358
+
359
+ // 5. HTTP health check the live URL
360
+ const url = health_check_url || `https://${deployment.url}`;
361
+ const httpResult = await vercelOps.httpCheck({ url });
362
+ steps.push({ step: "http_check", ...httpResult });
363
+
364
+ return {
365
+ ok: httpResult.healthy,
366
+ deployment,
367
+ checks,
368
+ http_check: httpResult,
369
+ steps,
370
+ };
371
+ }
@@ -0,0 +1,85 @@
1
+ // Workspace jail: optionally restrict file/path arguments to approved folders.
2
+ //
3
+ // Off by default. Turn on by setting CAUSLY_WORKSPACE to one or more folders,
4
+ // separated by ";" on Windows or ":" on macOS/Linux, e.g.
5
+ // CAUSLY_WORKSPACE=D:\causly-server;D:\causly-site
6
+ //
7
+ // This guards the path arguments of tool calls (files, directories, cwd,
8
+ // repo_path, terraform dir, ...). It is a guardrail, not a sandbox: shell
9
+ // command strings and docker volume mounts are not inspected.
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+ // Tool input fields that hold a filesystem path (or an array of paths).
14
+ export const PATH_FIELDS = [
15
+ "path",
16
+ "paths",
17
+ "root_path",
18
+ "cwd",
19
+ "repo_path",
20
+ "dir",
21
+ "project_dir",
22
+ "context_dir",
23
+ "dockerfile",
24
+ "source",
25
+ "destination",
26
+ "store_path",
27
+ "plan_file",
28
+ "var_file",
29
+ "transcript_file",
30
+ ];
31
+
32
+ // Resolve symlinks in the deepest existing part of a path, then re-attach the
33
+ // parts that don't exist yet (so new files are checked correctly and a symlink
34
+ // inside the workspace can't point outside it).
35
+ function realish(p) {
36
+ let current = path.resolve(p);
37
+ const missing = [];
38
+ for (;;) {
39
+ try {
40
+ const real = fs.realpathSync.native(current);
41
+ return missing.length ? path.join(real, ...missing.reverse()) : real;
42
+ } catch {
43
+ const parent = path.dirname(current);
44
+ if (parent === current) return path.resolve(p);
45
+ missing.push(path.basename(current));
46
+ current = parent;
47
+ }
48
+ }
49
+ }
50
+
51
+ const normalize = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
52
+
53
+ export function isInsideRoot(root, target) {
54
+ const rel = path.relative(normalize(realish(root)), normalize(realish(target)));
55
+ if (rel === "") return true;
56
+ if (path.isAbsolute(rel)) return false; // e.g. a different drive
57
+ return rel !== ".." && !rel.startsWith(".." + path.sep);
58
+ }
59
+
60
+ export function workspaceRoots(value = process.env.CAUSLY_WORKSPACE) {
61
+ return (value || "")
62
+ .split(path.delimiter)
63
+ .map((s) => s.trim())
64
+ .filter(Boolean)
65
+ .map((s) => path.resolve(s));
66
+ }
67
+
68
+ /** Returns { allowed: true } or { allowed: false, reason }. */
69
+ export function checkWorkspace(toolName, input, value = process.env.CAUSLY_WORKSPACE) {
70
+ const roots = workspaceRoots(value);
71
+ if (!roots.length) return { allowed: true };
72
+ for (const field of PATH_FIELDS) {
73
+ const raw = input?.[field];
74
+ for (const v of Array.isArray(raw) ? raw : [raw]) {
75
+ if (typeof v !== "string" || !v) continue;
76
+ if (!roots.some((root) => isInsideRoot(root, v))) {
77
+ return {
78
+ allowed: false,
79
+ reason: `'${toolName}' ${field} "${v}" is outside the allowed workspace (CAUSLY_WORKSPACE).`,
80
+ };
81
+ }
82
+ }
83
+ }
84
+ return { allowed: true };
85
+ }