pi-deck 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,41 @@ The script will: check for a clean working tree on `main`, bump the version in a
95
95
 
96
96
  The published package includes a bundled server (`dist/server.js`) and the pre-built client SPA (`packages/client/dist/`). Workspace dependencies are inlined by esbuild; only runtime dependencies (`express`, `better-sqlite3`, etc.) are installed by npm.
97
97
 
98
+ ## Job Management
99
+
100
+ Pi-deck integrates with Pi's job management system. Jobs are markdown files with YAML frontmatter that track work through phases (backlog → planning → ready → executing → review → complete).
101
+
102
+ ### Configurable Job Locations
103
+
104
+ By default, jobs are stored in two locations:
105
+ - `~/.pi/agent/jobs/<workspace-name>/` (primary)
106
+ - `<workspace>/.pi/jobs/` (local)
107
+
108
+ You can configure custom job directories by creating a `.pi/jobs.json` file in your workspace root:
109
+
110
+ ```json
111
+ {
112
+ "locations": [
113
+ "~/.pi/agent/jobs/pi-deck",
114
+ ".pi/jobs",
115
+ "./custom-jobs-folder",
116
+ "/absolute/path/to/jobs"
117
+ ],
118
+ "defaultLocation": "~/.pi/agent/jobs/pi-deck"
119
+ }
120
+ ```
121
+
122
+ **Configuration options:**
123
+ - `locations` (required): Array of directory paths to scan for jobs. Supports:
124
+ - Relative paths (resolved from workspace root): `"./jobs"`, `".pi/jobs"`
125
+ - Absolute paths: `"/Users/you/projects/jobs"`
126
+ - Home directory expansion: `"~/.pi/agent/jobs"`
127
+ - `defaultLocation` (optional): Where new jobs are created. If not specified, uses the first location in `locations`.
128
+
129
+ The configuration file is optional — without it, the default two-location behavior is preserved for backward compatibility.
130
+
131
+ See `.pi/jobs.json.example` for a template.
132
+
98
133
  ## Configuration
99
134
 
100
135
  Create a config file at `~/.config/pi-deck/config.json`:
package/dist/server.js CHANGED
@@ -24,8 +24,8 @@ __export(plan_service_exports, {
24
24
  writePlan: () => writePlan
25
25
  });
26
26
  import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, readdirSync as readdirSync2 } from "fs";
27
- import { join as join3, basename as basename2, resolve as resolve2 } from "path";
28
- import { homedir as homedir2 } from "os";
27
+ import { join as join3, basename, resolve as resolve2 } from "path";
28
+ import { homedir as homedir3 } from "os";
29
29
  function parseFrontmatter(content) {
30
30
  const frontmatter = {};
31
31
  if (!content.startsWith("---")) {
@@ -88,7 +88,7 @@ function parseTasks(content) {
88
88
  function parsePlan(filePath, content) {
89
89
  const { frontmatter } = parseFrontmatter(content);
90
90
  const tasks = parseTasks(content);
91
- const fileName = basename2(filePath, ".md");
91
+ const fileName = basename(filePath, ".md");
92
92
  let title = frontmatter.title;
93
93
  if (!title) {
94
94
  const h1Match = content.match(/^#\s+(.+)$/m);
@@ -161,9 +161,9 @@ ${fmBlock}
161
161
  ---${content.slice(endIndex + 4)}`;
162
162
  }
163
163
  function getPlanDirectories(workspacePath) {
164
- const workspaceName = basename2(workspacePath);
164
+ const workspaceName = basename(workspacePath);
165
165
  const dirs = [];
166
- const globalDir = join3(homedir2(), "plans", workspaceName);
166
+ const globalDir = join3(homedir3(), "plans", workspaceName);
167
167
  dirs.push(globalDir);
168
168
  const localDir = join3(workspacePath, ".pi", "plans");
169
169
  dirs.push(localDir);
@@ -254,13 +254,16 @@ __export(job_service_exports, {
254
254
  extractReviewSection: () => extractReviewSection,
255
255
  getActiveJobStates: () => getActiveJobStates,
256
256
  getJobDirectories: () => getJobDirectories,
257
+ getJobLocations: () => getJobLocations,
257
258
  getNextPhase: () => getNextPhase,
258
259
  getPreviousPhase: () => getPreviousPhase,
260
+ loadJobConfig: () => loadJobConfig,
259
261
  parseJob: () => parseJob,
260
262
  parseJobFrontmatter: () => parseJobFrontmatter,
261
263
  parseTasks: () => parseTasks,
262
264
  promoteJob: () => promoteJob,
263
265
  readJob: () => readJob,
266
+ resolveLocationPath: () => resolveLocationPath,
264
267
  setJobSessionId: () => setJobSessionId,
265
268
  unarchiveJob: () => unarchiveJob,
266
269
  updateJobFrontmatter: () => updateJobFrontmatter,
@@ -268,9 +271,55 @@ __export(job_service_exports, {
268
271
  writeJob: () => writeJob
269
272
  });
270
273
  import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, mkdirSync, renameSync } from "fs";
271
- import { join as join4, basename as basename3, resolve as resolve3, dirname as dirname2 } from "path";
272
- import { homedir as homedir3 } from "os";
274
+ import { join as join4, basename as basename2, resolve as resolve3, dirname as dirname2 } from "path";
275
+ import { homedir as homedir4 } from "os";
273
276
  import YAML from "yaml";
277
+ function loadJobConfig(workspacePath) {
278
+ const configPath = join4(workspacePath, ".pi", "jobs.json");
279
+ if (!existsSync4(configPath)) {
280
+ return null;
281
+ }
282
+ try {
283
+ const rawContent = readFileSync3(configPath, "utf-8");
284
+ const parsed = JSON.parse(rawContent);
285
+ if (typeof parsed !== "object" || parsed === null) {
286
+ throw new Error("Configuration must be a JSON object");
287
+ }
288
+ if (!Array.isArray(parsed.locations) || parsed.locations.length === 0) {
289
+ throw new Error('Configuration must have a non-empty "locations" array');
290
+ }
291
+ for (const loc of parsed.locations) {
292
+ if (typeof loc !== "string") {
293
+ throw new Error(`Location must be a string, got ${typeof loc}`);
294
+ }
295
+ }
296
+ if (parsed.defaultLocation !== void 0 && typeof parsed.defaultLocation !== "string") {
297
+ throw new Error(`defaultLocation must be a string, got ${typeof parsed.defaultLocation}`);
298
+ }
299
+ return {
300
+ locations: parsed.locations,
301
+ defaultLocation: parsed.defaultLocation
302
+ };
303
+ } catch (err) {
304
+ if (err instanceof SyntaxError) {
305
+ throw new Error(`Failed to parse ${configPath}: Invalid JSON`);
306
+ }
307
+ throw err;
308
+ }
309
+ }
310
+ function resolveLocationPath(location, workspacePath) {
311
+ if (typeof location !== "string") {
312
+ throw new Error(`Location must be a string, got ${typeof location}`);
313
+ }
314
+ let path = location;
315
+ if (path.startsWith("~/") || path === "~") {
316
+ path = join4(homedir4(), path.slice(1));
317
+ }
318
+ if (!path.startsWith("/")) {
319
+ path = resolve3(workspacePath, path);
320
+ }
321
+ return path;
322
+ }
274
323
  function statusToPhase(status) {
275
324
  switch (status) {
276
325
  case "draft":
@@ -366,7 +415,7 @@ function parseJobFrontmatter(content) {
366
415
  function parseJob(filePath, content) {
367
416
  const { frontmatter } = parseJobFrontmatter(content);
368
417
  const tasks = parseTasks(content);
369
- const fileName = basename3(filePath, ".md");
418
+ const fileName = basename2(filePath, ".md");
370
419
  let title = frontmatter.title;
371
420
  if (!title) {
372
421
  const h1Match = content.match(/^#\s+(.+)$/m);
@@ -434,9 +483,23 @@ function updateTaskInContent2(content, lineNumber, done) {
434
483
  return lines.join("\n");
435
484
  }
436
485
  function getJobDirectories(workspacePath) {
437
- const workspaceName = basename3(workspacePath);
486
+ try {
487
+ const config2 = loadJobConfig(workspacePath);
488
+ if (config2) {
489
+ return config2.locations.map((loc) => {
490
+ const resolvedPath = resolveLocationPath(loc, workspacePath);
491
+ if (!existsSync4(resolvedPath)) {
492
+ console.warn(`[JobService] Configured job directory does not exist: ${resolvedPath}`);
493
+ }
494
+ return resolvedPath;
495
+ });
496
+ }
497
+ } catch (err) {
498
+ console.warn(`[JobService] Failed to load job configuration: ${err instanceof Error ? err.message : String(err)}`);
499
+ }
500
+ const workspaceName = basename2(workspacePath);
438
501
  const dirs = [];
439
- dirs.push(join4(homedir3(), ".pi", "agent", "jobs", workspaceName));
502
+ dirs.push(join4(homedir4(), ".pi", "agent", "jobs", workspaceName));
440
503
  dirs.push(join4(workspacePath, ".pi", "jobs"));
441
504
  return dirs;
442
505
  }
@@ -490,11 +553,53 @@ function writeJob(jobPath, content) {
490
553
  writeFileSync2(jobPath, content, "utf-8");
491
554
  return parseJob(jobPath, content);
492
555
  }
493
- function createJob(workspacePath, title, description, tags) {
494
- const workspaceName = basename3(workspacePath);
495
- const jobsDir = join4(homedir3(), ".pi", "agent", "jobs", workspaceName);
496
- if (!existsSync4(jobsDir)) {
497
- mkdirSync(jobsDir, { recursive: true });
556
+ function createJob(workspacePath, title, description, tags, location) {
557
+ let config2 = null;
558
+ let jobsDir;
559
+ try {
560
+ config2 = loadJobConfig(workspacePath);
561
+ if (location) {
562
+ if (config2) {
563
+ const resolvedLocation = resolveLocationPath(location, workspacePath);
564
+ const availableLocations = config2.locations.map((loc) => resolveLocationPath(loc, workspacePath));
565
+ if (availableLocations.includes(resolvedLocation)) {
566
+ jobsDir = resolvedLocation;
567
+ } else {
568
+ throw new Error(`Location "${location}" is not in the configured job locations`);
569
+ }
570
+ } else {
571
+ const defaultLocs = [
572
+ join4(homedir4(), ".pi", "agent", "jobs", basename2(workspacePath)),
573
+ join4(workspacePath, ".pi", "jobs")
574
+ ];
575
+ if (defaultLocs.includes(location)) {
576
+ jobsDir = location;
577
+ } else {
578
+ throw new Error(`Location "${location}" is not available (no custom config)`);
579
+ }
580
+ }
581
+ } else if (config2 && config2.defaultLocation) {
582
+ jobsDir = resolveLocationPath(config2.defaultLocation, workspacePath);
583
+ } else if (config2 && config2.locations.length > 0) {
584
+ jobsDir = resolveLocationPath(config2.locations[0], workspacePath);
585
+ } else {
586
+ const workspaceName = basename2(workspacePath);
587
+ jobsDir = join4(homedir4(), ".pi", "agent", "jobs", workspaceName);
588
+ }
589
+ } catch (err) {
590
+ if (err instanceof Error && err.message.includes("not available")) {
591
+ throw err;
592
+ }
593
+ console.warn(`[JobService] Failed to load job config for createJob: ${err instanceof Error ? err.message : String(err)}`);
594
+ const workspaceName = basename2(workspacePath);
595
+ jobsDir = join4(homedir4(), ".pi", "agent", "jobs", workspaceName);
596
+ }
597
+ try {
598
+ if (!existsSync4(jobsDir)) {
599
+ mkdirSync(jobsDir, { recursive: true });
600
+ }
601
+ } catch (err) {
602
+ throw new Error(`Failed to create job directory at ${jobsDir}: ${err instanceof Error ? err.message : String(err)}`);
498
603
  }
499
604
  const now = /* @__PURE__ */ new Date();
500
605
  const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
@@ -579,7 +684,7 @@ function getArchivedDir(jobDir) {
579
684
  }
580
685
  function archiveJob(jobPath) {
581
686
  const dir = dirname2(jobPath);
582
- const file = basename3(jobPath);
687
+ const file = basename2(jobPath);
583
688
  const archivedDir = getArchivedDir(dir);
584
689
  if (!existsSync4(archivedDir)) {
585
690
  mkdirSync(archivedDir, { recursive: true });
@@ -591,7 +696,7 @@ function archiveJob(jobPath) {
591
696
  function unarchiveJob(jobPath) {
592
697
  const archivedDir = dirname2(jobPath);
593
698
  const parentDir = dirname2(archivedDir);
594
- const file = basename3(jobPath);
699
+ const file = basename2(jobPath);
595
700
  const newPath = join4(parentDir, file);
596
701
  renameSync(jobPath, newPath);
597
702
  return newPath;
@@ -623,14 +728,22 @@ function discoverArchivedJobs(workspacePath) {
623
728
  }
624
729
  function buildJobSystemContext(workspacePath) {
625
730
  const dirs = getJobDirectories(workspacePath);
731
+ const config2 = loadJobConfig(workspacePath);
626
732
  const primaryDir = dirs[0];
627
733
  const jobs = discoverJobs(workspacePath);
628
734
  const jobLines = jobs.map((j) => ` - [${j.phase}] "${j.title}" \u2192 ${j.path}`).join("\n");
629
735
  const jobListing = jobs.length > 0 ? `
630
736
  Current jobs:
631
737
  ${jobLines}` : "\nNo jobs exist yet.";
738
+ const dirListing = dirs.map((d) => ` - ${d}`).join("\n");
632
739
  return `<job_system>
633
- You have access to a job management system. Jobs are markdown files with YAML frontmatter stored in: ${primaryDir}
740
+ You have access to a job management system. Jobs are markdown files with YAML frontmatter.
741
+
742
+ ## Job Locations
743
+ Jobs are stored in the following directories:
744
+ ${dirListing}
745
+
746
+ ${config2 ? `Configuration loaded from: .pi/jobs.json` : "Using default job locations (no .pi/jobs.json config)"}
634
747
 
635
748
  ## Job File Format
636
749
  \`\`\`markdown
@@ -660,7 +773,7 @@ What needs to be done.
660
773
 
661
774
  ## Managing Jobs
662
775
  - **Create a job**: Write a new .md file to ${primaryDir} with the frontmatter format above. Use filename format: YYYYMMDD-slug.md
663
- - **List jobs**: Read files from ${primaryDir}
776
+ - **List jobs**: Read files from any of the job directories
664
777
  - **Update phase**: Edit the \`phase\` field in frontmatter. Update \`updated\` timestamp.
665
778
  - **Check off tasks**: Change \`- [ ]\` to \`- [x]\` in the job file.
666
779
  - **Complete a job**: Set phase to "complete" and add \`completedAt\` to frontmatter.
@@ -675,7 +788,19 @@ function buildPlanningPrompt(jobPath) {
675
788
  return `<active_job phase="planning">
676
789
  You have a job to plan at: ${jobPath}
677
790
  Read the job file. It contains a title and description.
791
+
792
+ Before creating the plan, you MUST:
793
+ 1. Explore the codebase to understand the current implementation
794
+ 2. Search for relevant files, functions, and existing patterns
795
+ 3. Read documentation and configuration files as needed
796
+ 4. Gather context about the architecture and conventions used
797
+
798
+ Do this research yourself \u2014 DO NOT include research or exploration tasks in the plan. The plan should only contain concrete implementation steps that will be performed after planning is complete.
799
+
678
800
  Your goal is to create a detailed implementation plan. Ask the user clarifying questions if needed, then write a concrete plan with \`- [ ]\` checkbox tasks back into the job file under a "## Plan" section.
801
+
802
+ Plan tasks should be actionable implementation steps only (e.g., "Add function X", "Update file Y"). Do not include research tasks like "review current implementation" \u2014 you should do that during planning, not put it in the plan.
803
+
679
804
  Group tasks under \`### Phase\` headings. Keep tasks concise and actionable (start with a verb).
680
805
  When you're done writing the plan, let the user know so they can review and iterate or mark it as ready.
681
806
  </active_job>`;
@@ -699,6 +824,24 @@ function extractReviewSection(content) {
699
824
  const trimmed = sectionText.trim();
700
825
  return trimmed.length > 0 ? trimmed : null;
701
826
  }
827
+ function getJobLocations(workspacePath) {
828
+ const config2 = loadJobConfig(workspacePath);
829
+ const dirs = getJobDirectories(workspacePath);
830
+ let defaultDir;
831
+ if (config2 && config2.defaultLocation) {
832
+ defaultDir = resolveLocationPath(config2.defaultLocation, workspacePath);
833
+ } else if (config2 && config2.locations.length > 0) {
834
+ defaultDir = resolveLocationPath(config2.locations[0], workspacePath);
835
+ } else {
836
+ defaultDir = dirs[0];
837
+ }
838
+ return dirs.map((dir) => ({
839
+ path: dir,
840
+ isDefault: dir === defaultDir,
841
+ displayName: dir.startsWith(workspacePath) ? `.${dir.slice(workspacePath.length)}` : dir.replace(homedir4(), "~")
842
+ // Replace home directory with ~
843
+ }));
844
+ }
702
845
  function buildReviewPrompt(jobPath) {
703
846
  const content = readFileSync3(jobPath, "utf-8");
704
847
  const reviewSection = extractReviewSection(content);
@@ -764,10 +907,10 @@ import { createServer } from "http";
764
907
  import { WebSocketServer, WebSocket } from "ws";
765
908
  import { fileURLToPath } from "url";
766
909
  import { dirname as dirname5, join as join7, resolve as resolve4, sep } from "path";
767
- import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
910
+ import { existsSync as existsSync9, readFileSync as readFileSync4, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
768
911
  import { unlink } from "fs/promises";
769
912
  import { spawn } from "child_process";
770
- import { homedir as homedir5 } from "os";
913
+ import { homedir as homedir6 } from "os";
771
914
  import { SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
772
915
 
773
916
  // packages/server/dist/config.js
@@ -801,9 +944,7 @@ function findProjectRoot(startDir) {
801
944
  var projectRoot = findProjectRoot(process.cwd());
802
945
  var DEFAULT_CONFIG = {
803
946
  port: 9741,
804
- host: "0.0.0.0",
805
- // Default to detected project root, then home as fallback
806
- allowedDirectories: [projectRoot, homedir()]
947
+ host: "0.0.0.0"
807
948
  };
808
949
  var CONFIG_PATHS = [
809
950
  resolve(process.cwd(), "pi-deck.config.json"),
@@ -833,9 +974,6 @@ function loadConfigFromEnv() {
833
974
  if (process.env.HOST) {
834
975
  config2.host = process.env.HOST;
835
976
  }
836
- if (process.env.PI_ALLOWED_DIRS) {
837
- config2.allowedDirectories = process.env.PI_ALLOWED_DIRS.split(":").map((d) => resolve(d.replace(/^~/, homedir())));
838
- }
839
977
  return config2;
840
978
  }
841
979
  var TILDE_PREFIX = /^~(?=\/|$)/;
@@ -847,57 +985,36 @@ function canonicalizePath(input) {
847
985
  return normalized;
848
986
  }
849
987
  }
850
- function normalizeDirectories(dirs) {
851
- return dirs.map((d) => canonicalizePath(d));
852
- }
853
988
  function loadConfig() {
854
989
  const fileConfig = loadConfigFromFile();
855
990
  const envConfig = loadConfigFromEnv();
856
991
  const config2 = {
857
992
  port: envConfig.port ?? fileConfig.port ?? DEFAULT_CONFIG.port,
858
- host: envConfig.host ?? fileConfig.host ?? DEFAULT_CONFIG.host,
859
- allowedDirectories: normalizeDirectories(envConfig.allowedDirectories ?? fileConfig.allowedDirectories ?? DEFAULT_CONFIG.allowedDirectories)
993
+ host: envConfig.host ?? fileConfig.host ?? DEFAULT_CONFIG.host
860
994
  };
861
- console.log("[Config] Allowed directories:", config2.allowedDirectories);
862
995
  return config2;
863
996
  }
864
- function isPathAllowed(path, allowedDirectories) {
865
- const normalizedPath = canonicalizePath(path);
866
- return allowedDirectories.some((allowed) => {
867
- const normalizedAllowed = canonicalizePath(allowed);
868
- if (normalizedAllowed === "/") {
869
- return true;
870
- }
871
- return normalizedPath === normalizedAllowed || normalizedPath.startsWith(normalizedAllowed + "/");
872
- });
873
- }
874
997
 
875
998
  // packages/server/dist/directory-browser.js
876
999
  import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
877
- import { basename, join as join2 } from "path";
1000
+ import { join as join2 } from "path";
1001
+ import { homedir as homedir2 } from "os";
878
1002
  var DirectoryBrowser = class {
879
- allowedDirectories;
880
- constructor(allowedDirectories) {
881
- this.allowedDirectories = allowedDirectories;
882
- }
883
1003
  /**
884
1004
  * List the allowed root directories
885
1005
  */
886
1006
  listRoots() {
887
- return this.allowedDirectories.map((dir) => ({
888
- name: basename(dir) || dir,
889
- path: dir,
890
- hasPiSessions: this.checkForPiSessions(dir)
891
- }));
1007
+ return [{
1008
+ name: "Home",
1009
+ path: homedir2(),
1010
+ hasPiSessions: this.checkForPiSessions(homedir2())
1011
+ }];
892
1012
  }
893
1013
  /**
894
1014
  * Browse a directory and return its subdirectories
895
1015
  */
896
1016
  browse(path) {
897
1017
  const normalizedPath = canonicalizePath(path);
898
- if (!isPathAllowed(normalizedPath, this.allowedDirectories)) {
899
- throw new Error(`Access denied: ${path} is not within allowed directories`);
900
- }
901
1018
  if (!existsSync2(normalizedPath)) {
902
1019
  throw new Error(`Directory not found: ${path}`);
903
1020
  }
@@ -942,13 +1059,13 @@ var DirectoryBrowser = class {
942
1059
  * Get the allowed directories
943
1060
  */
944
1061
  getAllowedDirectories() {
945
- return [...this.allowedDirectories];
1062
+ return [homedir2()];
946
1063
  }
947
1064
  };
948
1065
 
949
1066
  // packages/server/dist/workspace-manager.js
950
1067
  import { EventEmitter as EventEmitter3 } from "events";
951
- import { basename as basename4 } from "path";
1068
+ import { basename as basename3 } from "path";
952
1069
 
953
1070
  // packages/server/dist/session-orchestrator.js
954
1071
  import { EventEmitter as EventEmitter2 } from "events";
@@ -1927,7 +2044,9 @@ var PiSession = class extends EventEmitter {
1927
2044
  cacheRead: msg.usage.cacheRead || 0,
1928
2045
  cacheWrite: msg.usage.cacheWrite || 0,
1929
2046
  total: (msg.usage.input || 0) + (msg.usage.output || 0)
1930
- } : void 0
2047
+ } : void 0,
2048
+ stopReason: msg.stopReason,
2049
+ errorMessage: msg.errorMessage
1931
2050
  };
1932
2051
  }
1933
2052
  convertContent(content) {
@@ -2975,14 +3094,12 @@ var SessionOrchestrator = class extends EventEmitter2 {
2975
3094
 
2976
3095
  // packages/server/dist/workspace-manager.js
2977
3096
  var WorkspaceManager = class extends EventEmitter3 {
2978
- allowedDirectories;
2979
3097
  workspaces = /* @__PURE__ */ new Map();
2980
3098
  nextWorkspaceId = 1;
2981
3099
  syncIntegration = null;
2982
3100
  openingPaths = /* @__PURE__ */ new Set();
2983
- constructor(allowedDirectories) {
3101
+ constructor() {
2984
3102
  super();
2985
- this.allowedDirectories = allowedDirectories;
2986
3103
  }
2987
3104
  /**
2988
3105
  * Set the sync integration for state tracking
@@ -2997,9 +3114,6 @@ var WorkspaceManager = class extends EventEmitter3 {
2997
3114
  */
2998
3115
  async openWorkspace(path) {
2999
3116
  const normalizedPath = canonicalizePath(path);
3000
- if (!isPathAllowed(normalizedPath, this.allowedDirectories)) {
3001
- throw new Error(`Access denied: ${normalizedPath} is not within allowed directories`);
3002
- }
3003
3117
  while (this.openingPaths.has(normalizedPath)) {
3004
3118
  await new Promise((resolve5) => setTimeout(resolve5, 10));
3005
3119
  }
@@ -3044,7 +3158,7 @@ var WorkspaceManager = class extends EventEmitter3 {
3044
3158
  const workspace = {
3045
3159
  id,
3046
3160
  path: normalizedPath,
3047
- name: basename4(normalizedPath) || normalizedPath,
3161
+ name: basename3(normalizedPath) || normalizedPath,
3048
3162
  orchestrator,
3049
3163
  unsubscribe,
3050
3164
  clientCount: 1,
@@ -3258,9 +3372,9 @@ var WorkspaceManager = class extends EventEmitter3 {
3258
3372
  }
3259
3373
  };
3260
3374
  var workspaceManager = null;
3261
- function getWorkspaceManager(allowedDirectories) {
3375
+ function getWorkspaceManager() {
3262
3376
  if (!workspaceManager) {
3263
- workspaceManager = new WorkspaceManager(allowedDirectories);
3377
+ workspaceManager = new WorkspaceManager();
3264
3378
  }
3265
3379
  return workspaceManager;
3266
3380
  }
@@ -3268,7 +3382,7 @@ function getWorkspaceManager(allowedDirectories) {
3268
3382
  // packages/server/dist/ui-state.js
3269
3383
  import Database from "better-sqlite3";
3270
3384
  import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
3271
- import { homedir as homedir4 } from "os";
3385
+ import { homedir as homedir5 } from "os";
3272
3386
  import { dirname as dirname3, join as join5 } from "path";
3273
3387
  var DEFAULT_STATE = {
3274
3388
  openWorkspaces: [],
@@ -3287,7 +3401,7 @@ var DEFAULT_STATE = {
3287
3401
  var UIStateStore = class {
3288
3402
  db;
3289
3403
  constructor(dbPath) {
3290
- const path = dbPath || join5(homedir4(), ".config", "pi-deck", "ui-state.db");
3404
+ const path = dbPath || join5(homedir5(), ".config", "pi-deck", "ui-state.db");
3291
3405
  const dir = dirname3(path);
3292
3406
  if (!existsSync6(dir)) {
3293
3407
  mkdirSync2(dir, { recursive: true });
@@ -5179,7 +5293,7 @@ var SyncIntegration = class extends EventEmitter8 {
5179
5293
 
5180
5294
  // packages/server/dist/index.js
5181
5295
  var config = loadConfig();
5182
- var syncDbPath = join7(homedir5(), ".pi", "pi-deck-sync.db");
5296
+ var syncDbPath = join7(homedir6(), ".pi", "pi-deck-sync.db");
5183
5297
  var syncIntegration = new SyncIntegration(syncDbPath);
5184
5298
  console.log(`[Sync] Initialized sync database at ${syncDbPath}`);
5185
5299
  var PORT = config.port;
@@ -5219,9 +5333,9 @@ if (existsSync9(clientDistPath)) {
5219
5333
  }
5220
5334
  var server = createServer(app);
5221
5335
  var wss = new WebSocketServer({ server, path: "/ws" });
5222
- var directoryBrowser = new DirectoryBrowser(config.allowedDirectories);
5336
+ var directoryBrowser = new DirectoryBrowser();
5223
5337
  var uiStateStore = getUIStateStore();
5224
- var workspaceManager2 = getWorkspaceManager(config.allowedDirectories);
5338
+ var workspaceManager2 = getWorkspaceManager();
5225
5339
  workspaceManager2.setSyncIntegration(syncIntegration);
5226
5340
  var clientWorkspaces = /* @__PURE__ */ new Map();
5227
5341
  var pendingQuestionnaireRoutes = /* @__PURE__ */ new Map();
@@ -5360,7 +5474,6 @@ Please read the job file and execute the review steps.`;
5360
5474
  app.get("/health", (_req, res) => {
5361
5475
  res.json({
5362
5476
  status: "ok",
5363
- allowedDirectories: config.allowedDirectories,
5364
5477
  activeWorkspaces: workspaceManager2.listWorkspaces().length
5365
5478
  });
5366
5479
  });
@@ -5372,8 +5485,7 @@ wss.on("connection", async (ws) => {
5372
5485
  send(ws, {
5373
5486
  type: "connected",
5374
5487
  workspaces: existingWorkspaces,
5375
- allowedRoots: config.allowedDirectories,
5376
- homeDirectory: homedir5(),
5488
+ homeDirectory: homedir6(),
5377
5489
  uiState,
5378
5490
  ...updateAvailable ? { updateAvailable } : {}
5379
5491
  });
@@ -5520,8 +5632,7 @@ async function handleMessage(ws, message) {
5520
5632
  send(ws, {
5521
5633
  type: "directoryList",
5522
5634
  path: "/",
5523
- entries: directoryBrowser.listRoots(),
5524
- allowedRoots: directoryBrowser.getAllowedDirectories()
5635
+ entries: directoryBrowser.listRoots()
5525
5636
  });
5526
5637
  }
5527
5638
  break;
@@ -6183,26 +6294,6 @@ ${planNote}` : planNote;
6183
6294
  });
6184
6295
  break;
6185
6296
  }
6186
- case "updateAllowedRoots": {
6187
- const { roots } = message;
6188
- console.log("[Config] Updating allowed roots:", roots);
6189
- const configPath = join7(homedir5(), ".pi-deck.json");
6190
- let fileConfig = {};
6191
- try {
6192
- if (existsSync9(configPath)) {
6193
- fileConfig = JSON.parse(readFileSync4(configPath, "utf-8"));
6194
- }
6195
- } catch {
6196
- }
6197
- fileConfig.allowedDirectories = roots;
6198
- writeFileSync3(configPath, JSON.stringify(fileConfig, null, 2));
6199
- console.log("[Config] Saved config to", configPath);
6200
- send(ws, {
6201
- type: "allowedRootsUpdated",
6202
- roots
6203
- });
6204
- break;
6205
- }
6206
6297
  // ========================================================================
6207
6298
  // Session Tree Navigation
6208
6299
  // ========================================================================
@@ -6433,26 +6524,13 @@ ${planNote}` : planNote;
6433
6524
  }
6434
6525
  const rootPath = resolve4(workspace.path);
6435
6526
  const rawPath = message.path || "";
6436
- const expandedPath = rawPath.startsWith("~/") ? join7(homedir5(), rawPath.slice(2)) : rawPath;
6527
+ const expandedPath = rawPath.startsWith("~/") ? join7(homedir6(), rawPath.slice(2)) : rawPath;
6437
6528
  const isAbsolute = expandedPath.startsWith("/");
6438
6529
  let targetPath;
6439
6530
  let displayPath;
6440
6531
  if (isAbsolute) {
6441
6532
  targetPath = resolve4(expandedPath);
6442
6533
  displayPath = rawPath;
6443
- const inWorkspace = targetPath.startsWith(rootPath + sep) || targetPath === rootPath;
6444
- const inAllowed = config.allowedDirectories.some((dir) => targetPath.startsWith(resolve4(dir) + sep) || targetPath === resolve4(dir));
6445
- if (!inWorkspace && !inAllowed) {
6446
- send(ws, {
6447
- type: "workspaceFile",
6448
- workspaceId: message.workspaceId,
6449
- path: displayPath,
6450
- content: "",
6451
- truncated: false,
6452
- requestId: message.requestId
6453
- });
6454
- break;
6455
- }
6456
6534
  } else {
6457
6535
  const relativePath = rawPath.replace(/^\/+/, "");
6458
6536
  targetPath = resolve4(rootPath, relativePath);
@@ -6930,6 +7008,28 @@ Please read the plan file and begin working through the tasks.`;
6930
7008
  syncIntegration.setJobs(message.workspaceId, jobs);
6931
7009
  break;
6932
7010
  }
7011
+ case "getJobLocations": {
7012
+ const workspace = workspaceManager2.getWorkspace(message.workspaceId);
7013
+ if (!workspace)
7014
+ break;
7015
+ try {
7016
+ const locations = getJobLocations(workspace.path);
7017
+ const defaultLocation = locations.find((l) => l.isDefault)?.path || locations[0]?.path;
7018
+ send(ws, {
7019
+ type: "jobLocations",
7020
+ workspaceId: message.workspaceId,
7021
+ locations,
7022
+ defaultLocation
7023
+ });
7024
+ } catch (err) {
7025
+ send(ws, {
7026
+ type: "error",
7027
+ message: `Failed to get job locations: ${err instanceof Error ? err.message : "Unknown error"}`,
7028
+ workspaceId: message.workspaceId
7029
+ });
7030
+ }
7031
+ break;
7032
+ }
6933
7033
  case "getJobContent": {
6934
7034
  const workspace = workspaceManager2.getWorkspace(message.workspaceId);
6935
7035
  if (!workspace)
@@ -6957,7 +7057,7 @@ Please read the plan file and begin working through the tasks.`;
6957
7057
  if (!workspace)
6958
7058
  break;
6959
7059
  try {
6960
- const { path: jobPath, job } = createJob(workspace.path, message.title, message.description, message.tags);
7060
+ const { path: jobPath, job } = createJob(workspace.path, message.title, message.description, message.tags, message.location);
6961
7061
  broadcastToWorkspace(message.workspaceId, {
6962
7062
  type: "jobSaved",
6963
7063
  workspaceId: message.workspaceId,
@@ -7319,7 +7419,6 @@ if (existsSync9(clientDistPath)) {
7319
7419
  }
7320
7420
  server.listen(PORT, config.host, () => {
7321
7421
  console.log(`[Server] Pi-Deck server running on http://${config.host}:${PORT}`);
7322
- console.log(`[Server] Allowed directories: ${config.allowedDirectories.join(", ")}`);
7323
7422
  console.log(`[Server] WebSocket endpoint: ws://${config.host}:${PORT}/ws`);
7324
7423
  });
7325
7424
  //# sourceMappingURL=server.js.map