read-positioning.mjs
67 lines 2.2 KB
Raw
sha256:94ec65bd2b200240ac785a97cf14c5db066832bd608a24d6a9c151f17b918b02 feat(calendar): hosted bridge/gateway route parity and time… Human minor ⚠ breaking 14 hours ago
1 /**
2 * Skill: read-positioning
3 *
4 * Returns the current positioning + messaging outline for one of the three projects.
5 * Used by script-writer, blog-seo, social-poster, and newsletter agents to ensure
6 * every asset reflects the latest positioning, not a stale prior version.
7 *
8 * Vault path convention (defaults to the 2026-04 outline; pass `slug` to read another):
9 * vault/projects/<project>/outlines/positioning-and-messaging-2026-04.md
10 *
11 * @param {ReturnType<import('./hub-client.mjs').createHubClient>} hub
12 * @param {{ project: 'born-free' | 'store-free' | 'knowtation', slug?: string }} args
13 * @returns {Promise<{ path: string, frontmatter: object, body: string }>}
14 */
15 import { assertProject } from './hub-client.mjs';
16
17 const DEFAULT_SLUG = 'positioning-and-messaging-2026-04';
18
19 export async function readPositioning(hub, args) {
20 const project = assertProject(args.project);
21 const slug = sanitizeSlug(args.slug ?? DEFAULT_SLUG);
22 const path = `projects/${project}/outlines/${slug}.md`;
23
24 let note;
25 try {
26 note = await hub.getNote(path);
27 } catch (e) {
28 if (e.status === 404) {
29 throw Object.assign(
30 new Error(
31 `positioning_missing: vault/${path} does not exist on the Hub. ` +
32 `Either pass a different slug, or create the outline before running agents for ${project}.`
33 ),
34 { code: 'POSITIONING_MISSING', project, path, slug, cause: e }
35 );
36 }
37 throw e;
38 }
39
40 return {
41 path: note.path ?? path,
42 frontmatter: note.frontmatter ?? {},
43 body: typeof note.body === 'string' ? note.body : '',
44 };
45 }
46
47 /**
48 * Reject path traversal attempts. Slugs may contain a–z, 0–9, hyphens, underscores.
49 * Allowing `/` or `..` here would let a misbehaving agent read arbitrary vault paths
50 * and bypass project isolation.
51 * @param {string} slug
52 * @returns {string}
53 */
54 function sanitizeSlug(slug) {
55 if (typeof slug !== 'string' || !slug.trim()) {
56 throw Object.assign(new Error('invalid_slug: slug must be a non-empty string'), {
57 code: 'INVALID_SLUG',
58 });
59 }
60 if (!/^[a-z0-9][a-z0-9_-]*$/i.test(slug)) {
61 throw Object.assign(
62 new Error(`invalid_slug: '${slug}' contains illegal chars; allowed: a-z, 0-9, -, _`),
63 { code: 'INVALID_SLUG', slug }
64 );
65 }
66 return slug;
67 }
File History 1 commit
sha256:94ec65bd2b200240ac785a97cf14c5db066832bd608a24d6a9c151f17b918b02 feat(calendar): hosted bridge/gateway route parity and time… Human minor 14 hours ago