@mukulaggarwal/pacman 0.1.3 → 0.1.4
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/README.md +164 -18
- package/dist/{chunk-VCDPIN57.js → chunk-WH3UMGHQ.js} +21 -16
- package/dist/chunk-WH3UMGHQ.js.map +1 -0
- package/dist/index.js +44 -40
- package/dist/index.js.map +1 -1
- package/dist/mcp-compat.js +3 -3
- package/dist/mcp-compat.js.map +1 -1
- package/dist/onboarding-server.js +529 -46
- package/dist/onboarding-server.js.map +1 -1
- package/dist/onboarding-web/assets/index-BOAOMlJT.css +1 -0
- package/dist/onboarding-web/assets/index-roRRNUhE.js +71 -0
- package/dist/onboarding-web/index.html +5 -5
- package/package.json +1 -1
- package/dist/chunk-VCDPIN57.js.map +0 -1
- package/dist/onboarding-web/assets/index-IBoT-KD4.js +0 -75
- package/dist/onboarding-web/assets/index-ag5J9F2y.css +0 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validateProviderConfig
|
|
10
10
|
} from "./chunk-O6T35A4O.js";
|
|
11
11
|
import {
|
|
12
|
+
WORKSPACE_PATHS,
|
|
12
13
|
createConfigManager,
|
|
13
14
|
createGDriveStorage,
|
|
14
15
|
createLocalStorage
|
|
@@ -20,9 +21,9 @@ import {
|
|
|
20
21
|
|
|
21
22
|
// src/onboarding-server.ts
|
|
22
23
|
import express from "express";
|
|
23
|
-
import * as
|
|
24
|
-
import * as
|
|
25
|
-
import {
|
|
24
|
+
import * as path2 from "path";
|
|
25
|
+
import * as fs2 from "fs/promises";
|
|
26
|
+
import { execFile } from "child_process";
|
|
26
27
|
import { promisify } from "util";
|
|
27
28
|
|
|
28
29
|
// ../template-engine/dist/index.js
|
|
@@ -427,8 +428,446 @@ function renderTemplate(template, user) {
|
|
|
427
428
|
return files;
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
// src/project-structure.ts
|
|
432
|
+
import * as fs from "fs/promises";
|
|
433
|
+
import * as path from "path";
|
|
434
|
+
var MAX_DIR_ENTRIES = 10;
|
|
435
|
+
var README_CANDIDATES = ["README.md", "readme.md", "README.txt", "readme.txt"];
|
|
436
|
+
var OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
437
|
+
async function buildProjectStructurePreview(input) {
|
|
438
|
+
const baseFiles = renderTemplate(getTemplate(input.profileType), {
|
|
439
|
+
name: input.name,
|
|
440
|
+
assistantName: input.assistantName,
|
|
441
|
+
responsibilities: input.responsibilities
|
|
442
|
+
});
|
|
443
|
+
const folderSignals = await Promise.all(
|
|
444
|
+
input.localFolders.filter((folder) => folder.path.trim()).map((folder) => collectFolderSignal(folder))
|
|
445
|
+
);
|
|
446
|
+
const heuristicProjects = buildHeuristicProjects(folderSignals, input.integrations);
|
|
447
|
+
const ollamaResult = await enrichProjectsWithOllama(
|
|
448
|
+
heuristicProjects,
|
|
449
|
+
folderSignals,
|
|
450
|
+
input.integrations
|
|
451
|
+
);
|
|
452
|
+
const projects = ollamaResult?.projects ?? heuristicProjects;
|
|
453
|
+
const inference = ollamaResult?.inference ?? {
|
|
454
|
+
strategy: "heuristic",
|
|
455
|
+
note: "Structured locally from folder tags, folder names, and visible directory signals. No external API calls were made."
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
files: buildTemplateFiles(baseFiles, projects, folderSignals, input.integrations),
|
|
459
|
+
projects,
|
|
460
|
+
inference
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function buildTemplateFiles(baseFiles, projects, folderSignals, integrations) {
|
|
464
|
+
const files = { ...baseFiles };
|
|
465
|
+
const integrationNames = integrations.map(formatIntegrationName);
|
|
466
|
+
files["overview.md"] = [
|
|
467
|
+
baseFiles["overview.md"] ?? "# Overview",
|
|
468
|
+
"",
|
|
469
|
+
"## Confirmed Project Structure",
|
|
470
|
+
"",
|
|
471
|
+
projects.length > 0 ? projects.map((project) => `- **${project.name}** \u2014 ${project.primaryFocus || project.summary}`).join("\n") : "- No project groups were inferred yet. Add local folders or integrations and revisit this step."
|
|
472
|
+
].join("\n");
|
|
473
|
+
const sourceLines = [];
|
|
474
|
+
if (folderSignals.length > 0) {
|
|
475
|
+
sourceLines.push("## Local Sources", "");
|
|
476
|
+
sourceLines.push(...folderSignals.map((signal) => `- \`${signal.path}\``));
|
|
477
|
+
sourceLines.push("");
|
|
478
|
+
}
|
|
479
|
+
if (integrationNames.length > 0) {
|
|
480
|
+
sourceLines.push("## Connected Integrations", "");
|
|
481
|
+
sourceLines.push(...integrationNames.map((name) => `- ${name}`));
|
|
482
|
+
sourceLines.push("");
|
|
483
|
+
}
|
|
484
|
+
if (sourceLines.length > 0) {
|
|
485
|
+
files["docs.md"] = [baseFiles["docs.md"] ?? "# Documentation References", "", ...sourceLines].join(
|
|
486
|
+
"\n"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (integrationNames.length > 0) {
|
|
490
|
+
files["integrations.md"] = [
|
|
491
|
+
"# Connected Integrations",
|
|
492
|
+
"",
|
|
493
|
+
...integrationNames.map((name) => `- ${name}`),
|
|
494
|
+
"",
|
|
495
|
+
"## Notes",
|
|
496
|
+
"",
|
|
497
|
+
"These integrations are workspace-level sources. Confirm project-specific mappings after the first sync.",
|
|
498
|
+
""
|
|
499
|
+
].join("\n");
|
|
500
|
+
}
|
|
501
|
+
for (const project of projects) {
|
|
502
|
+
files[`projects/${project.slug}.md`] = buildProjectFile(project);
|
|
503
|
+
}
|
|
504
|
+
return files;
|
|
505
|
+
}
|
|
506
|
+
function buildProjectFile(project) {
|
|
507
|
+
return [
|
|
508
|
+
`# ${project.name}`,
|
|
509
|
+
"",
|
|
510
|
+
"## Summary",
|
|
511
|
+
"",
|
|
512
|
+
project.summary || "<!-- Add a short summary for this project -->",
|
|
513
|
+
"",
|
|
514
|
+
"## Primary Focus",
|
|
515
|
+
"",
|
|
516
|
+
project.primaryFocus || "<!-- Capture the main problem area, domain, or ownership -->",
|
|
517
|
+
"",
|
|
518
|
+
"## Local Sources",
|
|
519
|
+
"",
|
|
520
|
+
formatBulletList(
|
|
521
|
+
project.sourceFolders.map((folder) => `\`${folder}\``),
|
|
522
|
+
"No local folders linked to this project yet."
|
|
523
|
+
),
|
|
524
|
+
"",
|
|
525
|
+
"## Connected Integrations",
|
|
526
|
+
"",
|
|
527
|
+
formatBulletList(
|
|
528
|
+
project.connectedIntegrations,
|
|
529
|
+
"No integrations connected during onboarding."
|
|
530
|
+
),
|
|
531
|
+
"",
|
|
532
|
+
"## Grouping Signals",
|
|
533
|
+
"",
|
|
534
|
+
formatBulletList(project.keySignals, "Add more details after the first sync."),
|
|
535
|
+
"",
|
|
536
|
+
"## Notes To Confirm",
|
|
537
|
+
"",
|
|
538
|
+
"- Owners:",
|
|
539
|
+
"- Key repos/docs:",
|
|
540
|
+
"- Important decisions:",
|
|
541
|
+
""
|
|
542
|
+
].join("\n");
|
|
543
|
+
}
|
|
544
|
+
function formatBulletList(items, fallback) {
|
|
545
|
+
if (items.length === 0) {
|
|
546
|
+
return `- ${fallback}`;
|
|
547
|
+
}
|
|
548
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
549
|
+
}
|
|
550
|
+
async function collectFolderSignal(folder) {
|
|
551
|
+
const rawPath = folder.path.trim();
|
|
552
|
+
const resolvedPath = path.resolve(rawPath);
|
|
553
|
+
const basename2 = path.basename(resolvedPath);
|
|
554
|
+
const notes = [];
|
|
555
|
+
const entries = [];
|
|
556
|
+
let packageName;
|
|
557
|
+
let readmeHeading;
|
|
558
|
+
if (folder.project?.trim()) {
|
|
559
|
+
notes.push(`Tagged as "${folder.project.trim()}" during onboarding`);
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
563
|
+
const visibleEntries = dirEntries.filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_DIR_ENTRIES).map((entry) => entry.isDirectory() ? `${entry.name}/` : entry.name);
|
|
564
|
+
entries.push(...visibleEntries);
|
|
565
|
+
if (visibleEntries.length > 0) {
|
|
566
|
+
notes.push(`Top entries: ${visibleEntries.slice(0, 5).join(", ")}`);
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
notes.push("Folder contents could not be read during onboarding");
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const packageJson = await fs.readFile(path.join(resolvedPath, "package.json"), "utf-8");
|
|
573
|
+
const parsed = JSON.parse(packageJson);
|
|
574
|
+
if (parsed.name?.trim()) {
|
|
575
|
+
packageName = parsed.name.trim();
|
|
576
|
+
notes.push(`package.json name: ${packageName}`);
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
for (const candidate of README_CANDIDATES) {
|
|
581
|
+
try {
|
|
582
|
+
const readme = await fs.readFile(path.join(resolvedPath, candidate), "utf-8");
|
|
583
|
+
const heading = extractReadmeHeading(readme);
|
|
584
|
+
if (heading) {
|
|
585
|
+
readmeHeading = heading;
|
|
586
|
+
notes.push(`README heading: ${heading}`);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
path: rawPath,
|
|
594
|
+
explicitProject: folder.project?.trim() || void 0,
|
|
595
|
+
basename: basename2,
|
|
596
|
+
packageName,
|
|
597
|
+
readmeHeading,
|
|
598
|
+
entries,
|
|
599
|
+
notes
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function extractReadmeHeading(contents) {
|
|
603
|
+
for (const rawLine of contents.split("\n").slice(0, 30)) {
|
|
604
|
+
const line = rawLine.trim();
|
|
605
|
+
if (!line) continue;
|
|
606
|
+
if (line.startsWith("#")) {
|
|
607
|
+
return line.replace(/^#+\s*/, "").trim();
|
|
608
|
+
}
|
|
609
|
+
if (line.length <= 80) {
|
|
610
|
+
return line;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
function buildHeuristicProjects(folderSignals, integrations) {
|
|
616
|
+
const integrationNames = integrations.map(formatIntegrationName);
|
|
617
|
+
const groups = /* @__PURE__ */ new Map();
|
|
618
|
+
if (folderSignals.length === 0 && integrationNames.length > 0) {
|
|
619
|
+
return [
|
|
620
|
+
{
|
|
621
|
+
slug: "workspace-context",
|
|
622
|
+
name: "Workspace Context",
|
|
623
|
+
summary: "Connected integrations are ready, but no local folders were added during onboarding. Confirm the main project boundaries after the first sync.",
|
|
624
|
+
primaryFocus: "Cross-project workspace context",
|
|
625
|
+
sourceFolders: [],
|
|
626
|
+
connectedIntegrations: integrationNames,
|
|
627
|
+
keySignals: integrationNames.map((name) => `Connected integration: ${name}`)
|
|
628
|
+
}
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
for (const signal of folderSignals) {
|
|
632
|
+
const groupName = signal.explicitProject || inferProjectName(signal);
|
|
633
|
+
const slug = slugify(groupName) || "untitled-project";
|
|
634
|
+
const existing = groups.get(slug);
|
|
635
|
+
if (existing) {
|
|
636
|
+
existing.sourceFolders.push(signal.path);
|
|
637
|
+
existing.keySignals = unique([...existing.keySignals, ...signal.notes]);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
groups.set(slug, {
|
|
641
|
+
slug,
|
|
642
|
+
name: groupName,
|
|
643
|
+
summary: buildHeuristicSummary(groupName, signal, integrationNames),
|
|
644
|
+
primaryFocus: buildHeuristicFocus(signal),
|
|
645
|
+
sourceFolders: [signal.path],
|
|
646
|
+
connectedIntegrations: integrationNames,
|
|
647
|
+
keySignals: unique(signal.notes)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return [...groups.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
651
|
+
}
|
|
652
|
+
function buildHeuristicSummary(projectName, signal, integrationNames) {
|
|
653
|
+
const integrationSuffix = integrationNames.length > 0 ? ` It will also pull from ${integrationNames.join(", ")} once sync is enabled.` : "";
|
|
654
|
+
if (signal.readmeHeading) {
|
|
655
|
+
return `${projectName} appears to center on "${signal.readmeHeading}". Review and tighten this summary before finishing setup.${integrationSuffix}`;
|
|
656
|
+
}
|
|
657
|
+
if (signal.packageName) {
|
|
658
|
+
return `${projectName} was inferred from the local package "${signal.packageName}". Review and tighten this summary before finishing setup.${integrationSuffix}`;
|
|
659
|
+
}
|
|
660
|
+
return `${projectName} was inferred from the selected local workspace folder. Review and tighten this summary before finishing setup.${integrationSuffix}`;
|
|
661
|
+
}
|
|
662
|
+
function buildHeuristicFocus(signal) {
|
|
663
|
+
if (signal.packageName) {
|
|
664
|
+
return `Owns or contributes to the ${humanizeLabel(signal.packageName)} codebase.`;
|
|
665
|
+
}
|
|
666
|
+
if (signal.entries.some((entry) => entry === "docs/" || entry.toLowerCase().includes("docs"))) {
|
|
667
|
+
return "Mix of implementation and documentation workflows.";
|
|
668
|
+
}
|
|
669
|
+
if (signal.entries.some((entry) => entry === "src/" || entry === "app/" || entry.endsWith(".ts"))) {
|
|
670
|
+
return "Application code, implementation details, and related project context.";
|
|
671
|
+
}
|
|
672
|
+
return `Local context collected from the ${humanizeLabel(signal.basename)} workspace folder.`;
|
|
673
|
+
}
|
|
674
|
+
function inferProjectName(signal) {
|
|
675
|
+
if (signal.packageName) {
|
|
676
|
+
return humanizeLabel(signal.packageName);
|
|
677
|
+
}
|
|
678
|
+
const basename2 = humanizeLabel(signal.basename);
|
|
679
|
+
return basename2 || "Untitled Project";
|
|
680
|
+
}
|
|
681
|
+
async function enrichProjectsWithOllama(projects, folderSignals, integrations) {
|
|
682
|
+
if (projects.length === 0) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const model = await resolveOllamaModel();
|
|
686
|
+
if (!model) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const prompt = [
|
|
690
|
+
"You are organizing local workspace context during onboarding.",
|
|
691
|
+
"Return strict JSON only in the format:",
|
|
692
|
+
'{"projects":[{"slug":"","name":"","summary":"","primaryFocus":"","keySignals":[""]}]}',
|
|
693
|
+
"Rules:",
|
|
694
|
+
"- Keep the existing slugs exactly as provided.",
|
|
695
|
+
"- Do not invent repositories, teams, or integrations that are not in the input.",
|
|
696
|
+
"- Preserve the current project grouping; do not add or remove source folders.",
|
|
697
|
+
"- Summaries should be concise and grounded in the visible folder signals.",
|
|
698
|
+
"- Primary focus should be a short sentence.",
|
|
699
|
+
"",
|
|
700
|
+
JSON.stringify(
|
|
701
|
+
{
|
|
702
|
+
integrations: integrations.map(formatIntegrationName),
|
|
703
|
+
folders: folderSignals,
|
|
704
|
+
projects
|
|
705
|
+
},
|
|
706
|
+
null,
|
|
707
|
+
2
|
|
708
|
+
)
|
|
709
|
+
].join("\n");
|
|
710
|
+
try {
|
|
711
|
+
const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, {
|
|
712
|
+
method: "POST",
|
|
713
|
+
headers: { "Content-Type": "application/json" },
|
|
714
|
+
body: JSON.stringify({
|
|
715
|
+
model,
|
|
716
|
+
prompt,
|
|
717
|
+
stream: false,
|
|
718
|
+
format: "json",
|
|
719
|
+
options: {
|
|
720
|
+
temperature: 0.2
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
signal: AbortSignal.timeout(15e3)
|
|
724
|
+
});
|
|
725
|
+
if (!response.ok) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const payload = await response.json();
|
|
729
|
+
if (!payload.response) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const parsed = JSON.parse(payload.response);
|
|
733
|
+
const enrichedBySlug = new Map(
|
|
734
|
+
(parsed.projects ?? []).filter((project) => project.slug?.trim()).map((project) => [project.slug.trim(), project])
|
|
735
|
+
);
|
|
736
|
+
const enrichedProjects = projects.map((project) => {
|
|
737
|
+
const enriched = enrichedBySlug.get(project.slug);
|
|
738
|
+
if (!enriched) {
|
|
739
|
+
return project;
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
...project,
|
|
743
|
+
name: enriched.name?.trim() || project.name,
|
|
744
|
+
summary: enriched.summary?.trim() || project.summary,
|
|
745
|
+
primaryFocus: enriched.primaryFocus?.trim() || project.primaryFocus,
|
|
746
|
+
keySignals: enriched.keySignals?.map((signal) => signal.trim()).filter(Boolean).slice(0, 5) ?? project.keySignals
|
|
747
|
+
};
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
projects: enrichedProjects,
|
|
751
|
+
inference: {
|
|
752
|
+
strategy: "ollama",
|
|
753
|
+
model,
|
|
754
|
+
note: `Structured locally and enriched with Ollama (${model}). No external API calls were made.`
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function resolveOllamaModel() {
|
|
762
|
+
try {
|
|
763
|
+
const response = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
|
|
764
|
+
signal: AbortSignal.timeout(1500)
|
|
765
|
+
});
|
|
766
|
+
if (!response.ok) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
const payload = await response.json();
|
|
770
|
+
const modelNames = (payload.models ?? []).map((model) => model.name?.trim() ?? "").filter(Boolean);
|
|
771
|
+
if (modelNames.length === 0) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
return modelNames.find((name) => /qwen|llama|mistral|gemma/i.test(name)) ?? modelNames[0];
|
|
775
|
+
} catch {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function formatIntegrationName(id) {
|
|
780
|
+
const names = {
|
|
781
|
+
slack: "Slack",
|
|
782
|
+
github: "GitHub",
|
|
783
|
+
gitlab: "GitLab",
|
|
784
|
+
gmail: "Gmail",
|
|
785
|
+
gdrive: "Google Drive",
|
|
786
|
+
gchat: "Google Chat"
|
|
787
|
+
};
|
|
788
|
+
return names[id] ?? humanizeLabel(id);
|
|
789
|
+
}
|
|
790
|
+
function slugify(value) {
|
|
791
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
792
|
+
}
|
|
793
|
+
function humanizeLabel(value) {
|
|
794
|
+
return value.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
795
|
+
}
|
|
796
|
+
function unique(values) {
|
|
797
|
+
return [...new Set(values.filter(Boolean))];
|
|
798
|
+
}
|
|
799
|
+
|
|
430
800
|
// src/onboarding-server.ts
|
|
431
|
-
var
|
|
801
|
+
var execFileAsync = promisify(execFile);
|
|
802
|
+
var PICKER_COPY = {
|
|
803
|
+
"local-source": {
|
|
804
|
+
prompt: "Select a local project folder to tag in Pac-Man",
|
|
805
|
+
title: "Select project source folder"
|
|
806
|
+
},
|
|
807
|
+
storage: {
|
|
808
|
+
prompt: "Select the Pac-Man workspace folder",
|
|
809
|
+
title: "Select workspace folder"
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
async function resolvePickerStartPath(defaultPath) {
|
|
813
|
+
const trimmed = defaultPath?.trim();
|
|
814
|
+
if (!trimmed) {
|
|
815
|
+
return void 0;
|
|
816
|
+
}
|
|
817
|
+
const resolvedPath = path2.resolve(trimmed);
|
|
818
|
+
const candidates = [resolvedPath, path2.dirname(resolvedPath)];
|
|
819
|
+
for (const candidate of candidates) {
|
|
820
|
+
try {
|
|
821
|
+
const stats = await fs2.stat(candidate);
|
|
822
|
+
if (stats.isDirectory()) {
|
|
823
|
+
return candidate;
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return void 0;
|
|
829
|
+
}
|
|
830
|
+
function escapeAppleScriptString(value) {
|
|
831
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
832
|
+
}
|
|
833
|
+
function escapePowerShellString(value) {
|
|
834
|
+
return value.replace(/'/g, "''");
|
|
835
|
+
}
|
|
836
|
+
function ensureTrailingSeparator(folderPath) {
|
|
837
|
+
return folderPath.endsWith(path2.sep) ? folderPath : `${folderPath}${path2.sep}`;
|
|
838
|
+
}
|
|
839
|
+
function isPickerCancelledError(err) {
|
|
840
|
+
const message = String(err).toLowerCase();
|
|
841
|
+
return message.includes("user canceled") || message.includes("user cancelled") || message.includes("cancel");
|
|
842
|
+
}
|
|
843
|
+
async function openNativeFolderPicker(platform, purpose, startPath) {
|
|
844
|
+
const copy = PICKER_COPY[purpose];
|
|
845
|
+
if (platform === "darwin") {
|
|
846
|
+
const script = startPath ? `POSIX path of (choose folder with prompt "${escapeAppleScriptString(copy.prompt)}" default location POSIX file "${escapeAppleScriptString(startPath)}")` : `POSIX path of (choose folder with prompt "${escapeAppleScriptString(copy.prompt)}")`;
|
|
847
|
+
const { stdout } = await execFileAsync("osascript", ["-e", script]);
|
|
848
|
+
return stdout.trim();
|
|
849
|
+
}
|
|
850
|
+
if (platform === "linux") {
|
|
851
|
+
const args = ["--file-selection", "--directory", `--title=${copy.title}`];
|
|
852
|
+
if (startPath) {
|
|
853
|
+
args.push(`--filename=${ensureTrailingSeparator(startPath)}`);
|
|
854
|
+
}
|
|
855
|
+
const { stdout } = await execFileAsync("zenity", args);
|
|
856
|
+
return stdout.trim();
|
|
857
|
+
}
|
|
858
|
+
if (platform === "win32") {
|
|
859
|
+
const script = [
|
|
860
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
861
|
+
"$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
862
|
+
`$dialog.Description = '${escapePowerShellString(copy.prompt)}'`,
|
|
863
|
+
...startPath ? [`$dialog.SelectedPath = '${escapePowerShellString(startPath)}'`] : [],
|
|
864
|
+
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }"
|
|
865
|
+
].join("; ");
|
|
866
|
+
const { stdout } = await execFileAsync("powershell", ["-NoProfile", "-Command", script]);
|
|
867
|
+
return stdout.trim();
|
|
868
|
+
}
|
|
869
|
+
throw new Error("Folder picker not supported on this platform");
|
|
870
|
+
}
|
|
432
871
|
async function startOnboardingServer(port, workspacePath) {
|
|
433
872
|
const app = express();
|
|
434
873
|
app.use(express.json());
|
|
@@ -471,20 +910,44 @@ async function startOnboardingServer(port, workspacePath) {
|
|
|
471
910
|
res.status(400).json({ error: String(err) });
|
|
472
911
|
}
|
|
473
912
|
});
|
|
913
|
+
app.post("/api/project-structure-preview", async (req, res) => {
|
|
914
|
+
const {
|
|
915
|
+
profileType,
|
|
916
|
+
name,
|
|
917
|
+
assistantName,
|
|
918
|
+
responsibilities,
|
|
919
|
+
localFolders,
|
|
920
|
+
integrations
|
|
921
|
+
} = req.body;
|
|
922
|
+
try {
|
|
923
|
+
const preview = await buildProjectStructurePreview({
|
|
924
|
+
profileType,
|
|
925
|
+
name,
|
|
926
|
+
assistantName,
|
|
927
|
+
responsibilities: responsibilities ?? [],
|
|
928
|
+
localFolders: localFolders ?? [],
|
|
929
|
+
integrations: integrations ?? []
|
|
930
|
+
});
|
|
931
|
+
res.json(preview);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
res.status(400).json({ error: String(err) });
|
|
934
|
+
}
|
|
935
|
+
});
|
|
474
936
|
app.post("/api/save", async (req, res) => {
|
|
475
937
|
try {
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
938
|
+
const payload = req.body;
|
|
939
|
+
const config = payload;
|
|
940
|
+
const effectivePath = config.storage.mode === "local" && config.storage.workspacePath ? path2.resolve(config.storage.workspacePath) : workspacePath;
|
|
941
|
+
await fs2.mkdir(effectivePath, { recursive: true });
|
|
942
|
+
const rcPath = path2.join(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".personal-assistant-rc.json");
|
|
943
|
+
await fs2.writeFile(rcPath, JSON.stringify({ workspacePath: effectivePath }, null, 2), "utf-8");
|
|
481
944
|
const localStorage = createLocalStorage(effectivePath);
|
|
482
945
|
const localConfigManager = createConfigManager(localStorage);
|
|
483
946
|
await localConfigManager.saveConfig(config);
|
|
484
947
|
let targetStorage = localStorage;
|
|
485
948
|
if (config.storage.mode === "gdrive") {
|
|
486
949
|
const gdriveConfig = config.storage;
|
|
487
|
-
const resolvedCachePath =
|
|
950
|
+
const resolvedCachePath = path2.isAbsolute(gdriveConfig.cachePath) ? gdriveConfig.cachePath : path2.resolve(path2.dirname(effectivePath), gdriveConfig.cachePath);
|
|
488
951
|
const gdriveStorage = createGDriveStorage({
|
|
489
952
|
...gdriveConfig,
|
|
490
953
|
cachePath: resolvedCachePath
|
|
@@ -496,13 +959,25 @@ async function startOnboardingServer(port, workspacePath) {
|
|
|
496
959
|
}
|
|
497
960
|
const contextManager = createContextManager(targetStorage);
|
|
498
961
|
await contextManager.initWorkspace();
|
|
499
|
-
const
|
|
500
|
-
const files = renderTemplate(template, {
|
|
962
|
+
const files = payload.templateFiles && Object.keys(payload.templateFiles).length > 0 ? payload.templateFiles : renderTemplate(getTemplate(config.user.profileType), {
|
|
501
963
|
name: config.user.name,
|
|
502
964
|
assistantName: config.user.assistantName,
|
|
503
965
|
responsibilities: config.user.responsibilities
|
|
504
966
|
});
|
|
505
967
|
await contextManager.writeCanonicalFiles(files);
|
|
968
|
+
if (payload.inferredProjects && payload.inferredProjects.length > 0) {
|
|
969
|
+
await targetStorage.write(
|
|
970
|
+
WORKSPACE_PATHS.context.derived.suggestions.inferredProjects,
|
|
971
|
+
JSON.stringify(
|
|
972
|
+
payload.inferredProjects.map((project, index) => ({
|
|
973
|
+
name: project.slug ?? project.name ?? `project-${index + 1}`,
|
|
974
|
+
score: Number((1 - index * 0.1).toFixed(2))
|
|
975
|
+
})),
|
|
976
|
+
null,
|
|
977
|
+
2
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
}
|
|
506
981
|
const eventClient = createNoopEventClient();
|
|
507
982
|
await eventClient.emit("onboarding_completed", {
|
|
508
983
|
profile: config.user.profileType,
|
|
@@ -613,39 +1088,40 @@ async function startOnboardingServer(port, workspacePath) {
|
|
|
613
1088
|
});
|
|
614
1089
|
}
|
|
615
1090
|
});
|
|
616
|
-
app.
|
|
1091
|
+
app.post("/api/pick-folder", async (req, res) => {
|
|
1092
|
+
const { defaultPath, purpose = "storage" } = req.body;
|
|
1093
|
+
const pickerPurpose = purpose === "local-source" ? "local-source" : "storage";
|
|
1094
|
+
const requestStartedAt = Date.now();
|
|
1095
|
+
const startPath = await resolvePickerStartPath(defaultPath);
|
|
1096
|
+
console.info(
|
|
1097
|
+
`[onboarding] folder picker requested purpose=${pickerPurpose} start=${startPath ?? "system-default"}`
|
|
1098
|
+
);
|
|
617
1099
|
try {
|
|
618
|
-
|
|
619
|
-
const platform = process.platform;
|
|
620
|
-
if (platform === "darwin") {
|
|
621
|
-
const { stdout } = await execAsync(
|
|
622
|
-
`osascript -e 'POSIX path of (choose folder with prompt "Select a folder for your personal assistant context")'`
|
|
623
|
-
);
|
|
624
|
-
folderPath = stdout.trim();
|
|
625
|
-
} else if (platform === "linux") {
|
|
626
|
-
const { stdout } = await execAsync(
|
|
627
|
-
'zenity --file-selection --directory --title="Select storage folder"'
|
|
628
|
-
);
|
|
629
|
-
folderPath = stdout.trim();
|
|
630
|
-
} else if (platform === "win32") {
|
|
631
|
-
const ps = "[System.Reflection.Assembly]::LoadWithPartialName('System.windows.forms') | Out-Null; $f = New-Object System.Windows.Forms.FolderBrowserDialog; $f.Description = 'Select a folder for your personal assistant context'; $f.ShowDialog() | Out-Null; $f.SelectedPath";
|
|
632
|
-
const { stdout } = await execAsync(`powershell -command "${ps}"`);
|
|
633
|
-
folderPath = stdout.trim();
|
|
634
|
-
} else {
|
|
635
|
-
res.status(400).json({ error: "Folder picker not supported on this platform" });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
1100
|
+
const folderPath = await openNativeFolderPicker(process.platform, pickerPurpose, startPath);
|
|
638
1101
|
if (!folderPath) {
|
|
639
|
-
|
|
1102
|
+
console.info(
|
|
1103
|
+
`[onboarding] folder picker returned no path after ${Date.now() - requestStartedAt}ms`
|
|
1104
|
+
);
|
|
1105
|
+
res.json({ cancelled: true, durationMs: Date.now() - requestStartedAt });
|
|
640
1106
|
return;
|
|
641
1107
|
}
|
|
642
|
-
|
|
1108
|
+
console.info(
|
|
1109
|
+
`[onboarding] folder picker resolved in ${Date.now() - requestStartedAt}ms path=${folderPath}`
|
|
1110
|
+
);
|
|
1111
|
+
res.json({ path: folderPath, durationMs: Date.now() - requestStartedAt });
|
|
643
1112
|
} catch (err) {
|
|
644
|
-
if (
|
|
645
|
-
|
|
1113
|
+
if (isPickerCancelledError(err)) {
|
|
1114
|
+
console.info(
|
|
1115
|
+
`[onboarding] folder picker cancelled after ${Date.now() - requestStartedAt}ms`
|
|
1116
|
+
);
|
|
1117
|
+
res.json({ cancelled: true, durationMs: Date.now() - requestStartedAt });
|
|
646
1118
|
return;
|
|
647
1119
|
}
|
|
648
|
-
|
|
1120
|
+
console.error("[onboarding] folder picker failed", err);
|
|
1121
|
+
res.status(500).json({
|
|
1122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1123
|
+
durationMs: Date.now() - requestStartedAt
|
|
1124
|
+
});
|
|
649
1125
|
}
|
|
650
1126
|
});
|
|
651
1127
|
app.post("/api/gdrive/auth-start", async (req, res) => {
|
|
@@ -823,12 +1299,12 @@ async function startOnboardingServer(port, workspacePath) {
|
|
|
823
1299
|
async function resolveOnboardingStaticPath() {
|
|
824
1300
|
const baseDir = import.meta.dirname ?? __dirname;
|
|
825
1301
|
const candidates = [
|
|
826
|
-
|
|
827
|
-
|
|
1302
|
+
path2.resolve(baseDir, "./onboarding-web"),
|
|
1303
|
+
path2.resolve(baseDir, "../../../apps/onboarding-web/dist")
|
|
828
1304
|
];
|
|
829
1305
|
for (const candidate of candidates) {
|
|
830
1306
|
try {
|
|
831
|
-
await
|
|
1307
|
+
await fs2.access(path2.join(candidate, "index.html"));
|
|
832
1308
|
return candidate;
|
|
833
1309
|
} catch {
|
|
834
1310
|
}
|
|
@@ -1035,11 +1511,11 @@ pacman daemon
|
|
|
1035
1511
|
pacman slack listen
|
|
1036
1512
|
|
|
1037
1513
|
# Use in Claude Code
|
|
1038
|
-
/
|
|
1514
|
+
/pacman start <project>
|
|
1039
1515
|
|
|
1040
1516
|
# Use in Codex
|
|
1041
|
-
Ask Codex to load or refresh your
|
|
1042
|
-
<p style="margin-top:1rem;color:#64748b;font-size:0.85rem;">Restart Claude Code or Codex after installing the MCP server.<br>If <code style="background:#1e293b;padding:0 4px;border-radius:3px;">pacman</code> is not found, link it first
|
|
1517
|
+
Ask Codex to load or refresh your Pac-Man context for <project></pre>
|
|
1518
|
+
<p style="margin-top:1rem;color:#64748b;font-size:0.85rem;">Restart Claude Code or Codex after installing the MCP server.<br>If <code style="background:#1e293b;padding:0 4px;border-radius:3px;">pacman</code> is not found, link it first:<br>
|
|
1043
1519
|
<code style="background:#1e293b;padding:2px 6px;border-radius:3px;">pnpm setup && source ~/.zshrc && cd packages/cli && pnpm link --global</code></p>
|
|
1044
1520
|
</div>
|
|
1045
1521
|
</div>
|
|
@@ -1113,10 +1589,17 @@ Ask Codex to load or refresh your Personal Assistant context for <project>
|
|
|
1113
1589
|
|
|
1114
1590
|
async function browseFolder() {
|
|
1115
1591
|
const btn = document.getElementById('browseBtn');
|
|
1116
|
-
btn.textContent = '
|
|
1592
|
+
btn.textContent = 'Opening...';
|
|
1117
1593
|
btn.disabled = true;
|
|
1118
1594
|
try {
|
|
1119
|
-
const res = await fetch('/api/pick-folder'
|
|
1595
|
+
const res = await fetch('/api/pick-folder', {
|
|
1596
|
+
method: 'POST',
|
|
1597
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1598
|
+
body: JSON.stringify({
|
|
1599
|
+
defaultPath: document.getElementById('localPath').value,
|
|
1600
|
+
purpose: 'storage'
|
|
1601
|
+
})
|
|
1602
|
+
});
|
|
1120
1603
|
const data = await res.json();
|
|
1121
1604
|
if (!data.cancelled && data.path) {
|
|
1122
1605
|
document.getElementById('localPath').value = data.path;
|