@mukulaggarwal/pacman 0.1.5 → 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.
- package/dist/{chunk-DNI6TIXZ.js → chunk-LKKDJ5A7.js} +3 -3
- package/dist/{chunk-WH3UMGHQ.js → chunk-S5ZLV5QT.js} +2 -2
- package/dist/{chunk-YJ32S56Q.js → chunk-X6CHTBN2.js} +5 -3
- package/dist/chunk-X6CHTBN2.js.map +1 -0
- package/dist/daemon.js +1 -1
- package/dist/{dist-WFQSK6BF.js → dist-6XSJ2XC4.js} +48 -7
- package/dist/dist-6XSJ2XC4.js.map +1 -0
- package/dist/{dist-RFHCRKM3.js → dist-GJFZVMLI.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-compat.js +2 -2
- package/dist/onboarding-server.js +1684 -93
- package/dist/onboarding-server.js.map +1 -1
- package/dist/onboarding-web/assets/index-BwF9HPxA.css +1 -0
- package/dist/onboarding-web/assets/index-C0NQ8pNs.js +99 -0
- package/dist/onboarding-web/index.html +2 -2
- package/dist/slack-listener.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-YJ32S56Q.js.map +0 -1
- package/dist/dist-WFQSK6BF.js.map +0 -1
- package/dist/onboarding-web/assets/index-BOAOMlJT.css +0 -1
- package/dist/onboarding-web/assets/index-DBwmBIzA.js +0 -71
- /package/dist/{chunk-DNI6TIXZ.js.map → chunk-LKKDJ5A7.js.map} +0 -0
- /package/dist/{chunk-WH3UMGHQ.js.map → chunk-S5ZLV5QT.js.map} +0 -0
- /package/dist/{dist-RFHCRKM3.js.map → dist-GJFZVMLI.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createNoopEventClient,
|
|
3
3
|
validateIntegrationConfig
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
|
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("##
|
|
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
|
-
"##
|
|
1139
|
+
"## Sources",
|
|
519
1140
|
"",
|
|
520
1141
|
formatBulletList(
|
|
521
|
-
project.sourceFolders.map((
|
|
522
|
-
"No
|
|
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
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
820
|
-
|
|
1845
|
+
const results = await Promise.allSettled(
|
|
1846
|
+
candidates.map(async (candidate) => {
|
|
821
1847
|
const stats = await fs2.stat(candidate);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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 (
|
|
1177
|
-
|
|
1178
|
-
|
|
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/
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
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.
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
});
|
|
1209
|
-
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
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,
|