@deskwork/studio 0.9.5

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.
Files changed (93) hide show
  1. package/dist/build-client-assets.d.ts +51 -0
  2. package/dist/build-client-assets.d.ts.map +1 -0
  3. package/dist/build-client-assets.js +341 -0
  4. package/dist/build-client-assets.js.map +1 -0
  5. package/dist/components/scrapbook-item.d.ts +108 -0
  6. package/dist/components/scrapbook-item.d.ts.map +1 -0
  7. package/dist/components/scrapbook-item.js +205 -0
  8. package/dist/components/scrapbook-item.js.map +1 -0
  9. package/dist/lib/editorial-skills-catalogue.d.ts +33 -0
  10. package/dist/lib/editorial-skills-catalogue.d.ts.map +1 -0
  11. package/dist/lib/editorial-skills-catalogue.js +211 -0
  12. package/dist/lib/editorial-skills-catalogue.js.map +1 -0
  13. package/dist/lib/override-render.d.ts +41 -0
  14. package/dist/lib/override-render.d.ts.map +1 -0
  15. package/dist/lib/override-render.js +80 -0
  16. package/dist/lib/override-render.js.map +1 -0
  17. package/dist/listen.d.ts +78 -0
  18. package/dist/listen.d.ts.map +1 -0
  19. package/dist/listen.js +155 -0
  20. package/dist/listen.js.map +1 -0
  21. package/dist/pages/chrome.d.ts +26 -0
  22. package/dist/pages/chrome.d.ts.map +1 -0
  23. package/dist/pages/chrome.js +50 -0
  24. package/dist/pages/chrome.js.map +1 -0
  25. package/dist/pages/content-detail.d.ts +14 -0
  26. package/dist/pages/content-detail.d.ts.map +1 -0
  27. package/dist/pages/content-detail.js +279 -0
  28. package/dist/pages/content-detail.js.map +1 -0
  29. package/dist/pages/content.d.ts +39 -0
  30. package/dist/pages/content.d.ts.map +1 -0
  31. package/dist/pages/content.js +414 -0
  32. package/dist/pages/content.js.map +1 -0
  33. package/dist/pages/dashboard.d.ts +32 -0
  34. package/dist/pages/dashboard.d.ts.map +1 -0
  35. package/dist/pages/dashboard.js +803 -0
  36. package/dist/pages/dashboard.js.map +1 -0
  37. package/dist/pages/help.d.ts +24 -0
  38. package/dist/pages/help.d.ts.map +1 -0
  39. package/dist/pages/help.js +433 -0
  40. package/dist/pages/help.js.map +1 -0
  41. package/dist/pages/html.d.ts +35 -0
  42. package/dist/pages/html.d.ts.map +1 -0
  43. package/dist/pages/html.js +73 -0
  44. package/dist/pages/html.js.map +1 -0
  45. package/dist/pages/index.d.ts +21 -0
  46. package/dist/pages/index.d.ts.map +1 -0
  47. package/dist/pages/index.js +174 -0
  48. package/dist/pages/index.js.map +1 -0
  49. package/dist/pages/layout.d.ts +33 -0
  50. package/dist/pages/layout.d.ts.map +1 -0
  51. package/dist/pages/layout.js +50 -0
  52. package/dist/pages/layout.js.map +1 -0
  53. package/dist/pages/review-scrapbook-drawer.d.ts +20 -0
  54. package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -0
  55. package/dist/pages/review-scrapbook-drawer.js +98 -0
  56. package/dist/pages/review-scrapbook-drawer.js.map +1 -0
  57. package/dist/pages/review.d.ts +68 -0
  58. package/dist/pages/review.d.ts.map +1 -0
  59. package/dist/pages/review.js +434 -0
  60. package/dist/pages/review.js.map +1 -0
  61. package/dist/pages/scrapbook.d.ts +21 -0
  62. package/dist/pages/scrapbook.d.ts.map +1 -0
  63. package/dist/pages/scrapbook.js +250 -0
  64. package/dist/pages/scrapbook.js.map +1 -0
  65. package/dist/pages/shortform.d.ts +17 -0
  66. package/dist/pages/shortform.d.ts.map +1 -0
  67. package/dist/pages/shortform.js +142 -0
  68. package/dist/pages/shortform.js.map +1 -0
  69. package/dist/request-context.d.ts +52 -0
  70. package/dist/request-context.d.ts.map +1 -0
  71. package/dist/request-context.js +84 -0
  72. package/dist/request-context.js.map +1 -0
  73. package/dist/routes/api.d.ts +36 -0
  74. package/dist/routes/api.d.ts.map +1 -0
  75. package/dist/routes/api.js +175 -0
  76. package/dist/routes/api.js.map +1 -0
  77. package/dist/routes/scrapbook-file.d.ts +19 -0
  78. package/dist/routes/scrapbook-file.d.ts.map +1 -0
  79. package/dist/routes/scrapbook-file.js +77 -0
  80. package/dist/routes/scrapbook-file.js.map +1 -0
  81. package/dist/routes/scrapbook-mutations.d.ts +33 -0
  82. package/dist/routes/scrapbook-mutations.d.ts.map +1 -0
  83. package/dist/routes/scrapbook-mutations.js +310 -0
  84. package/dist/routes/scrapbook-mutations.js.map +1 -0
  85. package/dist/server.d.ts +52 -0
  86. package/dist/server.d.ts.map +1 -0
  87. package/dist/server.js +581 -0
  88. package/dist/server.js.map +1 -0
  89. package/dist/tailscale.d.ts +63 -0
  90. package/dist/tailscale.d.ts.map +1 -0
  91. package/dist/tailscale.js +118 -0
  92. package/dist/tailscale.js.map +1 -0
  93. package/package.json +60 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Shared scrapbook-item renderer.
3
+ *
4
+ * Three studio surfaces consume this module:
5
+ *
6
+ * 1. Standalone scrapbook viewer — `pages/scrapbook.ts`
7
+ * 2. Review-page drawer (Phase 16c) — `pages/review.ts`
8
+ * 3. Bird's-eye content view detail panel (Phase 16d) — `pages/content.ts`
9
+ *
10
+ * Goal: the operator sees consistent in-browser preview behavior wherever
11
+ * a scrapbook item appears. The standalone viewer keeps its richer
12
+ * disclosure / edit / rename / delete affordances; the read-only views
13
+ * (review drawer, content-view detail panel) reuse the same kind chip,
14
+ * filename, size, mtime, and the same in-browser preview rules:
15
+ *
16
+ * - Image kinds (`png`, `jpg`, `jpeg`, `webp`, `gif`, `svg`)
17
+ * render an inline thumbnail with a small overlay action to view
18
+ * full-size; clicking opens the served file in a new tab. Both
19
+ * image and PDF surfaces use a read-only binary endpoint
20
+ * (`GET /api/dev/scrapbook-file`) that the studio adds for these
21
+ * read-only views — distinct from the (not-yet-ported) full
22
+ * scrapbook CRUD API.
23
+ * - PDF — embedded via `<iframe>` using the same binary endpoint. The
24
+ * browser renders it natively.
25
+ * - Plain text / JSON — inline-truncated `<pre>` preview. Operators
26
+ * can read the first ~10 lines without leaving the page; a "view
27
+ * full" link opens the standalone viewer where the full file
28
+ * is mounted.
29
+ * - Markdown — kept as a kind-only row. Markdown is the editable
30
+ * surface — operators jump to the standalone viewer to edit. Showing
31
+ * a raw markdown preview here would either duplicate the editor or
32
+ * misrepresent what double-click does on this surface.
33
+ * - Anything else — kind chip + a download link. The browser can't
34
+ * render it, so make that explicit.
35
+ *
36
+ * Inline previews for text + JSON come from the server side: the
37
+ * standalone viewer only loads body content on disclosure (lazy), but
38
+ * the read-only renderers want the preview embedded at server-render
39
+ * time so operators don't see a flash. Callers pass an
40
+ * `inlinePreviewLoader` that knows how to read the first N bytes of
41
+ * the file and return a string; this module composes the result into
42
+ * the row HTML.
43
+ */
44
+ import { formatRelativeTime, formatSize, } from '@deskwork/core/scrapbook';
45
+ import { html, unsafe } from "../pages/html.js";
46
+ const DEFAULT_PREVIEW_BYTES = 800;
47
+ const TEXT_PREVIEW_LINES = 8;
48
+ // ---------------------------------------------------------------------------
49
+ // URL helpers
50
+ // ---------------------------------------------------------------------------
51
+ /**
52
+ * Build the URL to fetch a scrapbook file's raw bytes from the
53
+ * read-only binary endpoint. The endpoint is read-only by design —
54
+ * Phase 16's image / PDF previews need a stable URL, but full
55
+ * scrapbook CRUD remains in the standalone viewer's surface.
56
+ */
57
+ export function scrapbookFileUrl(address, filename, opts = {}) {
58
+ const params = new URLSearchParams({
59
+ site: address.site,
60
+ path: address.path,
61
+ name: filename,
62
+ });
63
+ if (opts.secret)
64
+ params.set('secret', '1');
65
+ return `/api/dev/scrapbook-file?${params.toString()}`;
66
+ }
67
+ /**
68
+ * Build the URL to the standalone scrapbook viewer for an address —
69
+ * the operator's "open scrapbook" jumping-off point.
70
+ */
71
+ export function scrapbookViewerUrl(address) {
72
+ // The path is already kebab-case + slash-separated; encodeURI keeps
73
+ // the slashes literal while escaping anything else (defensive).
74
+ return `/dev/scrapbook/${address.site}/${encodeURI(address.path)}`;
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Per-kind preview detection
78
+ // ---------------------------------------------------------------------------
79
+ const IMAGE_EXTENSIONS = new Set([
80
+ '.png',
81
+ '.jpg',
82
+ '.jpeg',
83
+ '.gif',
84
+ '.webp',
85
+ '.svg',
86
+ ]);
87
+ const PDF_EXTENSIONS = new Set(['.pdf']);
88
+ function lowerExt(filename) {
89
+ const dot = filename.lastIndexOf('.');
90
+ return dot < 0 ? '' : filename.slice(dot).toLowerCase();
91
+ }
92
+ function isImageFilename(filename) {
93
+ return IMAGE_EXTENSIONS.has(lowerExt(filename));
94
+ }
95
+ function isPdfFilename(filename) {
96
+ return PDF_EXTENSIONS.has(lowerExt(filename));
97
+ }
98
+ function kindLabel(kind) {
99
+ return kind === 'other' ? '·' : kind.toUpperCase();
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Inline preview helpers
103
+ // ---------------------------------------------------------------------------
104
+ function truncateForInline(raw, lines) {
105
+ const split = raw.split('\n').slice(0, lines);
106
+ // Append an ellipsis line when truncation actually happened. The
107
+ // visual fade-out gradient handles the polish; the ellipsis line
108
+ // makes the truncation explicit even with CSS off.
109
+ const truncated = raw.split('\n').length > lines || raw.length > 1024;
110
+ return truncated ? `${split.join('\n')}\n…` : split.join('\n');
111
+ }
112
+ function loadInlineText(filename, options) {
113
+ const loader = options.inlinePreviewLoader;
114
+ if (!loader)
115
+ return null;
116
+ const max = options.inlinePreviewMaxBytes ?? DEFAULT_PREVIEW_BYTES;
117
+ const raw = loader(filename, max);
118
+ if (raw === null)
119
+ return null;
120
+ return truncateForInline(raw, TEXT_PREVIEW_LINES);
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Read-only row renderer
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Render a single scrapbook item as a read-only row for the review
127
+ * drawer or the content-view detail panel. Returns the row HTML wrapped
128
+ * in `unsafe(...)`.
129
+ *
130
+ * Visual posture: kind chip + filename + size + mtime, with an inline
131
+ * preview block beneath the row for kinds the browser can render in
132
+ * place (image thumbnail, text/JSON truncated, PDF embed). Markdown +
133
+ * unrenderable kinds keep a single-line row.
134
+ */
135
+ export function renderReadOnlyScrapbookRow(address, item, opts = {}) {
136
+ const fileUrl = scrapbookFileUrl(address, item.name);
137
+ const sizeText = formatSize(item.size);
138
+ const mtimeText = formatRelativeTime(item.mtime);
139
+ if (isImageFilename(item.name)) {
140
+ return unsafe(html `
141
+ <div class="scrap scrap--img" data-kind="img" data-filename="${item.name}">
142
+ <a class="scrap__thumb-link" href="${fileUrl}" target="_blank" rel="noopener"
143
+ aria-label="Open ${item.name} in a new tab">
144
+ <img class="scrap__thumb" loading="lazy" alt="" src="${fileUrl}">
145
+ </a>
146
+ <span class="scrap__name">${item.name}</span>
147
+ <span class="scrap__size">${sizeText}</span>
148
+ <span class="scrap__mtime">${mtimeText}</span>
149
+ </div>`);
150
+ }
151
+ if (isPdfFilename(item.name)) {
152
+ return unsafe(html `
153
+ <div class="scrap scrap--pdf" data-kind="pdf" data-filename="${item.name}">
154
+ <span class="scrap__kind">PDF</span>
155
+ <span class="scrap__name">
156
+ <a class="scrap__name-link" href="${fileUrl}" target="_blank" rel="noopener">${item.name}</a>
157
+ </span>
158
+ <span class="scrap__size">${sizeText}</span>
159
+ <span class="scrap__mtime">${mtimeText}</span>
160
+ <iframe class="scrap__pdf-frame" src="${fileUrl}#view=FitH" title="${item.name}"
161
+ aria-label="PDF preview of ${item.name}"></iframe>
162
+ </div>`);
163
+ }
164
+ if (item.kind === 'txt' || item.kind === 'json') {
165
+ const inline = loadInlineText(item.name, opts);
166
+ if (inline !== null) {
167
+ return unsafe(html `
168
+ <div class="scrap scrap--with-preview" data-kind="${item.kind}" data-filename="${item.name}">
169
+ <span class="scrap__kind">${kindLabel(item.kind)}</span>
170
+ <span class="scrap__name">
171
+ <a class="scrap__name-link" href="${fileUrl}" target="_blank" rel="noopener">${item.name}</a>
172
+ </span>
173
+ <span class="scrap__size">${sizeText}</span>
174
+ <span class="scrap__mtime">${mtimeText}</span>
175
+ <pre class="scrap__inline-preview">${inline}</pre>
176
+ </div>`);
177
+ }
178
+ // No loader provided — fall through to the single-line row.
179
+ }
180
+ // Default row — kind chip + filename + size + mtime. Markdown lands
181
+ // here so operators jump to the standalone viewer to edit.
182
+ const linkAttr = item.kind === 'md' ? '' : ' target="_blank" rel="noopener"';
183
+ const href = item.kind === 'md' ? scrapbookViewerUrl(address) : fileUrl;
184
+ return unsafe(html `
185
+ <div class="scrap" data-kind="${item.kind}" data-filename="${item.name}">
186
+ <span class="scrap__kind">${kindLabel(item.kind)}</span>
187
+ <span class="scrap__name">
188
+ <a class="scrap__name-link" href="${href}"${unsafe(linkAttr)}>${item.name}</a>
189
+ </span>
190
+ <span class="scrap__size">${sizeText}</span>
191
+ <span class="scrap__mtime">${mtimeText}</span>
192
+ </div>`);
193
+ }
194
+ /**
195
+ * Render an empty-state row for a scrapbook drawer / panel that has
196
+ * no items. Shown faded so the operator still sees the section exists
197
+ * for this node.
198
+ */
199
+ export function renderEmptyScrapbookRow() {
200
+ return unsafe(html `
201
+ <div class="scrap scrap--empty" data-state="empty">
202
+ <span class="scrap__empty-text">no scrapbook items</span>
203
+ </div>`);
204
+ }
205
+ //# sourceMappingURL=scrapbook-item.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-item.js","sourceRoot":"","sources":["../../src/components/scrapbook-item.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Editorial skill catalogue — the source of truth for the specimen grid
3
+ * on /dev/editorial-help and any future CLI or doc generator that needs
4
+ * the same inventory.
5
+ *
6
+ * Each skill carries four fields: `slug`, `kind` (see below), `desc`,
7
+ * `when`, `changes`, and an optional `flags` string. The `kind` drives
8
+ * the corner stamp on the help-page specimen card:
9
+ *
10
+ * - cognitive → red pencil — Claude Code drafts/revises prose
11
+ * - mechanical → proof blue — state transition, disk write, no writing
12
+ * - readonly → faded ink — reports, audits, listings
13
+ * - voice → stamp purple — called by other skills, not directly
14
+ */
15
+ export type SkillKind = 'cognitive' | 'mechanical' | 'readonly' | 'voice';
16
+ export interface Skill {
17
+ slug: string;
18
+ kind: SkillKind;
19
+ desc: string;
20
+ when: string;
21
+ changes: string;
22
+ flags?: string;
23
+ }
24
+ /** Display label per kind. Used by the help-page specimen stamps. */
25
+ export declare const KIND_LABEL: Readonly<Record<SkillKind, string>>;
26
+ /**
27
+ * The 22 editorial skills that ship with this repository, in the order
28
+ * they appear in `.claude/skills/` (alphabetical). Voice skills belong
29
+ * at the end of any UI listing — see `SKILLS_SORTED` below.
30
+ */
31
+ export declare const SKILLS: readonly Skill[];
32
+ export declare const SKILLS_SORTED: readonly Skill[];
33
+ //# sourceMappingURL=editorial-skills-catalogue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorial-skills-catalogue.d.ts","sourceRoot":"","sources":["../../src/lib/editorial-skills-catalogue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAK1D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EA8KlC,CAAC;AAUF,eAAO,MAAM,aAAa,EAAE,SAAS,KAAK,EAA6B,CAAC"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Editorial skill catalogue — the source of truth for the specimen grid
3
+ * on /dev/editorial-help and any future CLI or doc generator that needs
4
+ * the same inventory.
5
+ *
6
+ * Each skill carries four fields: `slug`, `kind` (see below), `desc`,
7
+ * `when`, `changes`, and an optional `flags` string. The `kind` drives
8
+ * the corner stamp on the help-page specimen card:
9
+ *
10
+ * - cognitive → red pencil — Claude Code drafts/revises prose
11
+ * - mechanical → proof blue — state transition, disk write, no writing
12
+ * - readonly → faded ink — reports, audits, listings
13
+ * - voice → stamp purple — called by other skills, not directly
14
+ */
15
+ /** Display label per kind. Used by the help-page specimen stamps. */
16
+ export const KIND_LABEL = {
17
+ cognitive: 'cognitive',
18
+ mechanical: 'mechanical',
19
+ readonly: 'read-only',
20
+ voice: 'voice',
21
+ };
22
+ /**
23
+ * The 22 editorial skills that ship with this repository, in the order
24
+ * they appear in `.claude/skills/` (alphabetical). Voice skills belong
25
+ * at the end of any UI listing — see `SKILLS_SORTED` below.
26
+ */
27
+ export const SKILLS = [
28
+ {
29
+ slug: 'editorial-help',
30
+ kind: 'readonly',
31
+ desc: 'Print the workflow overview and calendar status across every stage.',
32
+ when: 'When you have forgotten the shape of the pipeline, or want a full situation report.',
33
+ changes: 'Nothing. Read-only.',
34
+ flags: '--site <slug>',
35
+ },
36
+ {
37
+ slug: 'editorial-status',
38
+ kind: 'readonly',
39
+ desc: 'Calendar status in a compact form — just the stage columns, no prose.',
40
+ when: 'Between sessions. Quick roll-call of what is where.',
41
+ changes: 'Nothing. Read-only.',
42
+ flags: '--site <slug>',
43
+ },
44
+ {
45
+ slug: 'editorial-add',
46
+ kind: 'mechanical',
47
+ desc: 'Capture a new idea in the Ideas stage — title, optional description, content type.',
48
+ when: 'The instant an idea is worth persisting. Pre-commit, not post-draft.',
49
+ changes: 'Appends a row to the calendar under Ideas. No file scaffolded yet.',
50
+ flags: '--site <slug>',
51
+ },
52
+ {
53
+ slug: 'editorial-plan',
54
+ kind: 'cognitive',
55
+ desc: 'Promote Ideas → Planned; set target keywords and topic tags.',
56
+ when: 'The idea has matured enough to point at a shape.',
57
+ changes: 'Moves the calendar row; writes keywords onto the entry.',
58
+ flags: '--site <slug> <slug>',
59
+ },
60
+ {
61
+ slug: 'editorial-draft',
62
+ kind: 'mechanical',
63
+ desc: 'Scaffold the blog post directory + frontmatter from a Planned entry and advance to Drafting.',
64
+ when: 'Ready to begin writing. Can also be triggered from the studio button.',
65
+ changes: 'Creates src/sites/<site>/content/blog/<slug>.md with frontmatter (state: draft); calendar stage flips to Drafting.',
66
+ flags: '--site <slug> <slug>',
67
+ },
68
+ {
69
+ slug: 'editorial-draft-review',
70
+ kind: 'mechanical',
71
+ desc: 'Enqueue an existing blog draft into the editorial-review pipeline and print the /dev/editorial-review URL.',
72
+ when: 'The draft is written and ready for annotation + iteration.',
73
+ changes: 'Opens a review workflow in state open. No edit to the draft itself.',
74
+ flags: '--site <slug> <slug>',
75
+ },
76
+ {
77
+ slug: 'editorial-iterate',
78
+ kind: 'cognitive',
79
+ desc: 'Revise a draft based on margin-note comments using the site voice skill; appends a new DraftVersion.',
80
+ when: 'After the operator clicks Iterate in the review page.',
81
+ changes: 'Writes a new version to the review journal; workflow flips iterating → in-review.',
82
+ flags: '<workflow-id> or --site <slug> <slug>',
83
+ },
84
+ {
85
+ slug: 'editorial-approve',
86
+ kind: 'mechanical',
87
+ desc: 'Write the approved draft version to its destination (blog file for longform; shortform copy into the calendar) and transition to applied.',
88
+ when: 'After the operator clicks Approve in the review page.',
89
+ changes: 'Overwrites the destination file; workflow becomes applied; calendar untouched (publish is separate).',
90
+ flags: '<workflow-id>',
91
+ },
92
+ {
93
+ slug: 'editorial-review-cancel',
94
+ kind: 'mechanical',
95
+ desc: 'Cancel an active review workflow; leaves the source file untouched.',
96
+ when: 'A draft has been abandoned or replaced; clean up the pipeline.',
97
+ changes: 'Workflow transitions to cancelled terminal state. Source file not modified.',
98
+ flags: '<workflow-id>',
99
+ },
100
+ {
101
+ slug: 'editorial-review-help',
102
+ kind: 'readonly',
103
+ desc: 'Editorial-review pipeline state across all active workflows + next action per workflow.',
104
+ when: 'Resuming review work across multiple drafts or sites.',
105
+ changes: 'Nothing. Read-only.',
106
+ },
107
+ {
108
+ slug: 'editorial-review-report',
109
+ kind: 'readonly',
110
+ desc: 'Aggregate comment-annotation categories across completed workflows — which voice-skill principles are drifting most.',
111
+ when: 'Monthly-ish. Inputs for voice-skill revisions.',
112
+ changes: 'Nothing. Read-only.',
113
+ flags: '--site <slug>',
114
+ },
115
+ {
116
+ slug: 'editorial-publish',
117
+ kind: 'mechanical',
118
+ desc: 'Flip a Drafting or Review entry to Published and stamp today’s date.',
119
+ when: 'The blog file is live. Usually after /editorial-approve and a human commit.',
120
+ changes: 'Sets datePublished on the entry; stage becomes Published. Does not commit.',
121
+ flags: '--site <slug> <slug>',
122
+ },
123
+ {
124
+ slug: 'editorial-shortform-draft',
125
+ kind: 'cognitive',
126
+ desc: 'Draft a social post (Reddit title+body, YouTube description, LinkedIn, newsletter) for a published entry, using the site voice. Enqueues a shortform review workflow.',
127
+ when: 'After /editorial-publish, once the post is live and worth amplifying.',
128
+ changes: 'Creates a new review workflow with contentKind=shortform and the drafted copy as v1.',
129
+ flags: '--site <slug> <slug> <platform> [channel]',
130
+ },
131
+ {
132
+ slug: 'editorial-distribute',
133
+ kind: 'mechanical',
134
+ desc: 'Record that a published post was shared to a social platform — URL, date, sub-channel.',
135
+ when: 'After you actually hit post. Closes the loop with analytics.',
136
+ changes: 'Appends a DistributionRecord to the calendar file.',
137
+ flags: '--site <slug> <slug> <platform> <url>',
138
+ },
139
+ {
140
+ slug: 'editorial-social-review',
141
+ kind: 'readonly',
142
+ desc: 'Matrix of published posts × social platforms showing which combinations have been shared.',
143
+ when: 'Looking for cross-post holes.',
144
+ changes: 'Nothing. Read-only.',
145
+ flags: '--site <slug>',
146
+ },
147
+ {
148
+ slug: 'editorial-reddit-sync',
149
+ kind: 'mechanical',
150
+ desc: 'Pull recent Reddit submissions via the API; upsert DistributionRecords for any that reference the site’s posts or videos.',
151
+ when: 'Reconciling distribution state without re-entering by hand.',
152
+ changes: 'Adds or updates DistributionRecord rows in the calendar.',
153
+ flags: '--site <slug>',
154
+ },
155
+ {
156
+ slug: 'editorial-reddit-opportunities',
157
+ kind: 'cognitive',
158
+ desc: 'For a published post, list relevant subreddits split into already-shared (skip) and unshared candidates with subscriber count and self-promo hints.',
159
+ when: 'Planning a cross-post run.',
160
+ changes: 'Nothing. Read-only.',
161
+ flags: '--site <slug> <slug>',
162
+ },
163
+ {
164
+ slug: 'editorial-cross-link-review',
165
+ kind: 'readonly',
166
+ desc: 'Audit bidirectional linking between blog posts and YouTube videos; flag missing reciprocal links.',
167
+ when: 'Before shipping a YouTube entry, or as a periodic audit.',
168
+ changes: 'Nothing. Read-only.',
169
+ flags: '--site <slug>',
170
+ },
171
+ {
172
+ slug: 'editorial-performance',
173
+ kind: 'readonly',
174
+ desc: 'Analytics metrics for published posts; flags underperformers that might warrant revision or a better cross-post.',
175
+ when: 'Cadence review. Not for individual posts.',
176
+ changes: 'Nothing. Read-only.',
177
+ flags: '--site <slug>',
178
+ },
179
+ {
180
+ slug: 'editorial-suggest',
181
+ kind: 'cognitive',
182
+ desc: 'Pull analytics and suggest new ideas for the Ideas stage based on observed queries and gaps.',
183
+ when: 'When the Ideas column is thin.',
184
+ changes: 'Prints suggestions. Does not write them — pair with /editorial-add.',
185
+ flags: '--site <slug>',
186
+ },
187
+ {
188
+ slug: 'audiocontrol-voice',
189
+ kind: 'voice',
190
+ desc: 'Voice skill for audiocontrol.org — service-manual register, hardware-specific vocabulary, dated specs.',
191
+ when: 'Called by /editorial-iterate and /editorial-shortform-draft when site=audiocontrol. Not invoked directly by the operator.',
192
+ changes: 'Nothing. Provides the register for the caller.',
193
+ },
194
+ {
195
+ slug: 'editorialcontrol-voice',
196
+ kind: 'voice',
197
+ desc: 'Voice skill for editorialcontrol.org — publication register, argument-driven, magazine typography.',
198
+ when: 'Called by /editorial-iterate and /editorial-shortform-draft when site=editorialcontrol. Not invoked directly.',
199
+ changes: 'Nothing. Provides the register for the caller.',
200
+ },
201
+ ];
202
+ /**
203
+ * Display order for UI listings: non-voice skills alphabetised first,
204
+ * voice skills alphabetised at the end. Voice skills are infrastructure
205
+ * for other skills, not directly invocable — putting them at the bottom
206
+ * reflects that.
207
+ */
208
+ const NON_VOICE = SKILLS.filter((s) => s.kind !== 'voice').slice().sort((a, b) => a.slug.localeCompare(b.slug));
209
+ const VOICE = SKILLS.filter((s) => s.kind === 'voice').slice().sort((a, b) => a.slug.localeCompare(b.slug));
210
+ export const SKILLS_SORTED = [...NON_VOICE, ...VOICE];
211
+ //# sourceMappingURL=editorial-skills-catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorial-skills-catalogue.js","sourceRoot":"","sources":["../../src/lib/editorial-skills-catalogue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Phase 23f — studio override-render helper.
3
+ *
4
+ * Page renderers consult the override resolver at the top of each
5
+ * render. When a `templates/<name>.ts` file exists in the project's
6
+ * `.deskwork/`, the renderer loads it and delegates the entire render
7
+ * to the override module's `default` export. The default export is
8
+ * called with the same arguments as the built-in renderer.
9
+ *
10
+ * The override contract:
11
+ * - Module exports a `default` function.
12
+ * - Function signature must match the built-in renderer (the type
13
+ * parameter `Args` here makes that explicit at the call site).
14
+ * - Function returns either a string (sync renderers) or a
15
+ * Promise<string> (async renderers). Both are awaited in
16
+ * `runOverride`.
17
+ *
18
+ * If the override exists but its `default` export is missing or not
19
+ * a function, we throw a descriptive error rather than falling back
20
+ * to the built-in renderer. Operators get a loud, actionable failure
21
+ * instead of a silent miss.
22
+ */
23
+ import type { OverrideResolver } from '@deskwork/core/overrides';
24
+ import type { StudioContext } from '../routes/api.ts';
25
+ /**
26
+ * Return a resolver for `ctx`. When the context already carries one
27
+ * (production boot), reuse it; otherwise build a fresh resolver from
28
+ * `ctx.projectRoot`. The result is always a real OverrideResolver —
29
+ * there is no skip path.
30
+ */
31
+ export declare function getResolver(ctx: StudioContext): OverrideResolver;
32
+ /**
33
+ * Load and execute a templates override.
34
+ *
35
+ * `name` is the template basename (no extension). When no override is
36
+ * registered, returns `null` — the caller proceeds with its built-in
37
+ * renderer. When an override IS registered, the module is dynamically
38
+ * imported and its `default` export is called with `args`.
39
+ */
40
+ export declare function runTemplateOverride<Args extends readonly unknown[]>(ctx: StudioContext, name: string, args: Args): Promise<string | null>;
41
+ //# sourceMappingURL=override-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"override-render.d.ts","sourceRoot":"","sources":["../../src/lib/override-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,gBAAgB,CAGhE;AAYD;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,SAAS,SAAS,OAAO,EAAE,EACvE,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Phase 23f — studio override-render helper.
3
+ *
4
+ * Page renderers consult the override resolver at the top of each
5
+ * render. When a `templates/<name>.ts` file exists in the project's
6
+ * `.deskwork/`, the renderer loads it and delegates the entire render
7
+ * to the override module's `default` export. The default export is
8
+ * called with the same arguments as the built-in renderer.
9
+ *
10
+ * The override contract:
11
+ * - Module exports a `default` function.
12
+ * - Function signature must match the built-in renderer (the type
13
+ * parameter `Args` here makes that explicit at the call site).
14
+ * - Function returns either a string (sync renderers) or a
15
+ * Promise<string> (async renderers). Both are awaited in
16
+ * `runOverride`.
17
+ *
18
+ * If the override exists but its `default` export is missing or not
19
+ * a function, we throw a descriptive error rather than falling back
20
+ * to the built-in renderer. Operators get a loud, actionable failure
21
+ * instead of a silent miss.
22
+ */
23
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
24
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
25
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
26
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
27
+ });
28
+ }
29
+ return path;
30
+ };
31
+ import { createOverrideResolver } from '@deskwork/core/overrides';
32
+ /**
33
+ * Return a resolver for `ctx`. When the context already carries one
34
+ * (production boot), reuse it; otherwise build a fresh resolver from
35
+ * `ctx.projectRoot`. The result is always a real OverrideResolver —
36
+ * there is no skip path.
37
+ */
38
+ export function getResolver(ctx) {
39
+ if (ctx.resolver)
40
+ return ctx.resolver;
41
+ return createOverrideResolver(ctx.projectRoot);
42
+ }
43
+ /**
44
+ * Discriminator for a module's default export. Narrows `unknown` to
45
+ * `(...args: Args) => string | Promise<string>` without `as`-casting.
46
+ */
47
+ function isOverrideRenderer(value) {
48
+ return typeof value === 'function';
49
+ }
50
+ /**
51
+ * Load and execute a templates override.
52
+ *
53
+ * `name` is the template basename (no extension). When no override is
54
+ * registered, returns `null` — the caller proceeds with its built-in
55
+ * renderer. When an override IS registered, the module is dynamically
56
+ * imported and its `default` export is called with `args`.
57
+ */
58
+ export async function runTemplateOverride(ctx, name, args) {
59
+ const resolver = getResolver(ctx);
60
+ const path = resolver.template(name);
61
+ if (path === null)
62
+ return null;
63
+ // Dynamic import of an absolute file path. The plugin's runtime tsx
64
+ // loader (the studio is always invoked through tsx — see the
65
+ // server.ts shebang) makes `.ts` imports work at runtime.
66
+ const mod = await import(__rewriteRelativeImportExtension(path));
67
+ if (typeof mod !== 'object' || mod === null) {
68
+ throw new Error(`template override at ${path} did not export a module object`);
69
+ }
70
+ const fn = Reflect.get(mod, 'default');
71
+ if (!isOverrideRenderer(fn)) {
72
+ throw new Error(`template override at ${path} must export a 'default' function (got ${typeof fn})`);
73
+ }
74
+ const out = await fn(...args);
75
+ if (typeof out !== 'string') {
76
+ throw new Error(`template override at ${path} returned a ${typeof out}; expected string`);
77
+ }
78
+ return out;
79
+ }
80
+ //# sourceMappingURL=override-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"override-render.js","sourceRoot":"","sources":["../../src/lib/override-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;;;;;;;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGlE;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAkB;IAC5C,IAAI,GAAG,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,QAAQ,CAAC;IACtC,OAAO,sBAAsB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CACzB,KAAc;IAEd,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAkB,EAClB,IAAY,EACZ,IAAU;IAEV,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE/B,oEAAoE;IACpE,6DAA6D;IAC7D,0DAA0D;IAC1D,MAAM,GAAG,GAAY,MAAM,MAAM,kCAAC,IAAI,EAAC,CAAC;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,iCAAiC,CAC9D,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,kBAAkB,CAAO,EAAE,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,0CAA0C,OAAO,EAAE,GAAG,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,eAAe,OAAO,GAAG,mBAAmB,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}