pi-deck 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/README.md +35 -0
- package/dist/server.js +176 -9
- package/dist/server.js.map +3 -3
- package/package.json +1 -1
- package/packages/client/dist/assets/{MarkdownContent-ChSvmdYI.js → MarkdownContent-wbm_GFDE.js} +1 -1
- package/packages/client/dist/assets/{index-Uft_WPvr.css → index-BZ24LH6A.css} +1 -1
- package/packages/client/dist/assets/{index-mBm5teBW.js → index-sHanNU2G.js} +120 -120
- package/packages/client/dist/index.html +2 -2
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
|
@@ -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,
|
|
@@ -271,6 +274,52 @@ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync
|
|
|
271
274
|
import { join as join4, basename as basename2, resolve as resolve3, dirname as dirname2 } from "path";
|
|
272
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":
|
|
@@ -434,6 +483,20 @@ function updateTaskInContent2(content, lineNumber, done) {
|
|
|
434
483
|
return lines.join("\n");
|
|
435
484
|
}
|
|
436
485
|
function getJobDirectories(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
|
+
}
|
|
437
500
|
const workspaceName = basename2(workspacePath);
|
|
438
501
|
const dirs = [];
|
|
439
502
|
dirs.push(join4(homedir4(), ".pi", "agent", "jobs", workspaceName));
|
|
@@ -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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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, "");
|
|
@@ -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
|
|
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
|
|
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);
|
|
@@ -1901,7 +2044,9 @@ var PiSession = class extends EventEmitter {
|
|
|
1901
2044
|
cacheRead: msg.usage.cacheRead || 0,
|
|
1902
2045
|
cacheWrite: msg.usage.cacheWrite || 0,
|
|
1903
2046
|
total: (msg.usage.input || 0) + (msg.usage.output || 0)
|
|
1904
|
-
} : void 0
|
|
2047
|
+
} : void 0,
|
|
2048
|
+
stopReason: msg.stopReason,
|
|
2049
|
+
errorMessage: msg.errorMessage
|
|
1905
2050
|
};
|
|
1906
2051
|
}
|
|
1907
2052
|
convertContent(content) {
|
|
@@ -6863,6 +7008,28 @@ Please read the plan file and begin working through the tasks.`;
|
|
|
6863
7008
|
syncIntegration.setJobs(message.workspaceId, jobs);
|
|
6864
7009
|
break;
|
|
6865
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
|
+
}
|
|
6866
7033
|
case "getJobContent": {
|
|
6867
7034
|
const workspace = workspaceManager2.getWorkspace(message.workspaceId);
|
|
6868
7035
|
if (!workspace)
|
|
@@ -6890,7 +7057,7 @@ Please read the plan file and begin working through the tasks.`;
|
|
|
6890
7057
|
if (!workspace)
|
|
6891
7058
|
break;
|
|
6892
7059
|
try {
|
|
6893
|
-
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);
|
|
6894
7061
|
broadcastToWorkspace(message.workspaceId, {
|
|
6895
7062
|
type: "jobSaved",
|
|
6896
7063
|
workspaceId: message.workspaceId,
|