@mukulaggarwal/pacman 0.1.4 → 0.1.6

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createNoopEventClient,
3
3
  validateIntegrationConfig
4
- } from "./chunk-DNI6TIXZ.js";
4
+ } from "./chunk-LKKDJ5A7.js";
5
5
  import {
6
6
  createContextManager
7
7
  } from "./chunk-UWT6AFJB.js";
@@ -17,14 +17,14 @@ import {
17
17
  import {
18
18
  createSlackConnector,
19
19
  validateSlackAppToken
20
- } from "./chunk-YJ32S56Q.js";
20
+ } from "./chunk-X6CHTBN2.js";
21
21
 
22
22
  // src/onboarding-server.ts
23
23
  import express from "express";
24
24
  import * as path2 from "path";
25
25
  import * as fs2 from "fs/promises";
26
- import { execFile } from "child_process";
27
- import { promisify } from "util";
26
+ import { execFile as execFile2 } from "child_process";
27
+ import { promisify as promisify2 } from "util";
28
28
 
29
29
  // ../template-engine/dist/index.js
30
30
  var PROFILE_TEMPLATES = {
@@ -65,6 +65,70 @@ Software Engineer
65
65
 
66
66
  ## Tech Stack
67
67
  <!-- Languages, frameworks, tools -->
68
+ `
69
+ },
70
+ {
71
+ name: "Context Details",
72
+ fileName: "context-details.md",
73
+ required: false,
74
+ defaultContent: `# Context Details
75
+
76
+ ## Executive Summary
77
+ <!-- One-paragraph project overview: status, owner, key objective, current phase -->
78
+
79
+ ## Key Context
80
+
81
+ | Attribute | Value |
82
+ |-----------|-------|
83
+ | Tech Stack | <!-- Languages, frameworks, key libraries --> |
84
+ | Team Size | <!-- Number of engineers --> |
85
+ | Dependencies | <!-- Upstream/downstream services --> |
86
+ | Blockers | <!-- Current blockers or risks --> |
87
+ | Repo(s) | <!-- Primary repositories --> |
88
+ | Deploy Target | <!-- Where this runs (k8s, lambda, etc.) --> |
89
+
90
+ ## Timeline
91
+
92
+ | Milestone | Date | Status | Owner |
93
+ |-----------|------|--------|-------|
94
+ | <!-- Milestone 1 --> | <!-- Date --> | <!-- On track / At risk / Done --> | <!-- Owner --> |
95
+ | <!-- Milestone 2 --> | <!-- Date --> | <!-- Status --> | <!-- Owner --> |
96
+
97
+ ## Active Decisions
98
+
99
+ | Decision | Options | Status | Deadline | Owner |
100
+ |----------|---------|--------|----------|-------|
101
+ | <!-- Decision 1 --> | <!-- A / B / C --> | <!-- Open / Decided --> | <!-- Date --> | <!-- Owner --> |
102
+
103
+ ## Metrics & Health
104
+
105
+ | Metric | Target | Current | Trend |
106
+ |--------|--------|---------|-------|
107
+ | Build time | <!-- e.g. < 5 min --> | <!-- Current --> | <!-- Up / Down / Stable --> |
108
+ | Test coverage | <!-- e.g. > 80% --> | <!-- Current --> | <!-- Trend --> |
109
+ | P0 bugs | <!-- e.g. 0 --> | <!-- Current --> | <!-- Trend --> |
110
+ | Deploy frequency | <!-- e.g. daily --> | <!-- Current --> | <!-- Trend --> |
111
+
112
+ ## Communication Map
113
+
114
+ | Stakeholder | Role | Channel | Cadence |
115
+ |-------------|------|---------|---------|
116
+ | <!-- Name --> | <!-- Role --> | <!-- Slack / Email / Meeting --> | <!-- Weekly / Ad-hoc --> |
117
+
118
+ ## Risk Register
119
+
120
+ | Risk | Likelihood | Impact | Mitigation | Owner |
121
+ |------|-----------|--------|------------|-------|
122
+ | <!-- Risk 1 --> | <!-- Low / Med / High --> | <!-- Low / Med / High --> | <!-- Mitigation plan --> | <!-- Owner --> |
123
+
124
+ ## Quick Reference
125
+
126
+ - **Primary repo:** <!-- link -->
127
+ - **CI dashboard:** <!-- link -->
128
+ - **Monitoring:** <!-- link -->
129
+ - **Runbook:** <!-- link -->
130
+ - **On-call rotation:** <!-- link -->
131
+ - **Team channel:** <!-- link -->
68
132
  `
69
133
  },
70
134
  {
@@ -154,6 +218,70 @@ Product Manager
154
218
 
155
219
  ## Current Quarter Goals
156
220
  <!-- OKRs or key goals -->
221
+ `
222
+ },
223
+ {
224
+ name: "Context Details",
225
+ fileName: "context-details.md",
226
+ required: false,
227
+ defaultContent: `# Context Details
228
+
229
+ ## Executive Summary
230
+ <!-- One-paragraph product overview: current phase, strategic priority, key objective, owner -->
231
+
232
+ ## Key Context
233
+
234
+ | Attribute | Value |
235
+ |-----------|-------|
236
+ | Product Area | <!-- Feature area or product line --> |
237
+ | User Segment | <!-- Target users --> |
238
+ | Revenue Impact | <!-- ARR / MRR contribution --> |
239
+ | Dependencies | <!-- Eng teams, design, legal, etc. --> |
240
+ | Blockers | <!-- Current blockers or open questions --> |
241
+ | Launch Vehicle | <!-- How this ships: feature flag, release train, etc. --> |
242
+
243
+ ## Timeline
244
+
245
+ | Milestone | Date | Status | Owner |
246
+ |-----------|------|--------|-------|
247
+ | <!-- Milestone 1 --> | <!-- Date --> | <!-- On track / At risk / Done --> | <!-- Owner --> |
248
+ | <!-- Milestone 2 --> | <!-- Date --> | <!-- Status --> | <!-- Owner --> |
249
+
250
+ ## Active Decisions
251
+
252
+ | Decision | Options | Status | Deadline | Owner |
253
+ |----------|---------|--------|----------|-------|
254
+ | <!-- Decision 1 --> | <!-- A / B / C --> | <!-- Open / Decided --> | <!-- Date --> | <!-- Owner --> |
255
+
256
+ ## Metrics & Health
257
+
258
+ | Metric | Target | Current | Trend |
259
+ |--------|--------|---------|-------|
260
+ | Adoption rate | <!-- e.g. 30% of MAU --> | <!-- Current --> | <!-- Trend --> |
261
+ | NPS / CSAT | <!-- Target --> | <!-- Current --> | <!-- Trend --> |
262
+ | Feature usage | <!-- Target --> | <!-- Current --> | <!-- Trend --> |
263
+ | Conversion | <!-- Target --> | <!-- Current --> | <!-- Trend --> |
264
+
265
+ ## Communication Map
266
+
267
+ | Stakeholder | Role | Channel | Cadence |
268
+ |-------------|------|---------|---------|
269
+ | <!-- Name --> | <!-- Role --> | <!-- Slack / Email / Meeting --> | <!-- Weekly / Ad-hoc --> |
270
+
271
+ ## Risk Register
272
+
273
+ | Risk | Likelihood | Impact | Mitigation | Owner |
274
+ |------|-----------|--------|------------|-------|
275
+ | <!-- Risk 1 --> | <!-- Low / Med / High --> | <!-- Low / Med / High --> | <!-- Mitigation plan --> | <!-- Owner --> |
276
+
277
+ ## Quick Reference
278
+
279
+ - **PRD:** <!-- link -->
280
+ - **Roadmap:** <!-- link -->
281
+ - **Analytics dashboard:** <!-- link -->
282
+ - **Design files:** <!-- link -->
283
+ - **Competitor research:** <!-- link -->
284
+ - **Team channel:** <!-- link -->
157
285
  `
158
286
  },
159
287
  {
@@ -243,6 +371,70 @@ Engineering Manager
243
371
 
244
372
  ## Key Systems
245
373
  <!-- Systems your team owns -->
374
+ `
375
+ },
376
+ {
377
+ name: "Context Details",
378
+ fileName: "context-details.md",
379
+ required: false,
380
+ defaultContent: `# Context Details
381
+
382
+ ## Executive Summary
383
+ <!-- One-paragraph overview: team mission, current priorities, headcount, key challenges -->
384
+
385
+ ## Key Context
386
+
387
+ | Attribute | Value |
388
+ |-----------|-------|
389
+ | Team Size | <!-- Current headcount --> |
390
+ | Open Roles | <!-- Positions being hired for --> |
391
+ | Tech Domains | <!-- Systems the team owns --> |
392
+ | Dependencies | <!-- Cross-team dependencies --> |
393
+ | Blockers | <!-- Current blockers or risks --> |
394
+ | Budget | <!-- Eng budget or resource constraints --> |
395
+
396
+ ## Timeline
397
+
398
+ | Milestone | Date | Status | Owner |
399
+ |-----------|------|--------|-------|
400
+ | <!-- Milestone 1 --> | <!-- Date --> | <!-- On track / At risk / Done --> | <!-- Owner --> |
401
+ | <!-- Milestone 2 --> | <!-- Date --> | <!-- Status --> | <!-- Owner --> |
402
+
403
+ ## Active Decisions
404
+
405
+ | Decision | Options | Status | Deadline | Owner |
406
+ |----------|---------|--------|----------|-------|
407
+ | <!-- Decision 1 --> | <!-- A / B / C --> | <!-- Open / Decided --> | <!-- Date --> | <!-- Owner --> |
408
+
409
+ ## Metrics & Health
410
+
411
+ | Metric | Target | Current | Trend |
412
+ |--------|--------|---------|-------|
413
+ | Sprint velocity | <!-- Target --> | <!-- Current --> | <!-- Trend --> |
414
+ | Team satisfaction | <!-- Target --> | <!-- Current --> | <!-- Trend --> |
415
+ | Attrition risk | <!-- e.g. Low --> | <!-- Current --> | <!-- Trend --> |
416
+ | Hiring pipeline | <!-- Target fill rate --> | <!-- Current --> | <!-- Trend --> |
417
+
418
+ ## Communication Map
419
+
420
+ | Stakeholder | Role | Channel | Cadence |
421
+ |-------------|------|---------|---------|
422
+ | <!-- Name --> | <!-- Role --> | <!-- Slack / Email / Meeting --> | <!-- Weekly / Ad-hoc --> |
423
+
424
+ ## Risk Register
425
+
426
+ | Risk | Likelihood | Impact | Mitigation | Owner |
427
+ |------|-----------|--------|------------|-------|
428
+ | <!-- Risk 1 --> | <!-- Low / Med / High --> | <!-- Low / Med / High --> | <!-- Mitigation plan --> | <!-- Owner --> |
429
+
430
+ ## Quick Reference
431
+
432
+ - **Team wiki:** <!-- link -->
433
+ - **Sprint board:** <!-- link -->
434
+ - **1:1 doc:** <!-- link -->
435
+ - **Performance review system:** <!-- link -->
436
+ - **Hiring tracker:** <!-- link -->
437
+ - **Team channel:** <!-- link -->
246
438
  `
247
439
  },
248
440
  {
@@ -335,6 +527,70 @@ DevOps Engineer
335
527
 
336
528
  ## Monitoring Stack
337
529
  <!-- Tools and dashboards -->
530
+ `
531
+ },
532
+ {
533
+ name: "Context Details",
534
+ fileName: "context-details.md",
535
+ required: false,
536
+ defaultContent: `# Context Details
537
+
538
+ ## Executive Summary
539
+ <!-- One-paragraph overview: infrastructure scope, current priorities, reliability posture, key challenges -->
540
+
541
+ ## Key Context
542
+
543
+ | Attribute | Value |
544
+ |-----------|-------|
545
+ | Cloud Provider | <!-- AWS / GCP / Azure / Multi --> |
546
+ | Infra-as-Code | <!-- Terraform / Pulumi / CDK --> |
547
+ | Container Runtime | <!-- k8s / ECS / Nomad --> |
548
+ | CI/CD Platform | <!-- GitHub Actions / Jenkins / ArgoCD --> |
549
+ | Dependencies | <!-- Shared services, vendor SLAs --> |
550
+ | Blockers | <!-- Current blockers or risks --> |
551
+
552
+ ## Timeline
553
+
554
+ | Milestone | Date | Status | Owner |
555
+ |-----------|------|--------|-------|
556
+ | <!-- Milestone 1 --> | <!-- Date --> | <!-- On track / At risk / Done --> | <!-- Owner --> |
557
+ | <!-- Milestone 2 --> | <!-- Date --> | <!-- Status --> | <!-- Owner --> |
558
+
559
+ ## Active Decisions
560
+
561
+ | Decision | Options | Status | Deadline | Owner |
562
+ |----------|---------|--------|----------|-------|
563
+ | <!-- Decision 1 --> | <!-- A / B / C --> | <!-- Open / Decided --> | <!-- Date --> | <!-- Owner --> |
564
+
565
+ ## Metrics & Health
566
+
567
+ | Metric | Target | Current | Trend |
568
+ |--------|--------|---------|-------|
569
+ | Uptime (SLA) | <!-- e.g. 99.95% --> | <!-- Current --> | <!-- Trend --> |
570
+ | MTTR | <!-- e.g. < 30 min --> | <!-- Current --> | <!-- Trend --> |
571
+ | Deploy frequency | <!-- e.g. 10/day --> | <!-- Current --> | <!-- Trend --> |
572
+ | Infra cost | <!-- Monthly target --> | <!-- Current --> | <!-- Trend --> |
573
+
574
+ ## Communication Map
575
+
576
+ | Stakeholder | Role | Channel | Cadence |
577
+ |-------------|------|---------|---------|
578
+ | <!-- Name --> | <!-- Role --> | <!-- Slack / Email / Meeting --> | <!-- Weekly / Ad-hoc --> |
579
+
580
+ ## Risk Register
581
+
582
+ | Risk | Likelihood | Impact | Mitigation | Owner |
583
+ |------|-----------|--------|------------|-------|
584
+ | <!-- Risk 1 --> | <!-- Low / Med / High --> | <!-- Low / Med / High --> | <!-- Mitigation plan --> | <!-- Owner --> |
585
+
586
+ ## Quick Reference
587
+
588
+ - **Infra repo:** <!-- link -->
589
+ - **Monitoring dashboard:** <!-- link -->
590
+ - **PagerDuty:** <!-- link -->
591
+ - **Runbooks:** <!-- link -->
592
+ - **Cost dashboard:** <!-- link -->
593
+ - **Incident channel:** <!-- link -->
338
594
  `
339
595
  },
340
596
  {
@@ -427,22 +683,75 @@ function renderTemplate(template, user) {
427
683
  }
428
684
  return files;
429
685
  }
686
+ function getTemplateSections(profileType) {
687
+ const template = PROFILE_TEMPLATES[profileType];
688
+ if (!template) {
689
+ throw new Error(`Unknown profile type: ${profileType}`);
690
+ }
691
+ return template.sections.map((s) => ({ ...s }));
692
+ }
693
+
694
+ // src/google-auth.ts
695
+ var GOOGLE_SCOPES_BY_FEATURE = {
696
+ storage: [
697
+ "https://www.googleapis.com/auth/drive.file",
698
+ "https://www.googleapis.com/auth/drive.readonly"
699
+ ],
700
+ gmail: ["https://www.googleapis.com/auth/gmail.readonly"],
701
+ gdrive: ["https://www.googleapis.com/auth/drive.readonly"]
702
+ };
703
+ function getGoogleScopesForFeatures(features) {
704
+ const scopes = /* @__PURE__ */ new Set();
705
+ for (const feature of features) {
706
+ for (const scope of GOOGLE_SCOPES_BY_FEATURE[feature]) {
707
+ scopes.add(scope);
708
+ }
709
+ }
710
+ return [...scopes];
711
+ }
712
+ function normalizeGoogleScopes(scopes) {
713
+ const values = Array.isArray(scopes) ? scopes : typeof scopes === "string" ? scopes.split(/\s+/) : [];
714
+ return [...new Set(values.map((scope) => scope.trim()).filter(Boolean))];
715
+ }
430
716
 
431
717
  // src/project-structure.ts
432
718
  import * as fs from "fs/promises";
433
719
  import * as path from "path";
720
+ import { execFile } from "child_process";
721
+ import { promisify } from "util";
434
722
  var MAX_DIR_ENTRIES = 10;
435
723
  var README_CANDIDATES = ["README.md", "readme.md", "README.txt", "readme.txt"];
436
724
  var OLLAMA_BASE_URL = "http://127.0.0.1:11434";
725
+ var MAX_KNOWLEDGE_SOURCE_FILES = 8;
726
+ var MAX_KNOWLEDGE_SOURCE_FILE_CATALOG = 20;
727
+ var MAX_KNOWLEDGE_SOURCE_SNIPPET_CHARS = 1800;
728
+ var MAX_KNOWLEDGE_SOURCE_TOTAL_CHARS = 12e3;
729
+ var MAX_KNOWLEDGE_SOURCE_DEPTH = 3;
730
+ var PDFTOTEXT_CANDIDATES = ["pdftotext", "/opt/homebrew/bin/pdftotext"];
731
+ var TEXTUTIL_CANDIDATES = ["/usr/bin/textutil", "textutil"];
732
+ var IGNORED_KNOWLEDGE_SOURCE_DIRS = /* @__PURE__ */ new Set([
733
+ ".git",
734
+ ".next",
735
+ ".turbo",
736
+ "build",
737
+ "coverage",
738
+ "dist",
739
+ "node_modules",
740
+ "out",
741
+ "tmp"
742
+ ]);
743
+ var execFileAsync = promisify(execFile);
437
744
  async function buildProjectStructurePreview(input) {
438
745
  const baseFiles = renderTemplate(getTemplate(input.profileType), {
439
746
  name: input.name,
440
747
  assistantName: input.assistantName,
441
748
  responsibilities: input.responsibilities
442
749
  });
443
- const folderSignals = await Promise.all(
750
+ const localFolderSignals = await Promise.all(
444
751
  input.localFolders.filter((folder) => folder.path.trim()).map((folder) => collectFolderSignal(folder))
445
752
  );
753
+ const driveFolderSignals = (input.driveFolders ?? []).filter((folder) => folder.path.trim()).map((folder) => collectDriveFolderSignal(folder));
754
+ const folderSignals = [...localFolderSignals, ...driveFolderSignals];
446
755
  const heuristicProjects = buildHeuristicProjects(folderSignals, input.integrations);
447
756
  const ollamaResult = await enrichProjectsWithOllama(
448
757
  heuristicProjects,
@@ -452,7 +761,7 @@ async function buildProjectStructurePreview(input) {
452
761
  const projects = ollamaResult?.projects ?? heuristicProjects;
453
762
  const inference = ollamaResult?.inference ?? {
454
763
  strategy: "heuristic",
455
- note: "Structured locally from folder tags, folder names, and visible directory signals. No external API calls were made."
764
+ note: "Structured locally from selected source folders, folder names, and visible directory signals. No external API calls were made."
456
765
  };
457
766
  return {
458
767
  files: buildTemplateFiles(baseFiles, projects, folderSignals, input.integrations),
@@ -460,6 +769,318 @@ async function buildProjectStructurePreview(input) {
460
769
  inference
461
770
  };
462
771
  }
772
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
773
+ var ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
774
+ async function buildProjectKnowledgeFiles(input) {
775
+ const prompt = await buildKnowledgePrompt(input);
776
+ const provider = input.provider ?? { type: "ollama" };
777
+ let rawJson;
778
+ let modelLabel;
779
+ if (provider.type === "openai") {
780
+ if (!provider.apiKey) throw new Error("OpenAI API key is required.");
781
+ const model = provider.model || "gpt-4.1-mini";
782
+ modelLabel = `OpenAI ${model}`;
783
+ const response = await fetch(`${OPENAI_BASE_URL}/chat/completions`, {
784
+ method: "POST",
785
+ headers: {
786
+ Authorization: `Bearer ${provider.apiKey}`,
787
+ "Content-Type": "application/json"
788
+ },
789
+ body: JSON.stringify({
790
+ model,
791
+ temperature: 0.2,
792
+ response_format: { type: "json_object" },
793
+ messages: [
794
+ { role: "system", content: "Return strict JSON only." },
795
+ { role: "user", content: prompt }
796
+ ]
797
+ }),
798
+ signal: AbortSignal.timeout(9e4)
799
+ });
800
+ if (!response.ok) {
801
+ throw new Error(`OpenAI returned HTTP ${response.status} while building project knowledge.`);
802
+ }
803
+ const data = await response.json();
804
+ rawJson = data.choices?.[0]?.message?.content ?? "";
805
+ } else if (provider.type === "anthropic") {
806
+ if (!provider.apiKey) throw new Error("Anthropic API key is required.");
807
+ const model = provider.model || "claude-sonnet-4-20250514";
808
+ modelLabel = `Anthropic ${model}`;
809
+ const response = await fetch(`${ANTHROPIC_BASE_URL}/messages`, {
810
+ method: "POST",
811
+ headers: {
812
+ "x-api-key": provider.apiKey,
813
+ "anthropic-version": "2023-06-01",
814
+ "Content-Type": "application/json"
815
+ },
816
+ body: JSON.stringify({
817
+ model,
818
+ max_tokens: 8192,
819
+ temperature: 0.2,
820
+ system: "Return strict JSON only.",
821
+ messages: [{ role: "user", content: prompt }]
822
+ }),
823
+ signal: AbortSignal.timeout(9e4)
824
+ });
825
+ if (!response.ok) {
826
+ throw new Error(`Anthropic returned HTTP ${response.status} while building project knowledge.`);
827
+ }
828
+ const data = await response.json();
829
+ rawJson = data.content?.find((c) => c.type === "text")?.text ?? "";
830
+ } else {
831
+ const ollamaModel = await resolveOllamaModel();
832
+ if (!ollamaModel) {
833
+ throw new Error("No local Ollama model is available. Start Ollama first, or configure an OpenAI / Anthropic API key.");
834
+ }
835
+ modelLabel = `Ollama ${ollamaModel}`;
836
+ const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, {
837
+ method: "POST",
838
+ headers: { "Content-Type": "application/json" },
839
+ body: JSON.stringify({
840
+ model: ollamaModel,
841
+ prompt,
842
+ stream: false,
843
+ format: "json",
844
+ options: { temperature: 0.2 }
845
+ }),
846
+ signal: AbortSignal.timeout(12e4)
847
+ });
848
+ if (!response.ok) {
849
+ throw new Error(`Ollama returned HTTP ${response.status} while building project knowledge.`);
850
+ }
851
+ const payload = await response.json();
852
+ rawJson = payload.response ?? "";
853
+ }
854
+ if (!rawJson.trim()) {
855
+ throw new Error(`${modelLabel} returned an empty response while building project knowledge.`);
856
+ }
857
+ const parsed = JSON.parse(rawJson);
858
+ const llmFiles = parsed.files ?? {};
859
+ const normalizedFiles = {};
860
+ for (const [filePath, existingContent] of Object.entries(input.files)) {
861
+ normalizedFiles[filePath] = llmFiles[filePath]?.trim() ? llmFiles[filePath] : existingContent;
862
+ }
863
+ const projectPrefix = `projects/${input.project.slug}/`;
864
+ for (const [filePath, content] of Object.entries(llmFiles)) {
865
+ if (normalizedFiles[filePath] === void 0 && content?.trim() && filePath.startsWith(projectPrefix)) {
866
+ normalizedFiles[filePath] = content;
867
+ }
868
+ }
869
+ return { files: normalizedFiles, model: modelLabel };
870
+ }
871
+ async function buildKnowledgePrompt(input) {
872
+ const slug = input.project.slug;
873
+ const lines = [];
874
+ const agreementReviewMode = isAgreementReviewProject(input);
875
+ const localKnowledgeEvidence = await collectLocalKnowledgeEvidence(
876
+ input.project,
877
+ input.localFolders ?? []
878
+ );
879
+ lines.push(
880
+ "You are a project knowledge generator for a personal assistant called Pac-Man.",
881
+ "Your job: given a project and its context, produce richly-filled markdown files",
882
+ `that live under the "projects/${slug}/" subfolder.`,
883
+ ""
884
+ );
885
+ if (input.userProfile) {
886
+ lines.push(
887
+ "# User Profile",
888
+ `- Name: ${input.userProfile.name}`,
889
+ `- Role: ${input.userProfile.profileType}`,
890
+ ...input.userProfile.assistantName ? [`- Assistant name: ${input.userProfile.assistantName}`] : [],
891
+ ...input.userProfile.responsibilities.length > 0 ? ["- Responsibilities:", ...input.userProfile.responsibilities.map((r) => ` - ${r}`)] : [],
892
+ ""
893
+ );
894
+ }
895
+ lines.push(
896
+ "# Project",
897
+ `- Name: ${input.project.name}`,
898
+ `- Slug: ${slug}`,
899
+ ...input.project.summary ? [`- Summary: ${input.project.summary}`] : [],
900
+ ...input.project.primaryFocus ? [`- Primary Focus: ${input.project.primaryFocus}`] : []
901
+ );
902
+ if (input.project.sourceFolders.length > 0) {
903
+ lines.push("- Source Folders:", ...input.project.sourceFolders.map((f) => ` - ${f}`));
904
+ }
905
+ if (input.project.connectedIntegrations.length > 0) {
906
+ lines.push("- Connected Integrations:", ...input.project.connectedIntegrations.map((i) => ` - ${i}`));
907
+ }
908
+ if (input.project.githubBranches.length > 0) {
909
+ lines.push("- GitHub Branches:", ...input.project.githubBranches.map((b) => ` - ${b}`));
910
+ }
911
+ if (input.project.gitlabBranches.length > 0) {
912
+ lines.push("- GitLab Branches:", ...input.project.gitlabBranches.map((b) => ` - ${b}`));
913
+ }
914
+ if (input.project.slackChannels.length > 0) {
915
+ lines.push("- Slack Channels:", ...input.project.slackChannels.map((c) => ` - ${c}`));
916
+ }
917
+ if (input.project.gdriveFolders.length > 0) {
918
+ lines.push("- GDrive Folders:", ...input.project.gdriveFolders.map((f) => ` - ${f}`));
919
+ }
920
+ if (input.project.gdocDocuments.length > 0) {
921
+ lines.push("- Google Docs:", ...input.project.gdocDocuments.map((d) => ` - ${d}`));
922
+ }
923
+ if (input.project.keySignals.length > 0) {
924
+ lines.push("- Key Signals:", ...input.project.keySignals.map((s) => ` - ${s}`));
925
+ }
926
+ if (input.project.llmInstructions) {
927
+ lines.push("", "## LLM Instructions (follow these)", input.project.llmInstructions);
928
+ }
929
+ lines.push("");
930
+ if (agreementReviewMode) {
931
+ lines.push(
932
+ "# Analysis Mode",
933
+ "This project is a contract / agreement review, not a software delivery project.",
934
+ "- Prioritize agreement-by-agreement summaries, counterparties, dates, commercial terms, exclusivity, obligations, governing law, notice, renewal / termination clauses, and diligence risks.",
935
+ "- Treat the source evidence as the primary truth. User role templates are secondary structure only.",
936
+ '- If a template section such as tech stack, CI, deploy target, or sprint metrics is not supported by evidence, write "Not applicable for this agreement review" or add a TODO. Do not invent software-project details.',
937
+ ""
938
+ );
939
+ }
940
+ if (input.integrations && input.integrations.length > 0) {
941
+ lines.push("# Workspace Integrations", ...input.integrations.map((i) => `- ${i}`), "");
942
+ }
943
+ const projectLocalFolders = (input.localFolders ?? []).filter(
944
+ (f) => f.project === input.project.name || input.project.sourceFolders.includes(f.path)
945
+ );
946
+ const otherLocalFolders = (input.localFolders ?? []).filter(
947
+ (f) => !projectLocalFolders.includes(f)
948
+ );
949
+ if (projectLocalFolders.length > 0) {
950
+ lines.push(
951
+ "# Local Folders (this project)",
952
+ ...projectLocalFolders.map((f) => `- ${f.path}`),
953
+ ""
954
+ );
955
+ }
956
+ if (otherLocalFolders.length > 0) {
957
+ lines.push(
958
+ "# Local Folders (other projects)",
959
+ ...otherLocalFolders.map((f) => `- ${f.path}${f.project ? ` (${f.project})` : ""}`),
960
+ ""
961
+ );
962
+ }
963
+ const projectDriveFolders = (input.driveFolders ?? []).filter(
964
+ (f) => input.project.gdriveFolders.includes(f.name) || input.project.gdriveFolders.includes(f.path)
965
+ );
966
+ if (projectDriveFolders.length > 0) {
967
+ lines.push(
968
+ "# Google Drive Folders (this project)",
969
+ ...projectDriveFolders.map((f) => `- ${f.name} (${f.path})`),
970
+ ""
971
+ );
972
+ }
973
+ if (localKnowledgeEvidence.length > 0) {
974
+ lines.push(
975
+ "# Local Source Evidence",
976
+ "Use these excerpts as primary grounding when filling in context, decisions, notes, and project-specific template files.",
977
+ ""
978
+ );
979
+ for (const evidence of localKnowledgeEvidence) {
980
+ lines.push(`## Folder: ${evidence.folderPath}`);
981
+ if (evidence.notes.length > 0) {
982
+ lines.push(...evidence.notes.map((note) => `- ${note}`));
983
+ }
984
+ if (evidence.fileCatalog.length > 0) {
985
+ lines.push(
986
+ "### File Catalog",
987
+ ...evidence.fileCatalog.map((file) => `- ${file.relativePath} \u2014 ${file.status}`)
988
+ );
989
+ }
990
+ if (evidence.excerpts.length > 0) {
991
+ for (const excerpt of evidence.excerpts) {
992
+ lines.push(
993
+ `### File: ${excerpt.relativePath}`,
994
+ "```text",
995
+ excerpt.snippet,
996
+ "```"
997
+ );
998
+ }
999
+ } else {
1000
+ lines.push("- No readable text files were sampled from this folder.");
1001
+ }
1002
+ lines.push("");
1003
+ }
1004
+ }
1005
+ if (input.templateSections && input.templateSections.length > 0) {
1006
+ lines.push(
1007
+ "# Base Template Sections",
1008
+ "These are the template sections for this user role. Use them as structure for the project-specific files.",
1009
+ ""
1010
+ );
1011
+ for (const section of input.templateSections) {
1012
+ lines.push(
1013
+ `## ${section.name} (${section.fileName})`,
1014
+ "```",
1015
+ section.defaultContent,
1016
+ "```",
1017
+ ""
1018
+ );
1019
+ }
1020
+ }
1021
+ lines.push(
1022
+ "# Output Instructions",
1023
+ "",
1024
+ "Return strict JSON only in this format:",
1025
+ '{"files":{"projects/<slug>/filename.md":"# Markdown content", ...}}',
1026
+ "",
1027
+ `All file paths MUST start with "projects/${slug}/".`,
1028
+ "",
1029
+ "Required files to generate:"
1030
+ );
1031
+ const requiredFiles = {};
1032
+ const projectMainPath = `projects/${slug}/project.md`;
1033
+ requiredFiles[projectMainPath] = input.files[projectMainPath] ?? "";
1034
+ lines.push(`- "${projectMainPath}" \u2014 the main project overview file`);
1035
+ if (agreementReviewMode) {
1036
+ const matrixPath = `projects/${slug}/agreement-matrix.md`;
1037
+ requiredFiles[matrixPath] = input.files[matrixPath] ?? [
1038
+ "# Agreement Matrix",
1039
+ "",
1040
+ "| File | Counterparty | Agreement Type | Effective Date | Commercial Terms | Key Obligations | Exclusivity / Territory | Term / Termination | Governing Law / Dispute | Risks / Gaps | Evidence Quality |",
1041
+ "|------|--------------|----------------|----------------|------------------|-----------------|-------------------------|--------------------|------------------------|--------------|------------------|",
1042
+ "| TODO | TODO | TODO | TODO | TODO | TODO | TODO | TODO | TODO | TODO | TODO |"
1043
+ ].join("\n");
1044
+ lines.push(`- "${matrixPath}" \u2014 agreement-by-agreement summary table grounded in the source files`);
1045
+ }
1046
+ if (input.templateSections && input.templateSections.length > 0) {
1047
+ for (const section of input.templateSections) {
1048
+ const filePath = `projects/${slug}/${section.fileName}`;
1049
+ requiredFiles[filePath] = input.files[filePath] ?? section.defaultContent;
1050
+ lines.push(`- "${filePath}" \u2014 ${section.name} (project-specific version of the base template)`);
1051
+ }
1052
+ }
1053
+ for (const [filePath, content] of Object.entries(input.files)) {
1054
+ if (!requiredFiles[filePath]) {
1055
+ requiredFiles[filePath] = content;
1056
+ lines.push(`- "${filePath}" \u2014 user-created schema file`);
1057
+ }
1058
+ }
1059
+ lines.push(
1060
+ "",
1061
+ "Rules:",
1062
+ "- Return content for EVERY file path listed above.",
1063
+ `- You MAY also add new files under "projects/${slug}/" if the project context warrants it (e.g., "projects/${slug}/runbook.md", "projects/${slug}/decisions.md").`,
1064
+ "- Fill in template placeholders and HTML comments with real content derived from the project context.",
1065
+ "- Ground all content in the provided project details, source folders, integrations, branches, channels, and signals.",
1066
+ "- Use the user role and responsibilities only as secondary context for structure. Source evidence takes precedence.",
1067
+ "- If specific details are missing, add actionable TODO bullets instead of inventing facts.",
1068
+ "- Prefer concise, structured markdown. Use headings, bullets, and tables.",
1069
+ "- Follow any LLM Instructions provided in the project.",
1070
+ "- DO NOT invent repositories, tech stack, CI, deploy targets, sprint timelines, metrics, or team structure unless directly supported by evidence.",
1071
+ "- If a source file could not be read because it is scan-only or OCR is missing, explicitly say so and keep the related fields as TODO / OCR-needed.",
1072
+ ...agreementReviewMode ? [
1073
+ "- For agreement review projects, summarize each agreement individually and cross-reference the file name.",
1074
+ "- Extract party names, dates, commission / fee mechanics, lead-generation flow, obligations, confidentiality, indemnity, exclusivity, customer support allocation, governing law, dispute resolution, and termination / notice periods whenever present.",
1075
+ "- If a clause is missing from the extracted evidence, mark it as not found instead of inferring it."
1076
+ ] : [],
1077
+ "",
1078
+ "# Seed Files (fill these in or improve them)",
1079
+ "",
1080
+ JSON.stringify(requiredFiles, null, 2)
1081
+ );
1082
+ return lines.join("\n");
1083
+ }
463
1084
  function buildTemplateFiles(baseFiles, projects, folderSignals, integrations) {
464
1085
  const files = { ...baseFiles };
465
1086
  const integrationNames = integrations.map(formatIntegrationName);
@@ -472,7 +1093,7 @@ function buildTemplateFiles(baseFiles, projects, folderSignals, integrations) {
472
1093
  ].join("\n");
473
1094
  const sourceLines = [];
474
1095
  if (folderSignals.length > 0) {
475
- sourceLines.push("## Local Sources", "");
1096
+ sourceLines.push("## Selected Sources", "");
476
1097
  sourceLines.push(...folderSignals.map((signal) => `- \`${signal.path}\``));
477
1098
  sourceLines.push("");
478
1099
  }
@@ -499,7 +1120,7 @@ function buildTemplateFiles(baseFiles, projects, folderSignals, integrations) {
499
1120
  ].join("\n");
500
1121
  }
501
1122
  for (const project of projects) {
502
- files[`projects/${project.slug}.md`] = buildProjectFile(project);
1123
+ files[`projects/${project.slug}/project.md`] = buildProjectFile(project);
503
1124
  }
504
1125
  return files;
505
1126
  }
@@ -515,11 +1136,11 @@ function buildProjectFile(project) {
515
1136
  "",
516
1137
  project.primaryFocus || "<!-- Capture the main problem area, domain, or ownership -->",
517
1138
  "",
518
- "## Local Sources",
1139
+ "## Sources",
519
1140
  "",
520
1141
  formatBulletList(
521
- project.sourceFolders.map((folder) => `\`${folder}\``),
522
- "No local folders linked to this project yet."
1142
+ [...project.sourceFolders, ...project.gdriveFolders, ...project.gdocDocuments].map((item) => `\`${item}\``),
1143
+ "No source folders linked to this project yet."
523
1144
  ),
524
1145
  "",
525
1146
  "## Connected Integrations",
@@ -529,10 +1150,49 @@ function buildProjectFile(project) {
529
1150
  "No integrations connected during onboarding."
530
1151
  ),
531
1152
  "",
1153
+ "## Source Monitoring",
1154
+ "",
1155
+ "### GitHub Branches",
1156
+ "",
1157
+ formatBulletList(project.githubBranches, "No GitHub branches scoped yet."),
1158
+ "",
1159
+ "### GitLab Branches",
1160
+ "",
1161
+ formatBulletList(project.gitlabBranches, "No GitLab branches scoped yet."),
1162
+ "",
1163
+ "### Slack Channels",
1164
+ "",
1165
+ formatBulletList(project.slackChannels, "No Slack channels scoped yet."),
1166
+ "",
1167
+ "### Google Drive Folders",
1168
+ "",
1169
+ formatBulletList(
1170
+ project.gdriveFolders.map((folder) => `\`${folder}\``),
1171
+ "No Google Drive folders scoped yet."
1172
+ ),
1173
+ "",
1174
+ "### Google Docs",
1175
+ "",
1176
+ formatBulletList(
1177
+ project.gdocDocuments.map((document) => `\`${document}\``),
1178
+ "No Google Docs scoped yet."
1179
+ ),
1180
+ "",
532
1181
  "## Grouping Signals",
533
1182
  "",
534
1183
  formatBulletList(project.keySignals, "Add more details after the first sync."),
535
1184
  "",
1185
+ "## Project Context Files",
1186
+ "",
1187
+ formatBulletList(
1188
+ project.templateFiles.map((filePath) => `\`${filePath}\``),
1189
+ "No project context files linked yet."
1190
+ ),
1191
+ "",
1192
+ "## Instructions For LLM",
1193
+ "",
1194
+ project.llmInstructions || "<!-- Add project-specific instructions that the LLM should follow -->",
1195
+ "",
536
1196
  "## Notes To Confirm",
537
1197
  "",
538
1198
  "- Owners:",
@@ -541,11 +1201,259 @@ function buildProjectFile(project) {
541
1201
  ""
542
1202
  ].join("\n");
543
1203
  }
544
- function formatBulletList(items, fallback) {
545
- if (items.length === 0) {
546
- return `- ${fallback}`;
1204
+ function formatBulletList(items, fallback) {
1205
+ if (items.length === 0) {
1206
+ return `- ${fallback}`;
1207
+ }
1208
+ return items.map((item) => `- ${item}`).join("\n");
1209
+ }
1210
+ async function collectLocalKnowledgeEvidence(project, localFolders) {
1211
+ const projectLocalFolders = localFolders.filter((folder) => {
1212
+ const trimmedPath = folder.path.trim();
1213
+ if (!trimmedPath) {
1214
+ return false;
1215
+ }
1216
+ return folder.project === project.name || project.sourceFolders.includes(trimmedPath);
1217
+ });
1218
+ const evidence = await Promise.all(
1219
+ projectLocalFolders.map((folder) => collectLocalFolderKnowledgeEvidence(folder))
1220
+ );
1221
+ return evidence.filter((item) => item !== null);
1222
+ }
1223
+ async function collectLocalFolderKnowledgeEvidence(folder) {
1224
+ const rawPath = folder.path.trim();
1225
+ if (!rawPath) {
1226
+ return null;
1227
+ }
1228
+ const resolvedPath = path.resolve(rawPath);
1229
+ const signal = await collectFolderSignal(folder);
1230
+ const candidates = [];
1231
+ await walkKnowledgeSourceDirectory(resolvedPath, "", 0, candidates);
1232
+ candidates.sort((left, right) => right.score - left.score || left.relativePath.localeCompare(right.relativePath));
1233
+ const excerpts = [];
1234
+ const fileCatalog = [];
1235
+ let totalChars = 0;
1236
+ const rankedCandidates = candidates.slice(0, MAX_KNOWLEDGE_SOURCE_FILE_CATALOG);
1237
+ for (const candidate of rankedCandidates) {
1238
+ const result = await readKnowledgeSourceSnippet(candidate.absolutePath);
1239
+ fileCatalog.push({
1240
+ relativePath: candidate.relativePath,
1241
+ status: result.status === "sampled" ? "text extracted" : result.status === "ocr-needed" ? "OCR or manual review needed" : "no readable text extracted"
1242
+ });
1243
+ if (result.status !== "sampled" || !result.snippet || excerpts.length >= MAX_KNOWLEDGE_SOURCE_FILES || totalChars >= MAX_KNOWLEDGE_SOURCE_TOTAL_CHARS) {
1244
+ continue;
1245
+ }
1246
+ const remaining = MAX_KNOWLEDGE_SOURCE_TOTAL_CHARS - totalChars;
1247
+ const trimmedSnippet = result.snippet.slice(0, remaining);
1248
+ excerpts.push({
1249
+ relativePath: candidate.relativePath,
1250
+ snippet: trimmedSnippet
1251
+ });
1252
+ totalChars += trimmedSnippet.length;
1253
+ }
1254
+ return {
1255
+ folderPath: rawPath,
1256
+ notes: signal.notes,
1257
+ excerpts,
1258
+ fileCatalog
1259
+ };
1260
+ }
1261
+ async function walkKnowledgeSourceDirectory(directoryPath, relativeDir, depth, candidates) {
1262
+ if (depth > MAX_KNOWLEDGE_SOURCE_DEPTH) {
1263
+ return;
1264
+ }
1265
+ let entries;
1266
+ try {
1267
+ entries = await fs.readdir(directoryPath, { withFileTypes: true });
1268
+ } catch {
1269
+ return;
1270
+ }
1271
+ for (const entry of entries) {
1272
+ if (entry.name.startsWith(".")) {
1273
+ continue;
1274
+ }
1275
+ const absolutePath = path.join(directoryPath, entry.name);
1276
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
1277
+ if (entry.isDirectory()) {
1278
+ if (IGNORED_KNOWLEDGE_SOURCE_DIRS.has(entry.name)) {
1279
+ continue;
1280
+ }
1281
+ await walkKnowledgeSourceDirectory(absolutePath, relativePath, depth + 1, candidates);
1282
+ continue;
1283
+ }
1284
+ if (!entry.isFile()) {
1285
+ continue;
1286
+ }
1287
+ const score = scoreKnowledgeSourceFile(relativePath);
1288
+ if (score <= 0) {
1289
+ continue;
1290
+ }
1291
+ candidates.push({ absolutePath, relativePath, score });
1292
+ }
1293
+ }
1294
+ function scoreKnowledgeSourceFile(relativePath) {
1295
+ const normalized = relativePath.toLowerCase();
1296
+ const baseName = path.basename(normalized);
1297
+ const extension = path.extname(normalized);
1298
+ if (!isKnowledgeSourceTextFile(baseName, extension)) {
1299
+ return 0;
1300
+ }
1301
+ let score = 10;
1302
+ if (baseName === "readme.md" || baseName === "readme.txt") {
1303
+ score += 120;
1304
+ }
1305
+ if (baseName === "package.json") {
1306
+ score += 70;
1307
+ }
1308
+ if (/(context|overview|summary|decision|decisions|adr|spec|design|roadmap|plan|note|notes|docs|doc|requirement|brief|meeting|architecture|product|vision|status)/.test(normalized)) {
1309
+ score += 90;
1310
+ }
1311
+ if (normalized.includes("/docs/") || normalized.includes("/notes/") || normalized.includes("/decisions/")) {
1312
+ score += 40;
1313
+ }
1314
+ if (extension === ".md" || extension === ".mdx" || extension === ".txt") {
1315
+ score += 30;
1316
+ }
1317
+ if (extension === ".json" || extension === ".yaml" || extension === ".yml") {
1318
+ score += 20;
1319
+ }
1320
+ if (extension === ".docx" || extension === ".doc") {
1321
+ score += 60;
1322
+ }
1323
+ if (extension === ".pdf") {
1324
+ score += 80;
1325
+ }
1326
+ if (extension === ".xlsx" || extension === ".xls" || extension === ".csv") {
1327
+ score += 15;
1328
+ }
1329
+ if (/(agreement|contract|mou|partner|annexure|term|marketing)/.test(normalized)) {
1330
+ score += 110;
1331
+ }
1332
+ return score;
1333
+ }
1334
+ function isKnowledgeSourceTextFile(baseName, extension) {
1335
+ if (baseName === "package.json") {
1336
+ return true;
1337
+ }
1338
+ return (/* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".pdf", ".xlsx", ".xls", ".csv", ".docx", ".doc"])).has(extension);
1339
+ }
1340
+ async function readKnowledgeSourceSnippet(filePath) {
1341
+ try {
1342
+ const extension = path.extname(filePath).toLowerCase();
1343
+ let normalized;
1344
+ if (extension === ".pdf") {
1345
+ const pdfSnippet = await extractPdfSnippet(filePath);
1346
+ if (pdfSnippet === null) {
1347
+ return {
1348
+ snippet: null,
1349
+ status: "ocr-needed"
1350
+ };
1351
+ }
1352
+ normalized = pdfSnippet.trim();
1353
+ } else if (extension === ".docx" || extension === ".doc") {
1354
+ normalized = (await extractDocxSnippet(filePath))?.trim() ?? "";
1355
+ } else if (extension === ".xlsx" || extension === ".xls") {
1356
+ normalized = (await extractSpreadsheetSnippet(filePath))?.trim() ?? "";
1357
+ } else {
1358
+ const raw = await fs.readFile(filePath, "utf-8");
1359
+ normalized = raw.trim();
1360
+ if (extension === ".json") {
1361
+ try {
1362
+ normalized = JSON.stringify(JSON.parse(normalized), null, 2);
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ }
1367
+ if (!normalized) {
1368
+ return {
1369
+ snippet: null,
1370
+ status: "unreadable"
1371
+ };
1372
+ }
1373
+ return {
1374
+ snippet: normalized.slice(0, MAX_KNOWLEDGE_SOURCE_SNIPPET_CHARS),
1375
+ status: "sampled"
1376
+ };
1377
+ } catch {
1378
+ return {
1379
+ snippet: null,
1380
+ status: "unreadable"
1381
+ };
1382
+ }
1383
+ }
1384
+ async function extractPdfSnippet(filePath) {
1385
+ for (const candidate of PDFTOTEXT_CANDIDATES) {
1386
+ try {
1387
+ const { stdout } = await execFileAsync(candidate, [filePath, "-"], {
1388
+ timeout: 15e3,
1389
+ maxBuffer: 4 * 1024 * 1024
1390
+ });
1391
+ const normalized = normalizeKnowledgeSourceText(stdout);
1392
+ if (normalized && !isLowSignalPdfText(normalized)) {
1393
+ return normalized;
1394
+ }
1395
+ } catch {
1396
+ }
547
1397
  }
548
- return items.map((item) => `- ${item}`).join("\n");
1398
+ return null;
1399
+ }
1400
+ async function extractDocxSnippet(filePath) {
1401
+ for (const candidate of TEXTUTIL_CANDIDATES) {
1402
+ try {
1403
+ const { stdout } = await execFileAsync(candidate, ["-convert", "txt", "-stdout", filePath], {
1404
+ timeout: 15e3,
1405
+ maxBuffer: 4 * 1024 * 1024
1406
+ });
1407
+ const normalized = normalizeKnowledgeSourceText(stdout);
1408
+ if (normalized) {
1409
+ return normalized;
1410
+ }
1411
+ } catch {
1412
+ }
1413
+ }
1414
+ return null;
1415
+ }
1416
+ async function extractSpreadsheetSnippet(filePath) {
1417
+ try {
1418
+ const sharedStrings = await execFileAsync("unzip", ["-p", filePath, "xl/sharedStrings.xml"], {
1419
+ timeout: 15e3,
1420
+ maxBuffer: 2 * 1024 * 1024
1421
+ });
1422
+ const normalized = normalizeKnowledgeSourceText(stripXmlTags(sharedStrings.stdout));
1423
+ if (normalized) {
1424
+ return normalized;
1425
+ }
1426
+ } catch {
1427
+ }
1428
+ return `Workbook file: ${path.basename(filePath)}`;
1429
+ }
1430
+ function stripXmlTags(value) {
1431
+ return value.replace(/<[^>]+>/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
1432
+ }
1433
+ function normalizeKnowledgeSourceText(value) {
1434
+ return value.replace(/\u0000/g, " ").replace(/\f/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[^\S\n]{2,}/g, " ").trim();
1435
+ }
1436
+ function isLowSignalPdfText(value) {
1437
+ const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
1438
+ if (!normalized) {
1439
+ return true;
1440
+ }
1441
+ const withoutScannerWatermarks = normalized.replace(/scanned (with|by) camscanner/g, "").replace(/\s+/g, " ").trim();
1442
+ return withoutScannerWatermarks.length === 0;
1443
+ }
1444
+ function isAgreementReviewProject(input) {
1445
+ const haystack = [
1446
+ input.project.name,
1447
+ input.project.summary,
1448
+ input.project.primaryFocus,
1449
+ input.project.llmInstructions,
1450
+ ...input.project.sourceFolders,
1451
+ ...(input.localFolders ?? []).map((folder) => folder.path),
1452
+ ...input.project.keySignals
1453
+ ].join(" ").toLowerCase();
1454
+ return /(agreement|agreements|contract|contracts|mou|due diligence|diligence|legal|partner|annexure|insurance documents|channel partner)/.test(
1455
+ haystack
1456
+ );
549
1457
  }
550
1458
  async function collectFolderSignal(folder) {
551
1459
  const rawPath = folder.path.trim();
@@ -590,6 +1498,7 @@ async function collectFolderSignal(folder) {
590
1498
  }
591
1499
  }
592
1500
  return {
1501
+ sourceType: "local",
593
1502
  path: rawPath,
594
1503
  explicitProject: folder.project?.trim() || void 0,
595
1504
  basename: basename2,
@@ -599,6 +1508,20 @@ async function collectFolderSignal(folder) {
599
1508
  notes
600
1509
  };
601
1510
  }
1511
+ function collectDriveFolderSignal(folder) {
1512
+ const rawPath = folder.path.trim();
1513
+ const basename2 = folder.name.trim() || rawPath.split("/").filter(Boolean).pop() || "Drive Folder";
1514
+ return {
1515
+ sourceType: "gdrive",
1516
+ path: rawPath,
1517
+ basename: basename2,
1518
+ entries: [],
1519
+ notes: [
1520
+ "Google Drive folder selected during onboarding",
1521
+ `Drive path: ${rawPath}`
1522
+ ]
1523
+ };
1524
+ }
602
1525
  function extractReadmeHeading(contents) {
603
1526
  for (const rawLine of contents.split("\n").slice(0, 30)) {
604
1527
  const line = rawLine.trim();
@@ -624,7 +1547,14 @@ function buildHeuristicProjects(folderSignals, integrations) {
624
1547
  primaryFocus: "Cross-project workspace context",
625
1548
  sourceFolders: [],
626
1549
  connectedIntegrations: integrationNames,
627
- keySignals: integrationNames.map((name) => `Connected integration: ${name}`)
1550
+ githubBranches: [],
1551
+ gitlabBranches: [],
1552
+ slackChannels: [],
1553
+ gdriveFolders: [],
1554
+ gdocDocuments: [],
1555
+ keySignals: integrationNames.map((name) => `Connected integration: ${name}`),
1556
+ llmInstructions: "",
1557
+ templateFiles: []
628
1558
  }
629
1559
  ];
630
1560
  }
@@ -633,7 +1563,11 @@ function buildHeuristicProjects(folderSignals, integrations) {
633
1563
  const slug = slugify(groupName) || "untitled-project";
634
1564
  const existing = groups.get(slug);
635
1565
  if (existing) {
636
- existing.sourceFolders.push(signal.path);
1566
+ if (signal.sourceType === "gdrive") {
1567
+ existing.gdriveFolders.push(signal.path);
1568
+ } else {
1569
+ existing.sourceFolders.push(signal.path);
1570
+ }
637
1571
  existing.keySignals = unique([...existing.keySignals, ...signal.notes]);
638
1572
  continue;
639
1573
  }
@@ -642,9 +1576,16 @@ function buildHeuristicProjects(folderSignals, integrations) {
642
1576
  name: groupName,
643
1577
  summary: buildHeuristicSummary(groupName, signal, integrationNames),
644
1578
  primaryFocus: buildHeuristicFocus(signal),
645
- sourceFolders: [signal.path],
1579
+ sourceFolders: signal.sourceType === "gdrive" ? [] : [signal.path],
646
1580
  connectedIntegrations: integrationNames,
647
- keySignals: unique(signal.notes)
1581
+ githubBranches: [],
1582
+ gitlabBranches: [],
1583
+ slackChannels: [],
1584
+ gdriveFolders: signal.sourceType === "gdrive" ? [signal.path] : [],
1585
+ gdocDocuments: [],
1586
+ keySignals: unique(signal.notes),
1587
+ llmInstructions: "",
1588
+ templateFiles: []
648
1589
  });
649
1590
  }
650
1591
  return [...groups.values()].sort((left, right) => left.name.localeCompare(right.name));
@@ -657,9 +1598,12 @@ function buildHeuristicSummary(projectName, signal, integrationNames) {
657
1598
  if (signal.packageName) {
658
1599
  return `${projectName} was inferred from the local package "${signal.packageName}". Review and tighten this summary before finishing setup.${integrationSuffix}`;
659
1600
  }
660
- return `${projectName} was inferred from the selected local workspace folder. Review and tighten this summary before finishing setup.${integrationSuffix}`;
1601
+ return `${signal.sourceType === "gdrive" ? `${projectName} was inferred from the selected Google Drive folder.` : `${projectName} was inferred from the selected local workspace folder.`} Review and tighten this summary before finishing setup.${integrationSuffix}`;
661
1602
  }
662
1603
  function buildHeuristicFocus(signal) {
1604
+ if (signal.sourceType === "gdrive") {
1605
+ return `Google Drive context collected from the ${humanizeLabel(signal.basename)} folder.`;
1606
+ }
663
1607
  if (signal.packageName) {
664
1608
  return `Owns or contributes to the ${humanizeLabel(signal.packageName)} codebase.`;
665
1609
  }
@@ -782,7 +1726,8 @@ function formatIntegrationName(id) {
782
1726
  github: "GitHub",
783
1727
  gitlab: "GitLab",
784
1728
  gmail: "Gmail",
785
- gdrive: "Google Drive",
1729
+ "google-drive-storage": "GDrive",
1730
+ gdrive: "GDocs",
786
1731
  gchat: "Google Chat"
787
1732
  };
788
1733
  return names[id] ?? humanizeLabel(id);
@@ -797,8 +1742,83 @@ function unique(values) {
797
1742
  return [...new Set(values.filter(Boolean))];
798
1743
  }
799
1744
 
1745
+ // src/slack-manifest.ts
1746
+ var SLACK_BOT_SCOPES = [
1747
+ "channels:history",
1748
+ "channels:read",
1749
+ "groups:history",
1750
+ "groups:read",
1751
+ "im:history",
1752
+ "app_mentions:read",
1753
+ "chat:write"
1754
+ ];
1755
+ var SLACK_BOT_EVENTS = [
1756
+ "message.channels",
1757
+ "message.groups",
1758
+ "app_mention"
1759
+ ];
1760
+ var SLACK_LONG_DESCRIPTION = "Pac-Man connects Slack threads to your local project context so every reply can reference saved notes, project summaries, folder-derived evidence, and prior decisions. It helps teams answer questions faster without losing the audit trail back to the workspace that produced the draft.";
1761
+ function buildSlackManifest(input = {}) {
1762
+ const assistantName = normalizeAssistantName(input.assistantName);
1763
+ const appName = truncateSlackAppName(`Pac-Man ${assistantName}`);
1764
+ const botDisplayName = sanitizeSlackBotDisplayName(assistantName);
1765
+ const manifest = {
1766
+ _metadata: {
1767
+ major_version: 2,
1768
+ minor_version: 1
1769
+ },
1770
+ display_information: {
1771
+ name: appName,
1772
+ description: "Project-aware Slack replies grounded in your Pac-Man workspace.",
1773
+ long_description: SLACK_LONG_DESCRIPTION,
1774
+ background_color: "#0B1220"
1775
+ },
1776
+ features: {
1777
+ bot_user: {
1778
+ display_name: botDisplayName,
1779
+ always_online: false
1780
+ }
1781
+ },
1782
+ oauth_config: {
1783
+ scopes: {
1784
+ bot: [...SLACK_BOT_SCOPES]
1785
+ }
1786
+ },
1787
+ settings: {
1788
+ event_subscriptions: {
1789
+ bot_events: [...SLACK_BOT_EVENTS]
1790
+ },
1791
+ org_deploy_enabled: false,
1792
+ socket_mode_enabled: true,
1793
+ is_hosted: false,
1794
+ token_rotation_enabled: false
1795
+ }
1796
+ };
1797
+ const manifestJson = JSON.stringify(manifest, null, 2);
1798
+ const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(manifestJson)}`;
1799
+ return {
1800
+ appName,
1801
+ botDisplayName,
1802
+ manifest,
1803
+ manifestJson,
1804
+ createUrl
1805
+ };
1806
+ }
1807
+ function normalizeAssistantName(value) {
1808
+ const trimmed = value?.trim().replace(/\s+/g, " ") ?? "";
1809
+ return trimmed || "Atlas";
1810
+ }
1811
+ function truncateSlackAppName(value) {
1812
+ const collapsed = value.replace(/\s+/g, " ").trim();
1813
+ return collapsed.slice(0, 35).trimEnd() || "Pac-Man Atlas";
1814
+ }
1815
+ function sanitizeSlackBotDisplayName(value) {
1816
+ const sanitized = value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "").replace(/[-_.]{2,}/g, "-").replace(/^[-_.]+|[-_.]+$/g, "").slice(0, 80);
1817
+ return sanitized || "atlas";
1818
+ }
1819
+
800
1820
  // src/onboarding-server.ts
801
- var execFileAsync = promisify(execFile);
1821
+ var execFileAsync2 = promisify2(execFile2);
802
1822
  var PICKER_COPY = {
803
1823
  "local-source": {
804
1824
  prompt: "Select a local project folder to tag in Pac-Man",
@@ -809,20 +1829,29 @@ var PICKER_COPY = {
809
1829
  title: "Select workspace folder"
810
1830
  }
811
1831
  };
1832
+ var pickerPathCache = /* @__PURE__ */ new Map();
1833
+ var PICKER_PATH_CACHE_TTL = 3e4;
812
1834
  async function resolvePickerStartPath(defaultPath) {
813
1835
  const trimmed = defaultPath?.trim();
814
1836
  if (!trimmed) {
815
1837
  return void 0;
816
1838
  }
817
1839
  const resolvedPath = path2.resolve(trimmed);
1840
+ const cached = pickerPathCache.get(resolvedPath);
1841
+ if (cached && Date.now() - cached.validatedAt < PICKER_PATH_CACHE_TTL) {
1842
+ return cached.resolvedPath;
1843
+ }
818
1844
  const candidates = [resolvedPath, path2.dirname(resolvedPath)];
819
- for (const candidate of candidates) {
820
- try {
1845
+ const results = await Promise.allSettled(
1846
+ candidates.map(async (candidate) => {
821
1847
  const stats = await fs2.stat(candidate);
822
- if (stats.isDirectory()) {
823
- return candidate;
824
- }
825
- } catch {
1848
+ return stats.isDirectory() ? candidate : null;
1849
+ })
1850
+ );
1851
+ for (const result of results) {
1852
+ if (result.status === "fulfilled" && result.value) {
1853
+ pickerPathCache.set(resolvedPath, { resolvedPath: result.value, validatedAt: Date.now() });
1854
+ return result.value;
826
1855
  }
827
1856
  }
828
1857
  return void 0;
@@ -844,7 +1873,7 @@ async function openNativeFolderPicker(platform, purpose, startPath) {
844
1873
  const copy = PICKER_COPY[purpose];
845
1874
  if (platform === "darwin") {
846
1875
  const script = startPath ? `POSIX path of (choose folder with prompt "${escapeAppleScriptString(copy.prompt)}" default location POSIX file "${escapeAppleScriptString(startPath)}")` : `POSIX path of (choose folder with prompt "${escapeAppleScriptString(copy.prompt)}")`;
847
- const { stdout } = await execFileAsync("osascript", ["-e", script]);
1876
+ const { stdout } = await execFileAsync2("osascript", ["-e", script]);
848
1877
  return stdout.trim();
849
1878
  }
850
1879
  if (platform === "linux") {
@@ -852,7 +1881,7 @@ async function openNativeFolderPicker(platform, purpose, startPath) {
852
1881
  if (startPath) {
853
1882
  args.push(`--filename=${ensureTrailingSeparator(startPath)}`);
854
1883
  }
855
- const { stdout } = await execFileAsync("zenity", args);
1884
+ const { stdout } = await execFileAsync2("zenity", args);
856
1885
  return stdout.trim();
857
1886
  }
858
1887
  if (platform === "win32") {
@@ -863,15 +1892,31 @@ async function openNativeFolderPicker(platform, purpose, startPath) {
863
1892
  ...startPath ? [`$dialog.SelectedPath = '${escapePowerShellString(startPath)}'`] : [],
864
1893
  "if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }"
865
1894
  ].join("; ");
866
- const { stdout } = await execFileAsync("powershell", ["-NoProfile", "-Command", script]);
1895
+ const { stdout } = await execFileAsync2("powershell", ["-NoProfile", "-Command", script]);
867
1896
  return stdout.trim();
868
1897
  }
869
1898
  throw new Error("Folder picker not supported on this platform");
870
1899
  }
871
1900
  async function startOnboardingServer(port, workspacePath) {
872
1901
  const app = express();
1902
+ app.use((req, res, next) => {
1903
+ const origin = req.headers.origin;
1904
+ if (origin) {
1905
+ res.header("Access-Control-Allow-Origin", origin);
1906
+ res.header("Vary", "Origin");
1907
+ } else {
1908
+ res.header("Access-Control-Allow-Origin", "*");
1909
+ }
1910
+ res.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
1911
+ res.header("Access-Control-Allow-Headers", "Content-Type, Accept");
1912
+ if (req.method === "OPTIONS") {
1913
+ res.sendStatus(204);
1914
+ return;
1915
+ }
1916
+ next();
1917
+ });
873
1918
  app.use(express.json());
874
- let gdriveAuthSession = null;
1919
+ let googleAuthSession = null;
875
1920
  const onboardingStaticPath = await resolveOnboardingStaticPath();
876
1921
  if (onboardingStaticPath) {
877
1922
  app.use(express.static(onboardingStaticPath));
@@ -917,6 +1962,7 @@ async function startOnboardingServer(port, workspacePath) {
917
1962
  assistantName,
918
1963
  responsibilities,
919
1964
  localFolders,
1965
+ driveFolders,
920
1966
  integrations
921
1967
  } = req.body;
922
1968
  try {
@@ -926,6 +1972,7 @@ async function startOnboardingServer(port, workspacePath) {
926
1972
  assistantName,
927
1973
  responsibilities: responsibilities ?? [],
928
1974
  localFolders: localFolders ?? [],
1975
+ driveFolders: driveFolders ?? [],
929
1976
  integrations: integrations ?? []
930
1977
  });
931
1978
  res.json(preview);
@@ -933,6 +1980,68 @@ async function startOnboardingServer(port, workspacePath) {
933
1980
  res.status(400).json({ error: String(err) });
934
1981
  }
935
1982
  });
1983
+ app.post("/api/build-project-knowledge", async (req, res) => {
1984
+ const { project, files, provider, userProfile, integrations, localFolders, driveFolders } = req.body;
1985
+ if (!project?.slug || !project.name) {
1986
+ res.status(400).json({ error: "Project slug and name are required." });
1987
+ return;
1988
+ }
1989
+ try {
1990
+ const selectedProvider = provider?.type === "openai" || provider?.type === "anthropic" ? {
1991
+ type: provider.type,
1992
+ apiKey: provider.apiKey ?? "",
1993
+ model: provider.model
1994
+ } : provider?.type === "ollama" ? { type: "ollama" } : void 0;
1995
+ let templateSections;
1996
+ if (userProfile?.profileType) {
1997
+ try {
1998
+ const sections = getTemplateSections(userProfile.profileType);
1999
+ templateSections = sections.map((s) => ({
2000
+ name: s.name,
2001
+ fileName: s.fileName,
2002
+ defaultContent: s.defaultContent
2003
+ }));
2004
+ } catch {
2005
+ }
2006
+ }
2007
+ const result = await buildProjectKnowledgeFiles({
2008
+ project: {
2009
+ slug: project.slug,
2010
+ name: project.name,
2011
+ summary: project.summary ?? "",
2012
+ primaryFocus: project.primaryFocus ?? "",
2013
+ sourceFolders: project.sourceFolders ?? [],
2014
+ connectedIntegrations: project.connectedIntegrations ?? [],
2015
+ githubBranches: project.githubBranches ?? [],
2016
+ gitlabBranches: project.gitlabBranches ?? [],
2017
+ slackChannels: project.slackChannels ?? [],
2018
+ gdriveFolders: project.gdriveFolders ?? [],
2019
+ gdocDocuments: project.gdocDocuments ?? [],
2020
+ keySignals: project.keySignals ?? [],
2021
+ llmInstructions: project.llmInstructions ?? "",
2022
+ templateFiles: project.templateFiles ?? []
2023
+ },
2024
+ files: files ?? {},
2025
+ provider: selectedProvider,
2026
+ userProfile: userProfile ? {
2027
+ name: userProfile.name ?? "",
2028
+ profileType: userProfile.profileType ?? "",
2029
+ assistantName: userProfile.assistantName ?? "",
2030
+ responsibilities: userProfile.responsibilities ?? []
2031
+ } : void 0,
2032
+ integrations: integrations ?? [],
2033
+ localFolders: localFolders ?? [],
2034
+ driveFolders: driveFolders ?? [],
2035
+ templateSections
2036
+ });
2037
+ res.json({
2038
+ files: result.files,
2039
+ message: `Project knowledge generated with ${result.model}. Review the files before finishing onboarding.`
2040
+ });
2041
+ } catch (err) {
2042
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
2043
+ }
2044
+ });
936
2045
  app.post("/api/save", async (req, res) => {
937
2046
  try {
938
2047
  const payload = req.body;
@@ -971,7 +2080,19 @@ async function startOnboardingServer(port, workspacePath) {
971
2080
  JSON.stringify(
972
2081
  payload.inferredProjects.map((project, index) => ({
973
2082
  name: project.slug ?? project.name ?? `project-${index + 1}`,
974
- score: Number((1 - index * 0.1).toFixed(2))
2083
+ score: Number((1 - index * 0.1).toFixed(2)),
2084
+ slug: project.slug ?? `project-${index + 1}`,
2085
+ summary: project.summary ?? "",
2086
+ primaryFocus: project.primaryFocus ?? "",
2087
+ sourceFolders: project.sourceFolders ?? [],
2088
+ connectedIntegrations: project.connectedIntegrations ?? [],
2089
+ githubBranches: project.githubBranches ?? [],
2090
+ gitlabBranches: project.gitlabBranches ?? [],
2091
+ slackChannels: project.slackChannels ?? [],
2092
+ gdriveFolders: project.gdriveFolders ?? [],
2093
+ keySignals: project.keySignals ?? [],
2094
+ llmInstructions: project.llmInstructions ?? "",
2095
+ templateFiles: project.templateFiles ?? []
975
2096
  })),
976
2097
  null,
977
2098
  2
@@ -988,6 +2109,114 @@ async function startOnboardingServer(port, workspacePath) {
988
2109
  res.status(500).json({ error: String(err) });
989
2110
  }
990
2111
  });
2112
+ app.post("/api/save-project", async (req, res) => {
2113
+ try {
2114
+ const { project, templateFiles, allProjects } = req.body;
2115
+ if (!project?.slug || !project.name) {
2116
+ res.status(400).json({ error: "Project slug and name are required." });
2117
+ return;
2118
+ }
2119
+ const effectivePath = workspacePath;
2120
+ await fs2.mkdir(effectivePath, { recursive: true });
2121
+ const projectsToSave = allProjects ?? [project];
2122
+ const suggestionsDir = path2.dirname(
2123
+ path2.join(effectivePath, WORKSPACE_PATHS.context.derived.suggestions.inferredProjects)
2124
+ );
2125
+ await fs2.mkdir(suggestionsDir, { recursive: true });
2126
+ await fs2.writeFile(
2127
+ path2.join(effectivePath, WORKSPACE_PATHS.context.derived.suggestions.inferredProjects),
2128
+ JSON.stringify(
2129
+ projectsToSave.map((p, index) => ({
2130
+ name: p.slug ?? p.name ?? `project-${index + 1}`,
2131
+ score: Number((1 - index * 0.1).toFixed(2)),
2132
+ slug: p.slug ?? `project-${index + 1}`,
2133
+ summary: p.summary ?? "",
2134
+ primaryFocus: p.primaryFocus ?? "",
2135
+ sourceFolders: p.sourceFolders ?? [],
2136
+ connectedIntegrations: p.connectedIntegrations ?? [],
2137
+ githubBranches: p.githubBranches ?? [],
2138
+ gitlabBranches: p.gitlabBranches ?? [],
2139
+ slackChannels: p.slackChannels ?? [],
2140
+ gdriveFolders: p.gdriveFolders ?? [],
2141
+ keySignals: p.keySignals ?? [],
2142
+ llmInstructions: p.llmInstructions ?? "",
2143
+ templateFiles: p.templateFiles ?? []
2144
+ })),
2145
+ null,
2146
+ 2
2147
+ ),
2148
+ "utf-8"
2149
+ );
2150
+ if (templateFiles) {
2151
+ const canonicalDir = path2.join(effectivePath, "context", "canonical");
2152
+ for (const [filePath, content] of Object.entries(templateFiles)) {
2153
+ if (filePath.startsWith(`projects/${project.slug}`) || filePath.startsWith(`${project.slug}/`)) {
2154
+ const fullPath = path2.join(canonicalDir, filePath);
2155
+ await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
2156
+ await fs2.writeFile(fullPath, content, "utf-8");
2157
+ }
2158
+ }
2159
+ }
2160
+ res.json({ success: true, slug: project.slug });
2161
+ } catch (err) {
2162
+ res.status(500).json({ error: String(err) });
2163
+ }
2164
+ });
2165
+ app.post("/api/delete-project", async (req, res) => {
2166
+ try {
2167
+ const { slug, allProjects } = req.body;
2168
+ if (!slug) {
2169
+ res.status(400).json({ error: "Project slug is required." });
2170
+ return;
2171
+ }
2172
+ const effectivePath = workspacePath;
2173
+ const suggestionsFile = path2.join(
2174
+ effectivePath,
2175
+ WORKSPACE_PATHS.context.derived.suggestions.inferredProjects
2176
+ );
2177
+ const projectsToSave = (allProjects ?? []).filter((p) => p.slug !== slug);
2178
+ const suggestionsDir = path2.dirname(suggestionsFile);
2179
+ await fs2.mkdir(suggestionsDir, { recursive: true });
2180
+ await fs2.writeFile(
2181
+ suggestionsFile,
2182
+ JSON.stringify(
2183
+ projectsToSave.map((p, index) => ({
2184
+ name: p.slug ?? p.name ?? `project-${index + 1}`,
2185
+ score: Number((1 - index * 0.1).toFixed(2)),
2186
+ slug: p.slug ?? `project-${index + 1}`,
2187
+ summary: p.summary ?? "",
2188
+ primaryFocus: p.primaryFocus ?? "",
2189
+ sourceFolders: p.sourceFolders ?? [],
2190
+ connectedIntegrations: p.connectedIntegrations ?? [],
2191
+ githubBranches: p.githubBranches ?? [],
2192
+ gitlabBranches: p.gitlabBranches ?? [],
2193
+ slackChannels: p.slackChannels ?? [],
2194
+ gdriveFolders: p.gdriveFolders ?? [],
2195
+ keySignals: p.keySignals ?? [],
2196
+ llmInstructions: p.llmInstructions ?? "",
2197
+ templateFiles: p.templateFiles ?? []
2198
+ })),
2199
+ null,
2200
+ 2
2201
+ ),
2202
+ "utf-8"
2203
+ );
2204
+ const canonicalDir = path2.join(effectivePath, "context", "canonical");
2205
+ const projectDir = path2.join(canonicalDir, slug);
2206
+ const projectFile = path2.join(canonicalDir, "projects", `${slug}.md`);
2207
+ try {
2208
+ await fs2.rm(projectDir, { recursive: true, force: true });
2209
+ } catch {
2210
+ }
2211
+ try {
2212
+ await fs2.rm(projectFile, { force: true });
2213
+ } catch {
2214
+ }
2215
+ res.json({ success: true, slug });
2216
+ } catch (err) {
2217
+ res.status(500).json({ error: String(err) });
2218
+ }
2219
+ });
991
2220
  app.post("/api/validate-integration", async (req, res) => {
992
2221
  const { type, credentials } = req.body;
993
2222
  try {
@@ -1017,6 +2246,14 @@ async function startOnboardingServer(port, workspacePath) {
1017
2246
  res.json({ valid: false, error: String(err) });
1018
2247
  }
1019
2248
  });
2249
+ app.post("/api/slack-manifest", (req, res) => {
2250
+ const { assistantName } = req.body;
2251
+ try {
2252
+ res.json(buildSlackManifest({ assistantName }));
2253
+ } catch (err) {
2254
+ res.status(400).json({ error: String(err) });
2255
+ }
2256
+ });
1020
2257
  app.post("/api/validate-slack-runtime", async (req, res) => {
1021
2258
  const {
1022
2259
  botToken,
@@ -1088,6 +2325,133 @@ async function startOnboardingServer(port, workspacePath) {
1088
2325
  });
1089
2326
  }
1090
2327
  });
2328
+ app.post("/api/slack/channels", async (req, res) => {
2329
+ const { botToken } = req.body;
2330
+ if (!botToken) {
2331
+ res.status(400).json({ error: "Slack bot token is required." });
2332
+ return;
2333
+ }
2334
+ try {
2335
+ const connector = createSlackConnector();
2336
+ await connector.authenticate({
2337
+ type: "slack",
2338
+ enabled: true,
2339
+ credentials: { botToken }
2340
+ });
2341
+ const channels = (await connector.listChannels()).slice(0, 100);
2342
+ res.json({
2343
+ options: channels.map((channel) => ({
2344
+ value: channel.id,
2345
+ label: `#${channel.name}${channel.isPrivate ? " (private)" : ""}`
2346
+ }))
2347
+ });
2348
+ } catch (err) {
2349
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
2350
+ }
2351
+ });
2352
+ app.post("/api/github/branches", async (req, res) => {
2353
+ const { token } = req.body;
2354
+ if (!token) {
2355
+ res.status(400).json({ error: "GitHub personal access token is required." });
2356
+ return;
2357
+ }
2358
+ try {
2359
+ const headers = {
2360
+ Authorization: `token ${token}`,
2361
+ Accept: "application/vnd.github+json",
2362
+ "User-Agent": "personal-assistant"
2363
+ };
2364
+ const userResponse = await fetch("https://api.github.com/user", {
2365
+ headers,
2366
+ signal: AbortSignal.timeout(4e3)
2367
+ });
2368
+ if (!userResponse.ok) {
2369
+ throw new Error("Failed to load the authenticated GitHub user.");
2370
+ }
2371
+ const user = await userResponse.json();
2372
+ if (!user.login) {
2373
+ throw new Error("GitHub did not return the authenticated login.");
2374
+ }
2375
+ const eventsResponse = await fetch(
2376
+ `https://api.github.com/users/${encodeURIComponent(user.login)}/events?per_page=50`,
2377
+ {
2378
+ headers,
2379
+ signal: AbortSignal.timeout(5e3)
2380
+ }
2381
+ );
2382
+ if (!eventsResponse.ok) {
2383
+ throw new Error("Failed to load recent GitHub activity for branch selection.");
2384
+ }
2385
+ const events = await eventsResponse.json();
2386
+ const seen = /* @__PURE__ */ new Set();
2387
+ const options = events.filter((event) => event.type === "PushEvent").map((event) => {
2388
+ const repo = event.repo?.name?.trim();
2389
+ const ref = event.payload?.ref?.trim() ?? "";
2390
+ const branch = ref.startsWith("refs/heads/") ? ref.replace("refs/heads/", "") : "";
2391
+ if (!repo || !branch) {
2392
+ return null;
2393
+ }
2394
+ const value = `${repo}:${branch}`;
2395
+ if (seen.has(value)) {
2396
+ return null;
2397
+ }
2398
+ seen.add(value);
2399
+ return {
2400
+ value,
2401
+ label: `${repo} \xB7 ${branch}`
2402
+ };
2403
+ }).filter((option) => Boolean(option)).slice(0, 40);
2404
+ res.json({
2405
+ options
2406
+ });
2407
+ } catch (err) {
2408
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
2409
+ }
2410
+ });
2411
+ app.post("/api/gitlab/branches", async (req, res) => {
2412
+ const { token, baseUrl } = req.body;
2413
+ if (!token) {
2414
+ res.status(400).json({ error: "GitLab personal access token is required." });
2415
+ return;
2416
+ }
2417
+ const resolvedBaseUrl = (baseUrl?.trim() || "https://gitlab.com/api/v4").replace(/\/$/, "");
2418
+ try {
2419
+ const headers = { "PRIVATE-TOKEN": token };
2420
+ const eventsResponse = await fetch(
2421
+ `${resolvedBaseUrl}/events?action=pushed&per_page=50`,
2422
+ {
2423
+ headers,
2424
+ signal: AbortSignal.timeout(5e3)
2425
+ }
2426
+ );
2427
+ if (!eventsResponse.ok) {
2428
+ throw new Error("Failed to load recent GitLab push activity for branch selection.");
2429
+ }
2430
+ const events = await eventsResponse.json();
2431
+ const seen = /* @__PURE__ */ new Set();
2432
+ const options = events.map((event) => {
2433
+ const repo = event.project?.path_with_namespace?.trim();
2434
+ const branch = event.push_data?.ref?.trim() ?? "";
2435
+ if (!repo || !branch) {
2436
+ return null;
2437
+ }
2438
+ const value = `${repo}:${branch}`;
2439
+ if (seen.has(value)) {
2440
+ return null;
2441
+ }
2442
+ seen.add(value);
2443
+ return {
2444
+ value,
2445
+ label: `${repo} \xB7 ${branch}`
2446
+ };
2447
+ }).filter((option) => Boolean(option)).slice(0, 40);
2448
+ res.json({
2449
+ options
2450
+ });
2451
+ } catch (err) {
2452
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
2453
+ }
2454
+ });
1091
2455
  app.post("/api/pick-folder", async (req, res) => {
1092
2456
  const { defaultPath, purpose = "storage" } = req.body;
1093
2457
  const pickerPurpose = purpose === "local-source" ? "local-source" : "storage";
@@ -1124,48 +2488,124 @@ async function startOnboardingServer(port, workspacePath) {
1124
2488
  });
1125
2489
  }
1126
2490
  });
1127
- app.post("/api/gdrive/auth-start", async (req, res) => {
1128
- const { clientId, clientSecret } = req.body;
1129
- if (!clientId || !clientSecret) {
1130
- res.status(400).json({ error: "clientId and clientSecret are required" });
1131
- return;
2491
+ const createGoogleDriveClient = async (requiredScopes = [], credentials) => {
2492
+ const activeSession = googleAuthSession && (googleAuthSession.status === "authenticated" || googleAuthSession.status === "complete") ? googleAuthSession : null;
2493
+ const fallbackClientId = credentials?.clientId?.trim() ?? "";
2494
+ const fallbackClientSecret = credentials?.clientSecret?.trim() ?? "";
2495
+ const fallbackRefreshToken = credentials?.refreshToken?.trim() ?? "";
2496
+ const fallbackGrantedScopes = Array.isArray(credentials?.grantedScopes) ? credentials.grantedScopes.filter((scope) => typeof scope === "string").map((scope) => scope.trim()).filter(Boolean) : [];
2497
+ if (!activeSession && (!fallbackClientId || !fallbackClientSecret || !fallbackRefreshToken)) {
2498
+ throw new Error("Not authenticated. Please connect Google first.");
1132
2499
  }
2500
+ const grantedScopes = activeSession?.grantedScopes ?? fallbackGrantedScopes;
2501
+ const missingScopes = grantedScopes.length > 0 ? requiredScopes.filter((scope) => !grantedScopes.includes(scope)) : [];
2502
+ if (missingScopes.length > 0) {
2503
+ throw new Error(
2504
+ missingScopes.includes(GOOGLE_SCOPES_BY_FEATURE.gdrive[0]) ? "Google Drive read access has not been granted yet. Reconnect Google and allow Drive read access." : "Google Drive storage access has not been granted yet. Reconnect Google and allow Drive storage access."
2505
+ );
2506
+ }
2507
+ const { google } = await import("googleapis");
2508
+ const redirectUri = activeSession ? `http://localhost:${port}${activeSession.callbackPath}` : `http://localhost:${port}/api/google/callback`;
2509
+ const oauth2Client = new google.auth.OAuth2(
2510
+ activeSession?.clientId ?? fallbackClientId,
2511
+ activeSession?.clientSecret ?? fallbackClientSecret,
2512
+ redirectUri
2513
+ );
2514
+ oauth2Client.setCredentials({
2515
+ access_token: activeSession?.accessToken,
2516
+ refresh_token: activeSession?.refreshToken ?? fallbackRefreshToken
2517
+ });
2518
+ return {
2519
+ oauth2Client,
2520
+ drive: google.drive({ version: "v3", auth: oauth2Client })
2521
+ };
2522
+ };
2523
+ const buildGoogleStatusPayload = (session) => {
2524
+ if (!session) {
2525
+ return { status: "idle" };
2526
+ }
2527
+ return {
2528
+ status: session.status,
2529
+ requestedFeatures: session.requestedFeatures,
2530
+ requestedScopes: session.requestedScopes,
2531
+ grantedScopes: session.grantedScopes,
2532
+ accountEmail: session.accountEmail,
2533
+ refreshToken: session.refreshToken,
2534
+ folderId: session.folderId,
2535
+ folderName: session.folderName,
2536
+ folderPath: session.folderPath,
2537
+ error: session.error
2538
+ };
2539
+ };
2540
+ const beginGoogleAuth = async (clientId, clientSecret, features, callbackPath) => {
2541
+ const { google } = await import("googleapis");
2542
+ const redirectUri = `http://localhost:${port}${callbackPath}`;
2543
+ const requestedScopes = getGoogleScopesForFeatures(features);
2544
+ const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
2545
+ const authUrl = oauth2Client.generateAuthUrl({
2546
+ access_type: "offline",
2547
+ include_granted_scopes: true,
2548
+ prompt: "consent",
2549
+ scope: requestedScopes
2550
+ });
2551
+ const previousSession = googleAuthSession && googleAuthSession.clientId === clientId && googleAuthSession.clientSecret === clientSecret ? googleAuthSession : null;
2552
+ googleAuthSession = {
2553
+ clientId,
2554
+ clientSecret,
2555
+ callbackPath,
2556
+ requestedFeatures: features,
2557
+ requestedScopes,
2558
+ grantedScopes: previousSession?.grantedScopes ?? [],
2559
+ status: "pending",
2560
+ refreshToken: previousSession?.refreshToken,
2561
+ accountEmail: previousSession?.accountEmail
2562
+ };
2563
+ return { authUrl, requestedScopes };
2564
+ };
2565
+ const completeGoogleAuth = async (code, callbackPath) => {
2566
+ if (!googleAuthSession || googleAuthSession.callbackPath !== callbackPath) {
2567
+ throw new Error("Invalid OAuth callback \u2014 no pending auth session.");
2568
+ }
2569
+ const { google } = await import("googleapis");
2570
+ const redirectUri = `http://localhost:${port}${callbackPath}`;
2571
+ const oauth2Client = new google.auth.OAuth2(
2572
+ googleAuthSession.clientId,
2573
+ googleAuthSession.clientSecret,
2574
+ redirectUri
2575
+ );
2576
+ const { tokens } = await oauth2Client.getToken(code);
2577
+ oauth2Client.setCredentials(tokens);
2578
+ const grantedScopes = normalizeGoogleScopes(tokens.scope) ?? googleAuthSession.requestedScopes;
2579
+ let accountEmail = googleAuthSession.accountEmail;
1133
2580
  try {
1134
- const { google } = await import("googleapis");
1135
- const redirectUri = `http://localhost:${port}/api/gdrive/callback`;
1136
- const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
1137
- const authUrl = oauth2Client.generateAuthUrl({
1138
- access_type: "offline",
1139
- prompt: "consent",
1140
- scope: ["https://www.googleapis.com/auth/drive.file"]
1141
- });
1142
- gdriveAuthSession = { clientId, clientSecret, status: "pending" };
1143
- res.json({ authUrl });
1144
- } catch (err) {
1145
- res.status(500).json({ error: String(err) });
2581
+ if (grantedScopes.includes(GOOGLE_SCOPES_BY_FEATURE.gmail[0])) {
2582
+ const gmail = google.gmail({ version: "v1", auth: oauth2Client });
2583
+ const profile = await gmail.users.getProfile({ userId: "me" });
2584
+ accountEmail = profile.data.emailAddress ?? googleAuthSession.accountEmail;
2585
+ } else if (grantedScopes.includes(GOOGLE_SCOPES_BY_FEATURE.storage[0]) || grantedScopes.includes(GOOGLE_SCOPES_BY_FEATURE.gdrive[0])) {
2586
+ const drive = google.drive({ version: "v3", auth: oauth2Client });
2587
+ const about = await drive.about.get({ fields: "user(emailAddress)" });
2588
+ accountEmail = about.data.user?.emailAddress ?? googleAuthSession.accountEmail;
2589
+ }
2590
+ } catch {
1146
2591
  }
1147
- });
1148
- app.get("/api/gdrive/callback", async (req, res) => {
1149
- const { code } = req.query;
1150
- if (!code || !gdriveAuthSession) {
2592
+ googleAuthSession = {
2593
+ ...googleAuthSession,
2594
+ status: "authenticated",
2595
+ refreshToken: tokens.refresh_token ?? googleAuthSession.refreshToken,
2596
+ accessToken: tokens.access_token ?? void 0,
2597
+ grantedScopes: grantedScopes.length > 0 ? grantedScopes : googleAuthSession.requestedScopes,
2598
+ accountEmail,
2599
+ error: void 0
2600
+ };
2601
+ };
2602
+ const handleGoogleCallback = async (code, callbackPath, res) => {
2603
+ if (!code) {
1151
2604
  res.status(400).send("Invalid OAuth callback \u2014 no pending auth session.");
1152
2605
  return;
1153
2606
  }
1154
2607
  try {
1155
- const { google } = await import("googleapis");
1156
- const redirectUri = `http://localhost:${port}/api/gdrive/callback`;
1157
- const oauth2Client = new google.auth.OAuth2(
1158
- gdriveAuthSession.clientId,
1159
- gdriveAuthSession.clientSecret,
1160
- redirectUri
1161
- );
1162
- const { tokens } = await oauth2Client.getToken(code);
1163
- gdriveAuthSession = {
1164
- ...gdriveAuthSession,
1165
- status: "authenticated",
1166
- refreshToken: tokens.refresh_token ?? void 0,
1167
- accessToken: tokens.access_token ?? void 0
1168
- };
2608
+ await completeGoogleAuth(code, callbackPath);
1169
2609
  res.send(`<!DOCTYPE html><html><body>
1170
2610
  <script>window.close();</script>
1171
2611
  <p style="font-family:sans-serif;padding:2rem;color:#4ade80;">
@@ -1173,40 +2613,187 @@ async function startOnboardingServer(port, workspacePath) {
1173
2613
  </p>
1174
2614
  </body></html>`);
1175
2615
  } catch (err) {
1176
- if (gdriveAuthSession) {
1177
- gdriveAuthSession.status = "error";
1178
- gdriveAuthSession.error = String(err);
2616
+ if (googleAuthSession) {
2617
+ googleAuthSession.status = "error";
2618
+ googleAuthSession.error = String(err);
1179
2619
  }
1180
2620
  res.status(500).send("Authentication failed: " + String(err));
1181
2621
  }
2622
+ };
2623
+ app.post("/api/google/auth-start", async (req, res) => {
2624
+ const { clientId, clientSecret, features } = req.body;
2625
+ if (!clientId || !clientSecret) {
2626
+ res.status(400).json({ error: "clientId and clientSecret are required" });
2627
+ return;
2628
+ }
2629
+ const requestedFeatures = Array.isArray(features) ? [...new Set(features.filter((feature) => feature === "storage" || feature === "gmail" || feature === "gdrive"))] : [];
2630
+ if (requestedFeatures.length === 0) {
2631
+ res.status(400).json({ error: "At least one Google feature is required." });
2632
+ return;
2633
+ }
2634
+ try {
2635
+ const result = await beginGoogleAuth(clientId, clientSecret, requestedFeatures, "/api/google/callback");
2636
+ res.json({
2637
+ authUrl: result.authUrl,
2638
+ requestedFeatures,
2639
+ requestedScopes: result.requestedScopes
2640
+ });
2641
+ } catch (err) {
2642
+ res.status(500).json({ error: String(err) });
2643
+ }
1182
2644
  });
1183
- app.get("/api/gdrive/auth-status", (_req, res) => {
1184
- if (!gdriveAuthSession) {
1185
- res.json({ status: "idle" });
2645
+ app.get("/api/google/callback", async (req, res) => {
2646
+ const { code } = req.query;
2647
+ await handleGoogleCallback(code, "/api/google/callback", res);
2648
+ });
2649
+ app.get("/api/google/auth-status", (_req, res) => {
2650
+ res.json(buildGoogleStatusPayload(googleAuthSession));
2651
+ });
2652
+ app.post("/api/gdrive/auth-start", async (req, res) => {
2653
+ const { clientId, clientSecret } = req.body;
2654
+ if (!clientId || !clientSecret) {
2655
+ res.status(400).json({ error: "clientId and clientSecret are required" });
1186
2656
  return;
1187
2657
  }
1188
- const { clientId: _cid, clientSecret: _csec, accessToken: _at, refreshToken: _rt, ...publicSession } = gdriveAuthSession;
1189
- res.json(publicSession);
2658
+ try {
2659
+ const result = await beginGoogleAuth(clientId, clientSecret, ["storage"], "/api/gdrive/callback");
2660
+ res.json({ authUrl: result.authUrl });
2661
+ } catch (err) {
2662
+ res.status(500).json({ error: String(err) });
2663
+ }
1190
2664
  });
1191
- app.post("/api/gdrive/create-folder", async (req, res) => {
1192
- if (!gdriveAuthSession || gdriveAuthSession.status !== "authenticated") {
1193
- res.status(400).json({ error: "Not authenticated. Please connect Google Drive first." });
2665
+ app.get("/api/gdrive/callback", async (req, res) => {
2666
+ const { code } = req.query;
2667
+ await handleGoogleCallback(code, "/api/gdrive/callback", res);
2668
+ });
2669
+ app.get("/api/gdrive/auth-status", (_req, res) => {
2670
+ res.json(buildGoogleStatusPayload(googleAuthSession));
2671
+ });
2672
+ const listGoogleDocsInFolderScope = async (drive, folders) => {
2673
+ const pending = folders.filter((folder) => folder.id.trim()).map((folder) => ({
2674
+ id: folder.id.trim(),
2675
+ path: folder.path?.trim() || folder.id.trim()
2676
+ }));
2677
+ const visited = /* @__PURE__ */ new Set();
2678
+ const documents = /* @__PURE__ */ new Map();
2679
+ while (pending.length > 0) {
2680
+ const current = pending.shift();
2681
+ if (!current || visited.has(current.id)) {
2682
+ continue;
2683
+ }
2684
+ visited.add(current.id);
2685
+ let pageToken;
2686
+ do {
2687
+ const response = await drive.files.list({
2688
+ q: `'${current.id}' in parents and trashed=false and (mimeType='application/vnd.google-apps.folder' or mimeType='application/vnd.google-apps.document')`,
2689
+ fields: "nextPageToken, files(id,name,mimeType)",
2690
+ orderBy: "name_natural",
2691
+ pageSize: 100,
2692
+ pageToken
2693
+ });
2694
+ for (const file of response.data.files ?? []) {
2695
+ if (!file.id || !file.name) {
2696
+ continue;
2697
+ }
2698
+ const nextPath = `${current.path} / ${file.name}`;
2699
+ if (file.mimeType === "application/vnd.google-apps.folder") {
2700
+ pending.push({ id: file.id, path: nextPath });
2701
+ continue;
2702
+ }
2703
+ if (file.mimeType === "application/vnd.google-apps.document") {
2704
+ documents.set(file.id, {
2705
+ value: nextPath,
2706
+ label: nextPath
2707
+ });
2708
+ }
2709
+ }
2710
+ pageToken = response.data.nextPageToken ?? void 0;
2711
+ } while (pageToken);
2712
+ }
2713
+ return [...documents.values()].sort((left, right) => left.label.localeCompare(right.label));
2714
+ };
2715
+ const listRecentGoogleDocs = async (drive) => {
2716
+ const response = await drive.files.list({
2717
+ q: `mimeType='application/vnd.google-apps.document' and trashed=false`,
2718
+ fields: "files(id,name)",
2719
+ orderBy: "modifiedTime desc",
2720
+ pageSize: 100
2721
+ });
2722
+ return (response.data.files ?? []).filter(
2723
+ (file) => Boolean(file?.id && file?.name)
2724
+ ).map((file) => ({
2725
+ value: file.name,
2726
+ label: file.name
2727
+ }));
2728
+ };
2729
+ const handleGoogleDriveFolders = async (parentId, credentials, res) => {
2730
+ try {
2731
+ const { drive } = await createGoogleDriveClient([], credentials);
2732
+ const response = await drive.files.list({
2733
+ q: `'${parentId}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false`,
2734
+ fields: "files(id,name)",
2735
+ orderBy: "name_natural",
2736
+ pageSize: 200
2737
+ });
2738
+ res.json({
2739
+ folders: (response.data.files ?? []).filter((file) => file.id && file.name).map((file) => ({
2740
+ id: file.id,
2741
+ name: file.name
2742
+ }))
2743
+ });
2744
+ } catch (err) {
2745
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
2746
+ }
2747
+ };
2748
+ app.get("/api/google/drive-folders", async (req, res) => {
2749
+ const parentId = typeof req.query.parentId === "string" && req.query.parentId.trim() ? req.query.parentId.trim() : "root";
2750
+ await handleGoogleDriveFolders(parentId, void 0, res);
2751
+ });
2752
+ app.post("/api/google/drive-folders", async (req, res) => {
2753
+ const parentId = typeof req.body?.parentId === "string" && req.body.parentId.trim() ? req.body.parentId.trim() : "root";
2754
+ await handleGoogleDriveFolders(parentId, req.body, res);
2755
+ });
2756
+ app.post("/api/google/access-token", async (req, res) => {
2757
+ const clientId = req.body.clientId?.trim();
2758
+ const clientSecret = req.body.clientSecret?.trim();
2759
+ const refreshToken = req.body.refreshToken?.trim();
2760
+ if (!clientId || !clientSecret || !refreshToken) {
2761
+ res.status(400).json({ error: "clientId, clientSecret, and refreshToken are required." });
1194
2762
  return;
1195
2763
  }
1196
- const { folderName = "Personal Assistant", parentFolderName = "" } = req.body;
1197
2764
  try {
1198
2765
  const { google } = await import("googleapis");
1199
- const redirectUri = `http://localhost:${port}/api/gdrive/callback`;
1200
- const oauth2Client = new google.auth.OAuth2(
1201
- gdriveAuthSession.clientId,
1202
- gdriveAuthSession.clientSecret,
1203
- redirectUri
2766
+ const oauth2Client = new google.auth.OAuth2(clientId, clientSecret);
2767
+ oauth2Client.setCredentials({ refresh_token: refreshToken });
2768
+ const { token } = await oauth2Client.getAccessToken();
2769
+ if (!token) {
2770
+ res.status(500).json({ error: "Failed to obtain access token." });
2771
+ return;
2772
+ }
2773
+ res.json({ accessToken: token });
2774
+ } catch (err) {
2775
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
2776
+ }
2777
+ });
2778
+ app.post("/api/google/documents", async (req, res) => {
2779
+ const folders = Array.isArray(req.body?.folders) ? req.body.folders.filter(
2780
+ (folder) => Boolean(folder) && typeof folder === "object" && typeof folder.id === "string"
2781
+ ) : [];
2782
+ try {
2783
+ const { drive } = await createGoogleDriveClient(
2784
+ [GOOGLE_SCOPES_BY_FEATURE.gdrive[0]],
2785
+ req.body
1204
2786
  );
1205
- oauth2Client.setCredentials({
1206
- access_token: gdriveAuthSession.accessToken,
1207
- refresh_token: gdriveAuthSession.refreshToken
1208
- });
1209
- const drive = google.drive({ version: "v3", auth: oauth2Client });
2787
+ const options = folders.length > 0 ? await listGoogleDocsInFolderScope(drive, folders) : await listRecentGoogleDocs(drive);
2788
+ res.json({ options });
2789
+ } catch (err) {
2790
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
2791
+ }
2792
+ });
2793
+ app.post("/api/gdrive/create-folder", async (req, res) => {
2794
+ const { folderName = "Personal Assistant", parentFolderName = "" } = req.body;
2795
+ try {
2796
+ const { oauth2Client, drive } = await createGoogleDriveClient([GOOGLE_SCOPES_BY_FEATURE.storage[0]]);
1210
2797
  let parentId = "root";
1211
2798
  let locationPath = "My Drive";
1212
2799
  if (parentFolderName.trim()) {
@@ -1253,9 +2840,13 @@ async function startOnboardingServer(port, workspacePath) {
1253
2840
  }
1254
2841
  const folderPath = `${locationPath} / ${resolvedFolderName}`;
1255
2842
  const freshCredentials = await oauth2Client.getAccessToken();
1256
- const latestRefreshToken = oauth2Client.credentials.refresh_token ?? gdriveAuthSession.refreshToken;
1257
- gdriveAuthSession = {
1258
- ...gdriveAuthSession,
2843
+ const currentSession = googleAuthSession;
2844
+ if (!currentSession) {
2845
+ throw new Error("Google auth session was lost before the folder could be saved.");
2846
+ }
2847
+ const latestRefreshToken = oauth2Client.credentials.refresh_token ?? currentSession.refreshToken;
2848
+ googleAuthSession = {
2849
+ ...currentSession,
1259
2850
  status: "complete",
1260
2851
  folderId,
1261
2852
  folderName: resolvedFolderName,