pluribus-context 0.2.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/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/pluribus.js +108 -0
- package/docs/composable-contexts.md +124 -0
- package/docs/openclaw-integration.md +145 -0
- package/docs/release-checklist.md +91 -0
- package/docs/remote-composable-context-imports.md +233 -0
- package/examples/claude-cowork/.cursorrules +82 -0
- package/examples/claude-cowork/pluribus.md +137 -0
- package/examples/composable-contexts/pluribus.md +29 -0
- package/examples/composable-contexts/shared/security-constraints.md +6 -0
- package/examples/composable-contexts/shared/team-context.md +10 -0
- package/examples/openclaw/AGENTS.md +134 -0
- package/examples/openclaw/CLAUDE.md +132 -0
- package/examples/openclaw/pluribus.md +99 -0
- package/package.json +52 -0
- package/spec/context-format.md +356 -0
- package/spec/skills-format.md +325 -0
- package/src/commands/init.js +153 -0
- package/src/commands/sync.js +213 -0
- package/src/commands/validate.js +146 -0
- package/src/commands/watch.js +111 -0
- package/src/index.js +11 -0
- package/src/skills/built-in.js +345 -0
- package/src/utils/args.js +35 -0
- package/src/utils/imports.js +690 -0
- package/src/utils/parser.js +74 -0
- package/src/utils/renderer.js +123 -0
- package/src/utils/version.js +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pluribus.md parser
|
|
3
|
+
* Splits a markdown file into top-level sections (# Heading)
|
|
4
|
+
* Returns an object: { sectionName: sectionBody }
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} content
|
|
9
|
+
* @returns {Record<string, string>}
|
|
10
|
+
*/
|
|
11
|
+
export function parsePluribusFile(content) {
|
|
12
|
+
// Strip BOM if present
|
|
13
|
+
const cleaned = content.replace(/^\uFEFF/, '')
|
|
14
|
+
|
|
15
|
+
const sections = {}
|
|
16
|
+
const lines = cleaned.split(/\r?\n/)
|
|
17
|
+
|
|
18
|
+
let currentSection = null
|
|
19
|
+
let currentLines = []
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.startsWith('# ') && !line.startsWith('## ')) {
|
|
23
|
+
// Save previous section
|
|
24
|
+
if (currentSection !== null) {
|
|
25
|
+
sections[currentSection] = currentLines.join('\n').trim()
|
|
26
|
+
}
|
|
27
|
+
currentSection = line.slice(2).trim()
|
|
28
|
+
currentLines = []
|
|
29
|
+
} else {
|
|
30
|
+
if (currentSection !== null) {
|
|
31
|
+
currentLines.push(line)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Save last section
|
|
37
|
+
if (currentSection !== null) {
|
|
38
|
+
sections[currentSection] = currentLines.join('\n').trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return sections
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the slug form of a section name (lowercase, spaces→hyphens)
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function slugify(name) {
|
|
50
|
+
return name.toLowerCase().replace(/\s+/g, '-')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Required sections per the spec */
|
|
54
|
+
export const REQUIRED_SECTIONS = ['Identity', 'Stack', 'Conventions', 'Goals', 'Constraints']
|
|
55
|
+
|
|
56
|
+
/** Optional sections per the spec */
|
|
57
|
+
export const OPTIONAL_SECTIONS = ['Examples', 'Anti-patterns', 'Workflow', 'Context', 'Team']
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a parsed pluribus.md sections object.
|
|
61
|
+
* @param {Record<string, string>} sections
|
|
62
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
63
|
+
*/
|
|
64
|
+
export function validateSections(sections) {
|
|
65
|
+
const errors = []
|
|
66
|
+
for (const required of REQUIRED_SECTIONS) {
|
|
67
|
+
if (!sections[required] || sections[required].trim() === '') {
|
|
68
|
+
errors.push(`Missing or empty required section: # ${required}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for duplicates (can't happen with our parser since later overwrites earlier, but let's be safe)
|
|
73
|
+
return { valid: errors.length === 0, errors }
|
|
74
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template renderer for Pluribus Template Syntax (PTS)
|
|
3
|
+
* Supports: {{variable}}, {{#if variable}} ... {{/if}}, {{pluribus.version}}, {{pluribus.date}}, {{pluribus.source}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { slugify } from './parser.js'
|
|
7
|
+
import { VERSION } from './version.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} template
|
|
11
|
+
* @param {Record<string, string>} sections — raw sections from parsePluribusFile
|
|
12
|
+
* @param {string} sourcePath
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function renderTemplate(template, sections, sourcePath) {
|
|
16
|
+
// Build a variables map: both exact name and slug → content
|
|
17
|
+
const vars = {}
|
|
18
|
+
|
|
19
|
+
for (const [name, body] of Object.entries(sections)) {
|
|
20
|
+
vars[name.toLowerCase()] = body
|
|
21
|
+
vars[slugify(name)] = body
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Built-in pluribus.* variables
|
|
25
|
+
const meta = {
|
|
26
|
+
'pluribus.version': VERSION,
|
|
27
|
+
'pluribus.date': new Date().toISOString().slice(0, 10),
|
|
28
|
+
'pluribus.source': sourcePath,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let result = template
|
|
32
|
+
|
|
33
|
+
// 1. Process {{#if variable}} ... {{/if}} blocks
|
|
34
|
+
result = result.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, inner) => {
|
|
35
|
+
const val = vars[key.trim().toLowerCase()] ?? vars[key.trim()]
|
|
36
|
+
if (val && val.trim() !== '') {
|
|
37
|
+
// Render inner (still apply variable substitution later)
|
|
38
|
+
return inner
|
|
39
|
+
}
|
|
40
|
+
return ''
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 2. Replace {{pluribus.*}} meta vars
|
|
44
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
45
|
+
result = result.replaceAll(`{{${key}}}`, value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Replace {{variable}} with section content
|
|
49
|
+
result = result.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
50
|
+
const k = key.trim().toLowerCase()
|
|
51
|
+
if (vars[k] !== undefined) return vars[k]
|
|
52
|
+
if (vars[key.trim()] !== undefined) return vars[key.trim()]
|
|
53
|
+
// Unknown variable: emit warning but keep the placeholder commented out
|
|
54
|
+
return `<!-- pluribus: unknown variable "${key.trim()}" -->`
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return result.trim() + '\n'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a skill file into its sections.
|
|
62
|
+
* @param {string} content
|
|
63
|
+
* @returns {{ output: string[], template: string, sections: { required: string[], optional: string[] }, meta: Record<string, string> }}
|
|
64
|
+
*/
|
|
65
|
+
export function parseSkillFile(content) {
|
|
66
|
+
// Split into top-level sections
|
|
67
|
+
const rawSections = {}
|
|
68
|
+
const lines = content.split(/\r?\n/)
|
|
69
|
+
let currentSection = null
|
|
70
|
+
let currentLines = []
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (line.startsWith('# ') && !line.startsWith('## ')) {
|
|
74
|
+
if (currentSection !== null) {
|
|
75
|
+
rawSections[currentSection] = currentLines.join('\n').trim()
|
|
76
|
+
}
|
|
77
|
+
currentSection = line.slice(2).trim()
|
|
78
|
+
currentLines = []
|
|
79
|
+
} else {
|
|
80
|
+
if (currentSection !== null) {
|
|
81
|
+
currentLines.push(line)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (currentSection !== null) {
|
|
86
|
+
rawSections[currentSection] = currentLines.join('\n').trim()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse # Output — list of file paths
|
|
90
|
+
const outputRaw = rawSections['Output'] || ''
|
|
91
|
+
const output = outputRaw
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.map((l) => l.trim())
|
|
94
|
+
.filter((l) => l.length > 0 && !l.startsWith('#'))
|
|
95
|
+
|
|
96
|
+
// Parse # Sections — required: ..., optional: ...
|
|
97
|
+
const sectionsRaw = rawSections['Sections'] || ''
|
|
98
|
+
const requiredMatch = sectionsRaw.match(/required:\s*(.+)/)
|
|
99
|
+
const optionalMatch = sectionsRaw.match(/optional:\s*(.+)/)
|
|
100
|
+
const requiredSections = requiredMatch
|
|
101
|
+
? requiredMatch[1].split(',').map((s) => s.trim())
|
|
102
|
+
: []
|
|
103
|
+
const optionalSections = optionalMatch
|
|
104
|
+
? optionalMatch[1].split(',').map((s) => s.trim())
|
|
105
|
+
: []
|
|
106
|
+
|
|
107
|
+
// Parse # Meta
|
|
108
|
+
const metaRaw = rawSections['Meta'] || ''
|
|
109
|
+
const meta = {}
|
|
110
|
+
for (const line of metaRaw.split(/\r?\n/)) {
|
|
111
|
+
const colonIdx = line.indexOf(':')
|
|
112
|
+
if (colonIdx !== -1) {
|
|
113
|
+
meta[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
output,
|
|
119
|
+
template: rawSections['Template'] || '',
|
|
120
|
+
sections: { required: requiredSections, optional: optionalSections },
|
|
121
|
+
meta,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.2.0'
|