@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.
@@ -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 path from "path";
24
- import * as fs from "fs/promises";
25
- import { exec } from "child_process";
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 execAsync = promisify(exec);
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 config = req.body;
477
- const effectivePath = config.storage.mode === "local" && config.storage.workspacePath ? path.resolve(config.storage.workspacePath) : workspacePath;
478
- await fs.mkdir(effectivePath, { recursive: true });
479
- const rcPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".personal-assistant-rc.json");
480
- await fs.writeFile(rcPath, JSON.stringify({ workspacePath: effectivePath }, null, 2), "utf-8");
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 = path.isAbsolute(gdriveConfig.cachePath) ? gdriveConfig.cachePath : path.resolve(path.dirname(effectivePath), gdriveConfig.cachePath);
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 template = getTemplate(config.user.profileType);
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.get("/api/pick-folder", async (_req, res) => {
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
- let folderPath;
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
- res.json({ cancelled: true });
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
- res.json({ path: folderPath });
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 (String(err).includes("User canceled") || String(err).includes("cancel")) {
645
- res.json({ cancelled: true });
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
- res.status(500).json({ error: String(err) });
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
- path.resolve(baseDir, "./onboarding-web"),
827
- path.resolve(baseDir, "../../../apps/onboarding-web/dist")
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 fs.access(path.join(candidate, "index.html"));
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
- /personal-assistant start &lt;project&gt;
1514
+ /pacman start &lt;project&gt;
1039
1515
 
1040
1516
  # Use in Codex
1041
- Ask Codex to load or refresh your Personal Assistant context for &lt;project&gt;</pre>
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. The legacy alias <code style="background:#1e293b;padding:0 4px;border-radius:3px;">personal-assistant</code> also remains available:<br>
1517
+ Ask Codex to load or refresh your Pac-Man context for &lt;project&gt;</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 &amp;&amp; source ~/.zshrc &amp;&amp; cd packages/cli &amp;&amp; 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 &lt;project&gt;
1113
1589
 
1114
1590
  async function browseFolder() {
1115
1591
  const btn = document.getElementById('browseBtn');
1116
- btn.textContent = '\u2026';
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;