portable-agent-layer 0.41.1 → 0.43.0
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/.husky/install.mjs +8 -0
- package/README.md +2 -1
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/consulting-report/template/components/code-block.tsx +21 -0
- package/assets/skills/consulting-report/template/components/cover-page.tsx +88 -27
- package/assets/skills/consulting-report/template/components/decision-table.tsx +62 -0
- package/assets/skills/consulting-report/template/components/process-stage.tsx +28 -0
- package/assets/skills/consulting-report/template/components/tier-matrix.tsx +102 -0
- package/assets/skills/consulting-report/template/lib/types.ts +49 -1
- package/assets/skills/consulting-report/tools/generate-pdf.ts +99 -16
- package/assets/skills/entities/SKILL.md +95 -0
- package/assets/skills/telos/SKILL.md +1 -1
- package/assets/templates/PAL/ALGORITHM.md +1 -1
- package/assets/templates/PAL/README.md +2 -2
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/rules.codex.rules +64 -0
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +11 -12
- package/src/cli/index.ts +8 -0
- package/src/cli/knowledge.ts +620 -0
- package/src/cli/migrate.ts +188 -3
- package/src/hooks/lib/claude-md.ts +6 -2
- package/src/hooks/lib/export.ts +1 -1
- package/src/hooks/lib/paths.ts +3 -1
- package/src/targets/codex/install.ts +14 -0
- package/src/targets/codex/uninstall.ts +14 -0
- package/src/targets/lib.ts +53 -36
- package/src/tools/knowledge/graph.ts +395 -0
- package/src/tools/knowledge/ingest.ts +409 -0
- package/src/tools/knowledge/lib.ts +493 -0
- package/assets/skills/extract-entities/SKILL.md +0 -62
- package/assets/skills/extract-entities/tools/entity-save.ts +0 -110
- package/src/hooks/lib/entities.ts +0 -304
- package/src/tools/export.ts +0 -40
- package/src/tools/import.ts +0 -111
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ pal cli status # check your setup
|
|
|
84
84
|
| `pal cli doctor` | Check prerequisites and system health |
|
|
85
85
|
| `pal cli migrate` | Run pending data migrations (non-destructive) |
|
|
86
86
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
87
|
+
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
87
88
|
|
|
88
89
|
### Target flags
|
|
89
90
|
|
|
@@ -159,7 +160,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
|
|
|
159
160
|
| `council` | Multi-perspective parallel debate on decisions |
|
|
160
161
|
| `create-pdf` | Render structured content into a PDF |
|
|
161
162
|
| `create-skill` | Scaffold a new skill from a description |
|
|
162
|
-
| `
|
|
163
|
+
| `entities` | Detect, save, and query people & companies in the personal knowledge graph |
|
|
163
164
|
| `extract-wisdom` | Extract structured insights from content |
|
|
164
165
|
| `first-principles` | Break down problems to fundamentals |
|
|
165
166
|
| `fyzz-chat-api` | Query Fyzz Chat conversations via API |
|
|
@@ -23,7 +23,7 @@ Follow the user's request. Common tasks:
|
|
|
23
23
|
|
|
24
24
|
- **Summarize** the video content
|
|
25
25
|
- **Answer a specific question** about what's shown or discussed
|
|
26
|
-
- **Extract entities** (defer to /
|
|
26
|
+
- **Extract entities** (defer to /entities if installed)
|
|
27
27
|
- **Extract wisdom** (defer to /extract-wisdom if installed)
|
|
28
28
|
- **Compare** with other content
|
|
29
29
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CodeSample } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface CodeBlockProps {
|
|
4
|
+
sample: CodeSample;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function CodeBlock({ sample }: CodeBlockProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="my-4 break-inside-avoid">
|
|
10
|
+
<div className="font-sans text-[0.65rem] font-bold uppercase tracking-widest text-primary mb-1">
|
|
11
|
+
{sample.language}
|
|
12
|
+
</div>
|
|
13
|
+
<pre className="rounded-lg overflow-x-auto text-[0.8rem] leading-relaxed px-5 py-4 bg-[#f6f8fa]">
|
|
14
|
+
<code>{sample.code}</code>
|
|
15
|
+
</pre>
|
|
16
|
+
{sample.caption && (
|
|
17
|
+
<p className="text-[0.78rem] text-muted italic mt-1">{sample.caption}</p>
|
|
18
|
+
)}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -1,46 +1,107 @@
|
|
|
1
|
+
interface MetadataItem {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
interface CoverPageProps {
|
|
2
|
-
clientName: string;
|
|
3
7
|
reportTitle: string;
|
|
4
8
|
reportDate: string;
|
|
5
|
-
|
|
9
|
+
clientName: string;
|
|
6
10
|
consultancyName: string;
|
|
7
11
|
preTitle?: string;
|
|
12
|
+
/** Path to consultancy logo image. Falls back to consultancyName text wordmark. */
|
|
13
|
+
consultancyLogoSrc?: string;
|
|
14
|
+
/** Path to client logo image. Falls back to clientName text wordmark. */
|
|
15
|
+
clientLogoSrc?: string;
|
|
16
|
+
/** Optional metadata rows rendered in the bottom strip (e.g. classification, version). */
|
|
17
|
+
metadata?: MetadataItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function LogoSlot({
|
|
21
|
+
src,
|
|
22
|
+
name,
|
|
23
|
+
align = "left",
|
|
24
|
+
}: {
|
|
25
|
+
src?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
align?: "left" | "right";
|
|
28
|
+
}) {
|
|
29
|
+
if (src) {
|
|
30
|
+
return (
|
|
31
|
+
// biome-ignore lint/performance/noImgElement: Template reports are static exports; next/image is not needed for print logos.
|
|
32
|
+
<img
|
|
33
|
+
src={src}
|
|
34
|
+
alt={name}
|
|
35
|
+
className="h-10 w-auto object-contain"
|
|
36
|
+
style={{ maxWidth: 180 }}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
className={`font-sans text-sm font-bold uppercase tracking-widest text-foreground ${
|
|
43
|
+
align === "right" ? "text-right" : ""
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{name}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
8
49
|
}
|
|
9
50
|
|
|
10
51
|
export function CoverPage({
|
|
11
|
-
clientName,
|
|
12
52
|
reportTitle,
|
|
13
|
-
|
|
14
|
-
classification,
|
|
53
|
+
clientName,
|
|
15
54
|
consultancyName,
|
|
16
|
-
preTitle
|
|
55
|
+
preTitle,
|
|
56
|
+
consultancyLogoSrc,
|
|
57
|
+
clientLogoSrc,
|
|
58
|
+
metadata,
|
|
17
59
|
}: CoverPageProps) {
|
|
18
60
|
return (
|
|
19
|
-
<div
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
].join(" ")}
|
|
25
|
-
>
|
|
26
|
-
<div className="font-sans text-sm font-semibold text-destructive uppercase tracking-[0.15em] mb-16">
|
|
27
|
-
{classification}
|
|
61
|
+
<div className="min-h-screen flex flex-col break-after-page bg-background">
|
|
62
|
+
{/* Logo strip */}
|
|
63
|
+
<div className="flex items-center justify-between px-16 py-8 border-b-2 border-primary">
|
|
64
|
+
<LogoSlot src={consultancyLogoSrc} name={consultancyName} align="left" />
|
|
65
|
+
<LogoSlot src={clientLogoSrc} name={clientName} align="right" />
|
|
28
66
|
</div>
|
|
29
67
|
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
{/* Main content — vertically centered */}
|
|
69
|
+
<div className="flex-1 flex flex-col justify-center px-16 py-20">
|
|
70
|
+
{preTitle && (
|
|
71
|
+
<p className="font-sans text-xs font-bold uppercase tracking-widest text-primary mb-5">
|
|
72
|
+
{preTitle}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
<h1 className="font-heading text-5xl font-semibold text-foreground leading-tight mb-6 tracking-tight max-w-2xl">
|
|
76
|
+
{reportTitle}
|
|
77
|
+
</h1>
|
|
78
|
+
<p className="font-heading text-xl text-muted font-normal">
|
|
79
|
+
Prepared for {clientName}
|
|
80
|
+
</p>
|
|
32
81
|
</div>
|
|
33
|
-
<h1 className="font-heading text-5xl font-semibold text-foreground leading-tight mb-4 tracking-tight">
|
|
34
|
-
{reportTitle}
|
|
35
|
-
</h1>
|
|
36
|
-
<p className="font-heading text-2xl text-muted mb-16 font-normal">
|
|
37
|
-
Prepared for {clientName}
|
|
38
|
-
</p>
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
83
|
+
{/* Metadata strip */}
|
|
84
|
+
{metadata && metadata.length > 0 && (
|
|
85
|
+
<div className="border-t border-border px-16 py-8">
|
|
86
|
+
<dl
|
|
87
|
+
className="grid gap-x-12 gap-y-4"
|
|
88
|
+
style={{
|
|
89
|
+
gridTemplateColumns: `repeat(${Math.min(metadata.length, 4)}, auto) 1fr`,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{metadata.map((item) => (
|
|
93
|
+
<div key={item.label}>
|
|
94
|
+
<dt className="font-sans text-[0.62rem] font-bold uppercase tracking-widest text-muted mb-1">
|
|
95
|
+
{item.label}
|
|
96
|
+
</dt>
|
|
97
|
+
<dd className="font-sans text-sm font-medium text-foreground">
|
|
98
|
+
{item.value}
|
|
99
|
+
</dd>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</dl>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
44
105
|
</div>
|
|
45
106
|
);
|
|
46
107
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Decision } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface DecisionTableProps {
|
|
4
|
+
decisions: Decision[];
|
|
5
|
+
intro?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function StatusBadge({ status }: { status: Decision["status"] }) {
|
|
9
|
+
if (status === "adopt-default") {
|
|
10
|
+
return (
|
|
11
|
+
<span className="inline-block px-2 py-0.5 rounded font-sans text-[0.65rem] font-bold uppercase tracking-widest bg-success/10 text-success border border-success/30">
|
|
12
|
+
Adopt default
|
|
13
|
+
</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return (
|
|
17
|
+
<span className="inline-block px-2 py-0.5 rounded font-sans text-[0.65rem] font-bold uppercase tracking-widest bg-warning/10 text-warning border border-warning/30">
|
|
18
|
+
Confirm at sync
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DecisionTable({ decisions, intro }: DecisionTableProps) {
|
|
24
|
+
const thClass =
|
|
25
|
+
"font-sans text-[0.68rem] font-bold uppercase tracking-widest text-primary px-3 py-2 border-b-2 border-primary text-left";
|
|
26
|
+
const tdClass = "px-3 py-2.5 border-b border-border-subtle align-top";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="my-4">
|
|
30
|
+
{intro && <p className="text-[0.9375rem] mb-4">{intro}</p>}
|
|
31
|
+
<table className="w-full border-collapse font-body text-[0.85rem]">
|
|
32
|
+
<thead>
|
|
33
|
+
<tr>
|
|
34
|
+
<th className={`${thClass} w-[4%]`}>#</th>
|
|
35
|
+
<th className={`${thClass} w-[28%]`}>Decision</th>
|
|
36
|
+
<th className={`${thClass} w-[44%]`}>Recommended default</th>
|
|
37
|
+
<th className={`${thClass} w-[24%]`}>Status</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
{decisions.map((d, i) => (
|
|
42
|
+
<tr key={d.id} className="break-inside-avoid">
|
|
43
|
+
<td className={`${tdClass} text-muted font-sans text-[0.78rem]`}>
|
|
44
|
+
{i + 1}
|
|
45
|
+
</td>
|
|
46
|
+
<td className={tdClass}>
|
|
47
|
+
<div className="font-sans font-semibold text-foreground">{d.title}</div>
|
|
48
|
+
{d.description && (
|
|
49
|
+
<div className="text-muted text-[0.82rem] mt-0.5">{d.description}</div>
|
|
50
|
+
)}
|
|
51
|
+
</td>
|
|
52
|
+
<td className={`${tdClass} text-muted`}>{d.recommendedDefault}</td>
|
|
53
|
+
<td className={tdClass}>
|
|
54
|
+
<StatusBadge status={d.status} />
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
))}
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProcessStage } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface ProcessStageProps {
|
|
4
|
+
stage: ProcessStage;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ProcessStageBlock({ stage }: ProcessStageProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mb-6 break-inside-avoid">
|
|
10
|
+
<div className="font-sans font-semibold text-primary text-xs uppercase tracking-widest mb-3">
|
|
11
|
+
{stage.name}
|
|
12
|
+
</div>
|
|
13
|
+
<ol className="list-decimal list-outside ml-5 space-y-2">
|
|
14
|
+
{stage.items.map((item) => (
|
|
15
|
+
<li
|
|
16
|
+
key={`${item.text}:${item.note ?? ""}`}
|
|
17
|
+
className="text-[0.9375rem] text-foreground pl-1"
|
|
18
|
+
>
|
|
19
|
+
{item.text}
|
|
20
|
+
{item.note && (
|
|
21
|
+
<span className="ml-1 text-muted text-[0.82rem] italic">({item.note})</span>
|
|
22
|
+
)}
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ol>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { TierMatrixCellValue, TierMatrixRow } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
interface TierMatrixProps {
|
|
5
|
+
columns: string[];
|
|
6
|
+
rows: TierMatrixRow[];
|
|
7
|
+
caption?: string;
|
|
8
|
+
/** Alignment for data cells. Defaults to "center" for symbol matrices; use "left" for text-heavy tables. */
|
|
9
|
+
alignCells?: "left" | "center";
|
|
10
|
+
/**
|
|
11
|
+
* Column sizing mode:
|
|
12
|
+
* - "auto" (default): browser distributes space by content — no explicit widths applied.
|
|
13
|
+
* - "capped": columnWidths[i] applied as max-width — content drives size up to the cap.
|
|
14
|
+
* - "manual": columnWidths[i] applied as explicit width.
|
|
15
|
+
* columnWidths is parallel to ALL columns: [0] = layer column, [1..n] = data columns.
|
|
16
|
+
*/
|
|
17
|
+
sizing?: "auto" | "capped" | "manual";
|
|
18
|
+
columnWidths?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function CellContent({ value }: { value: TierMatrixCellValue }) {
|
|
22
|
+
if (value === "required") {
|
|
23
|
+
return <span className="text-success font-bold text-base">✓</span>;
|
|
24
|
+
}
|
|
25
|
+
if (value === "recommended") {
|
|
26
|
+
return (
|
|
27
|
+
<span className="font-sans text-[0.7rem] font-semibold uppercase tracking-widest text-warning">
|
|
28
|
+
rec.
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (value === false) {
|
|
33
|
+
return <span className="text-muted">—</span>;
|
|
34
|
+
}
|
|
35
|
+
return <span>{value}</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TierMatrix({
|
|
39
|
+
columns,
|
|
40
|
+
rows,
|
|
41
|
+
caption,
|
|
42
|
+
alignCells = "center",
|
|
43
|
+
sizing = "auto",
|
|
44
|
+
columnWidths,
|
|
45
|
+
}: TierMatrixProps) {
|
|
46
|
+
const thClass =
|
|
47
|
+
"font-sans text-[0.68rem] font-bold uppercase tracking-widest text-primary px-3 py-2 border-b-2 border-primary";
|
|
48
|
+
const tdClass = "px-3 py-2 border-b border-border-subtle align-middle";
|
|
49
|
+
const colHeaderAlign = alignCells === "left" ? "text-left" : "text-center";
|
|
50
|
+
const cellAlign = alignCells === "left" ? "text-left" : "text-center";
|
|
51
|
+
|
|
52
|
+
function colStyle(index: number): CSSProperties | undefined {
|
|
53
|
+
const val = columnWidths?.[index];
|
|
54
|
+
if (!val) return undefined;
|
|
55
|
+
if (sizing === "manual") return { width: val };
|
|
56
|
+
if (sizing === "capped") return { maxWidth: val };
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="my-4">
|
|
62
|
+
<table className="w-full border-collapse font-body text-[0.85rem] break-inside-avoid">
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th className={`${thClass} text-left`} style={colStyle(0)}>
|
|
66
|
+
Layer
|
|
67
|
+
</th>
|
|
68
|
+
{columns.map((col, i) => (
|
|
69
|
+
<th
|
|
70
|
+
key={col}
|
|
71
|
+
className={`${thClass} ${colHeaderAlign}`}
|
|
72
|
+
style={colStyle(i + 1)}
|
|
73
|
+
>
|
|
74
|
+
{col}
|
|
75
|
+
</th>
|
|
76
|
+
))}
|
|
77
|
+
</tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
{rows.map((row) => (
|
|
81
|
+
<tr key={`${row.layer}:${row.cells.join("|")}`}>
|
|
82
|
+
<td
|
|
83
|
+
className={`${tdClass} text-left font-sans font-medium text-foreground`}
|
|
84
|
+
>
|
|
85
|
+
{row.layer}
|
|
86
|
+
</td>
|
|
87
|
+
{row.cells.map((cell, i) => (
|
|
88
|
+
<td
|
|
89
|
+
key={`${columns[i] ?? "cell"}:${String(cell)}`}
|
|
90
|
+
className={`${tdClass} ${cellAlign}`}
|
|
91
|
+
>
|
|
92
|
+
<CellContent value={cell} />
|
|
93
|
+
</td>
|
|
94
|
+
))}
|
|
95
|
+
</tr>
|
|
96
|
+
))}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
{caption && <p className="text-[0.78rem] text-muted italic mt-1">{caption}</p>}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// project (the report instance that supplies the actual data). Projects import
|
|
5
5
|
// the types they need; they do NOT edit this file.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Three families of types:
|
|
8
8
|
//
|
|
9
9
|
// 1. Strategic-assessment shapes — Finding, Recommendation, TimelinePhase,
|
|
10
10
|
// and the ReportData interface composed of them. These power the default
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// power rubric-style deliverables (an opportunity scoring playbook is one
|
|
15
15
|
// example). Each is optional; projects use whichever apply.
|
|
16
16
|
//
|
|
17
|
+
// 3. Process-guide shapes — TierMatrixRow, ProcessStage, CodeSample, Decision.
|
|
18
|
+
// Power process documentation deliverables (tiered requirements matrices,
|
|
19
|
+
// stage-by-stage process flows, technical appendices, sign-off tables).
|
|
20
|
+
//
|
|
17
21
|
// Adding a new section type? Add its interface here so all reports share the
|
|
18
22
|
// same vocabulary.
|
|
19
23
|
|
|
@@ -188,3 +192,47 @@ export interface TuningLogEntry {
|
|
|
188
192
|
rationale: string;
|
|
189
193
|
approver: string;
|
|
190
194
|
}
|
|
195
|
+
|
|
196
|
+
// --- Process-guide shapes ---
|
|
197
|
+
|
|
198
|
+
/** Generic N-column checklist matrix (e.g. requirements by tier/level). */
|
|
199
|
+
export type TierMatrixCellValue = "required" | "recommended" | false | string;
|
|
200
|
+
export interface TierMatrixRow {
|
|
201
|
+
/** Row label (e.g. "Formatter", "Secret scan"). */
|
|
202
|
+
layer: string;
|
|
203
|
+
/** One value per column, parallel to the `columns` prop on TierMatrix. */
|
|
204
|
+
cells: TierMatrixCellValue[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One item inside a ProcessStage. */
|
|
208
|
+
export interface ProcessStageItem {
|
|
209
|
+
text: string;
|
|
210
|
+
/** Optional inline cross-reference, e.g. "see §4.6". Renders muted. */
|
|
211
|
+
note?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A named process stage with ordered action items. */
|
|
215
|
+
export interface ProcessStage {
|
|
216
|
+
id: string;
|
|
217
|
+
name: string;
|
|
218
|
+
items: ProcessStageItem[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A code sample for a technical appendix. */
|
|
222
|
+
export interface CodeSample {
|
|
223
|
+
id: string;
|
|
224
|
+
title: string;
|
|
225
|
+
/** Language identifier shown as a badge, e.g. "yaml", "typescript", "markdown". */
|
|
226
|
+
language: string;
|
|
227
|
+
caption?: string;
|
|
228
|
+
code: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A single row in a sign-off or open-decisions table. */
|
|
232
|
+
export interface Decision {
|
|
233
|
+
id: string;
|
|
234
|
+
title: string;
|
|
235
|
+
description?: string;
|
|
236
|
+
recommendedDefault: string;
|
|
237
|
+
status: "adopt-default" | "confirm-at-sync";
|
|
238
|
+
}
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir> [--pdf <out>] [--html <out>] [--skip-build]
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { createReadStream, constants as fsConstants } from "node:fs";
|
|
16
|
-
import { access, stat } from "node:fs/promises";
|
|
15
|
+
import { createReadStream, constants as fsConstants, realpathSync } from "node:fs";
|
|
16
|
+
import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
17
17
|
import { createServer, type Server } from "node:http";
|
|
18
18
|
import { extname, join, resolve } from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { PDFDocument } from "pdf-lib";
|
|
20
21
|
import { chromium } from "playwright";
|
|
21
22
|
|
|
22
23
|
interface ReportMeta {
|
|
@@ -24,6 +25,10 @@ interface ReportMeta {
|
|
|
24
25
|
reportTitle: string;
|
|
25
26
|
classification: string;
|
|
26
27
|
consultancyName: string;
|
|
28
|
+
/** Public path to consultancy logo (e.g. "/logos/konvert7.svg"). Used in the PDF footer. */
|
|
29
|
+
consultancyLogoSrc?: string;
|
|
30
|
+
/** Public path to client logo (e.g. "/logos/transcend.svg"). Used in the PDF header. */
|
|
31
|
+
clientLogoSrc?: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const COLOR = {
|
|
@@ -56,6 +61,25 @@ function slugify(s: string): string {
|
|
|
56
61
|
.replace(/^-|-$/g, "");
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** Reads a logo file from the static export and returns a base64 data URI, or null if not found. */
|
|
65
|
+
async function logoDataUri(
|
|
66
|
+
outDir: string,
|
|
67
|
+
publicPath: string | undefined
|
|
68
|
+
): Promise<string | null> {
|
|
69
|
+
if (!publicPath) return null;
|
|
70
|
+
const filePath = join(outDir, publicPath);
|
|
71
|
+
if (!(await exists(filePath))) return null;
|
|
72
|
+
const buf = await readFile(filePath);
|
|
73
|
+
const ext = extname(filePath).toLowerCase();
|
|
74
|
+
let mime = "image/jpeg";
|
|
75
|
+
if (ext === ".svg") {
|
|
76
|
+
mime = "image/svg+xml";
|
|
77
|
+
} else if (ext === ".png") {
|
|
78
|
+
mime = "image/png";
|
|
79
|
+
}
|
|
80
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
async function loadMeta(reportDir: string): Promise<ReportMeta> {
|
|
60
84
|
const dataPath = join(reportDir, "lib", "report-data.ts");
|
|
61
85
|
if (!(await exists(dataPath))) {
|
|
@@ -150,29 +174,79 @@ async function renderPdf(
|
|
|
150
174
|
);
|
|
151
175
|
});
|
|
152
176
|
|
|
177
|
+
// Load logos as base64 data URIs — Playwright's header/footer templates
|
|
178
|
+
// run in an isolated context and cannot load external or local-path URLs.
|
|
179
|
+
const [clientUri, consultancyUri] = await Promise.all([
|
|
180
|
+
logoDataUri(outDir, meta.clientLogoSrc),
|
|
181
|
+
logoDataUri(outDir, meta.consultancyLogoSrc),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const clientSlot = clientUri
|
|
185
|
+
? `<img src="${clientUri}" style="height:18px; width:auto; object-fit:contain; display:block;">`
|
|
186
|
+
: `<span style="font-weight:600; color:${COLOR.navy}; letter-spacing:0.05em;">${escapeHtml(meta.clientName.toUpperCase())}</span>`;
|
|
187
|
+
|
|
188
|
+
const consultancySlot = consultancyUri
|
|
189
|
+
? `<img src="${consultancyUri}" style="height:14px; width:auto; object-fit:contain; display:block;">`
|
|
190
|
+
: `<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>`;
|
|
191
|
+
|
|
153
192
|
const header = `
|
|
154
193
|
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
155
|
-
|
|
194
|
+
${clientSlot}
|
|
156
195
|
<span style="color:#94a3b8;">${escapeHtml(meta.reportTitle)}</span>
|
|
157
196
|
</div>`;
|
|
158
197
|
|
|
198
|
+
// Footer: consultancy logo left, page number right — no classification (cover carries it)
|
|
159
199
|
const footer = `
|
|
160
200
|
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
161
|
-
|
|
162
|
-
<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>
|
|
201
|
+
${consultancySlot}
|
|
163
202
|
<span style="color:${COLOR.navy};"><span class="pageNumber"></span></span>
|
|
164
203
|
</div>`;
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
const margin = { top: "0.7in", right: "0.7in", bottom: "0.7in", left: "0.7in" };
|
|
206
|
+
|
|
207
|
+
// Render cover (page 1) and body (pages 2+) separately so the cover has
|
|
208
|
+
// no header/footer and full-bleed margins, then merge with pdf-lib.
|
|
209
|
+
const tmpCover = `${pdfPath}.cover.tmp.pdf`;
|
|
210
|
+
const tmpBody = `${pdfPath}.body.tmp.pdf`;
|
|
211
|
+
|
|
212
|
+
await Promise.all([
|
|
213
|
+
page.pdf({
|
|
214
|
+
path: tmpCover,
|
|
215
|
+
format: "A4",
|
|
216
|
+
pageRanges: "1",
|
|
217
|
+
printBackground: true,
|
|
218
|
+
displayHeaderFooter: false,
|
|
219
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
220
|
+
preferCSSPageSize: false,
|
|
221
|
+
}),
|
|
222
|
+
page.pdf({
|
|
223
|
+
path: tmpBody,
|
|
224
|
+
format: "A4",
|
|
225
|
+
pageRanges: "2-",
|
|
226
|
+
printBackground: true,
|
|
227
|
+
displayHeaderFooter: true,
|
|
228
|
+
headerTemplate: header,
|
|
229
|
+
footerTemplate: footer,
|
|
230
|
+
margin,
|
|
231
|
+
preferCSSPageSize: false,
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
// Merge: use body PDF as the base so its named destinations and link
|
|
236
|
+
// annotations stay intact. Insert the cover page at position 0.
|
|
237
|
+
const [coverBytes, bodyBytes] = await Promise.all([
|
|
238
|
+
readFile(tmpCover),
|
|
239
|
+
readFile(tmpBody),
|
|
240
|
+
]);
|
|
241
|
+
const [coverDoc, bodyDoc] = await Promise.all([
|
|
242
|
+
PDFDocument.load(coverBytes),
|
|
243
|
+
PDFDocument.load(bodyBytes),
|
|
244
|
+
]);
|
|
245
|
+
const [coverPage] = await bodyDoc.copyPages(coverDoc, [0]);
|
|
246
|
+
bodyDoc.insertPage(0, coverPage);
|
|
247
|
+
await writeFile(pdfPath, await bodyDoc.save());
|
|
248
|
+
|
|
249
|
+
await Promise.all([unlink(tmpCover), unlink(tmpBody)]);
|
|
176
250
|
} finally {
|
|
177
251
|
await browser.close();
|
|
178
252
|
server.close();
|
|
@@ -236,9 +310,18 @@ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
|
236
310
|
}
|
|
237
311
|
|
|
238
312
|
// Node ≥ 22.6 doesn't expose import.meta.main; gate on argv[1] instead.
|
|
313
|
+
// Use realpathSync on both sides so symlinked skill paths (e.g. ~/.pal/skills →
|
|
314
|
+
// PAL repo) match the resolved import.meta.url.
|
|
315
|
+
function realResolve(p: string): string {
|
|
316
|
+
try {
|
|
317
|
+
return realpathSync(resolve(p));
|
|
318
|
+
} catch {
|
|
319
|
+
return resolve(p);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
239
322
|
const isMain =
|
|
240
323
|
process.argv[1] &&
|
|
241
|
-
|
|
324
|
+
realResolve(process.argv[1]) === realResolve(new URL(import.meta.url).pathname);
|
|
242
325
|
if (isMain) {
|
|
243
326
|
await run();
|
|
244
327
|
}
|