daedalion 0.0.1

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.
@@ -0,0 +1,183 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { ensureDir } from '../utils.js';
4
+
5
+ export function generateTools(spec, outputDir, config = {}, options = {}) {
6
+ const tools = extractTools(spec);
7
+ const language = config.tools?.language || 'python';
8
+ const generated = [];
9
+
10
+ for (const tool of tools) {
11
+ const stubResult = generateToolStub(tool, spec, outputDir, language, options);
12
+ generated.push(stubResult);
13
+ }
14
+
15
+ return generated;
16
+ }
17
+
18
+ function extractTools(spec) {
19
+ const tools = [];
20
+ const frontmatter = spec.frontmatter || {};
21
+
22
+ if (frontmatter.tools && Array.isArray(frontmatter.tools)) {
23
+ for (const tool of frontmatter.tools) {
24
+ if (typeof tool === 'string') {
25
+ tools.push({ name: tool });
26
+ } else if (tool.name) {
27
+ tools.push(tool);
28
+ }
29
+ }
30
+ }
31
+
32
+ return tools;
33
+ }
34
+
35
+ function mapPythonType(type) {
36
+ const typeMap = {
37
+ 'string': 'str',
38
+ 'String': 'str',
39
+ 'number': 'int | float',
40
+ 'Number': 'int | float',
41
+ 'object': 'dict',
42
+ 'Object': 'dict',
43
+ 'array': 'Array',
44
+ 'Array': 'Array',
45
+ 'boolean': 'bool',
46
+ 'Boolean': 'bool'
47
+ };
48
+
49
+ if (typeof type !== 'string') {
50
+ return 'Any';
51
+ }
52
+
53
+ return typeMap[type] || type || 'Any';
54
+ }
55
+
56
+ function getAllRequirementIds(spec) {
57
+ return spec.requirements.map((req, i) => ({
58
+ id: `REQ-${String(i + 1).padStart(3, '0')}`,
59
+ name: req.name
60
+ }));
61
+ }
62
+
63
+ function filterRelevantRequirements(spec, tool) {
64
+ if (!tool.requirements || !Array.isArray(tool.requirements)) {
65
+ return getAllRequirementIds(spec).slice(0, 2);
66
+ }
67
+
68
+ const allReqs = getAllRequirementIds(spec);
69
+ const toolReqIds = tool.requirements.map(r => {
70
+ if (typeof r === 'string' && r.startsWith('REQ-')) {
71
+ return r;
72
+ }
73
+ const idx = parseInt(r) || 1;
74
+ return `REQ-${String(idx).padStart(3, '0')}`;
75
+ });
76
+
77
+ return allReqs.filter(req => toolReqIds.includes(req.id));
78
+ }
79
+
80
+ function generatePythonStub(tool, spec, filePath, options) {
81
+ const toolReqs = filterRelevantRequirements(spec, tool);
82
+ const toolReqIds = toolReqs.map(r => r.id);
83
+
84
+ let content = `"""
85
+ ${tool.description || tool.name}
86
+ Requirements: ${toolReqIds.join(', ') || 'TODO'}
87
+ """
88
+
89
+ `;
90
+
91
+ content += `def ${tool.name}(${generatePythonSignature(tool)}) -> ${mapPythonType(tool.outputs?.[0] || 'dict')}:\n`;
92
+ content += ` """
93
+ ${tool.description || 'TODO: Add description'}
94
+ \n`;
95
+
96
+ if (tool.inputs && tool.inputs.length > 0) {
97
+ content += ` Args:\n`;
98
+ for (const input of tool.inputs) {
99
+ content += ` ${input.name}: ${mapPythonType(input.type || 'Any')} - ${input.description || 'TODO'}\n`;
100
+ }
101
+ }
102
+
103
+ if (tool.outputs && tool.outputs.length > 0) {
104
+ content += ` \n Returns:\n`;
105
+ content += ` ${mapPythonType(tool.outputs[0] || 'dict')}\n`;
106
+ }
107
+
108
+ if (toolReqIds.length > 0) {
109
+ content += ` \n Spec References:\n`;
110
+ for (const ref of toolReqIds) {
111
+ content += ` - ${ref}\n`;
112
+ }
113
+ }
114
+
115
+ content += ` """\n`;
116
+ content += ` raise NotImplementedError("Implement ${tool.name} logic")\n`;
117
+
118
+ if (options.dryRun) {
119
+ return { path: filePath, content };
120
+ }
121
+
122
+ ensureDir(filePath);
123
+ writeFileSync(filePath, content, 'utf-8');
124
+ return { path: filePath, content };
125
+ }
126
+
127
+ function generateToolStub(tool, spec, outputDir, language, options) {
128
+ const toolsDir = join(outputDir, 'tools');
129
+ const ext = getLanguageExt(language);
130
+ const toolPath = join(toolsDir, `${tool.name.toLowerCase()}${ext}`);
131
+
132
+ if (language === 'python') {
133
+ return generatePythonStub(tool, spec, toolPath, options);
134
+ } else if (language === 'javascript') {
135
+ return generateJavaScriptStub(tool, spec, toolPath, options);
136
+ }
137
+ }
138
+
139
+ function generatePythonSignature(tool) {
140
+ if (!tool.inputs || tool.inputs.length === 0) {
141
+ return '';
142
+ }
143
+
144
+ return tool.inputs.map(input => `${input.name}: ${mapPythonType(input.type)}`).join(', ');
145
+ }
146
+
147
+ function generateJSSignature(tool) {
148
+ if (!tool.inputs || tool.inputs.length === 0) {
149
+ return '{}';
150
+ }
151
+
152
+ return `{ ${tool.inputs.map(input => `${input.name}`).join(', ')} }`;
153
+ }
154
+
155
+ function getLanguageExt(language) {
156
+ const exts = { python: '.py', javascript: '.js' };
157
+ return exts[language] || '.py';
158
+ }
159
+
160
+ function mapJSType(type) {
161
+ const typeMap = {
162
+ 'string': 'string',
163
+ 'String': 'string',
164
+ 'number': 'number',
165
+ 'Number': 'number',
166
+ 'object': 'Object',
167
+ 'Object': 'Object',
168
+ 'array': 'Array',
169
+ 'Array': 'Array',
170
+ 'boolean': 'boolean',
171
+ 'Boolean': 'boolean'
172
+ };
173
+
174
+ if (typeof type !== 'string') {
175
+ return 'any';
176
+ }
177
+
178
+ return typeMap[type] || type || 'any';
179
+ }
180
+
181
+ function camelCase(str) {
182
+ return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase());
183
+ }
@@ -0,0 +1,65 @@
1
+ import { ensureDir } from '../utils.js';
2
+ import { writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ export function generateWorkflow(config, outputDir, options = {}) {
6
+ const workflowPath = join(outputDir, 'workflows', 'daedalion.yml');
7
+
8
+ const content = config.ci?.auto_commit
9
+ ? generateAutoCommitWorkflow(config)
10
+ : generateValidateOnlyWorkflow(config);
11
+
12
+ if (options.dryRun) {
13
+ return { path: workflowPath, content };
14
+ }
15
+
16
+ ensureDir(workflowPath);
17
+ writeFileSync(workflowPath, content);
18
+ return { path: workflowPath, content };
19
+ }
20
+
21
+ function generateValidateOnlyWorkflow(config) {
22
+ return `name: Daedalion
23
+ on:
24
+ push:
25
+ paths: ['openspec/**']
26
+ pull_request:
27
+ paths: ['openspec/**']
28
+ jobs:
29
+ validate:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-node@v4
34
+ with:
35
+ node-version: '20'
36
+ - run: npm install -g daedalion
37
+ - run: daedalion build --dry-run
38
+ - run: daedalion validate
39
+ `;
40
+ }
41
+
42
+ function generateAutoCommitWorkflow(config) {
43
+ const commitMessage = config.ci?.commit_message || 'chore: regenerate agents from specs';
44
+
45
+ return `name: Daedalion
46
+ on:
47
+ push:
48
+ branches: [main]
49
+ paths: ['openspec/**']
50
+ jobs:
51
+ build:
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ - uses: actions/setup-node@v4
56
+ with:
57
+ node-version: '20'
58
+ - run: npm install -g daedalion
59
+ - run: daedalion build
60
+ - run: daedalion validate
61
+ - uses: stefanzweifel/git-auto-commit-action@v5
62
+ with:
63
+ commit_message: '${commitMessage}'
64
+ `;
65
+ }
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { init } from './commands/init.js';
2
+ export { build } from './commands/build.js';
3
+ export { validate } from './commands/validate.js';
4
+ export { clean } from './commands/clean.js';
5
+ export { loadConfig } from './config.js';
6
+ export { parseSpec } from './parsers/spec.js';
7
+ export { parseProposal } from './parsers/proposal.js';
8
+ export { parseTasks } from './parsers/tasks.js';
9
+ export { generateSkill } from './generators/skill.js';
10
+ export { generateAgent } from './generators/agent.js';
11
+ export { generatePrompt } from './generators/prompt.js';
12
+ export { generateWorkflow } from './generators/workflow.js';
13
+ export { generateInstructions } from './generators/instructions.js';
14
+ export { generateTools } from './generators/tools.js';
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename, dirname } from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ export function parseProposal(proposalPath) {
6
+ const content = readFileSync(proposalPath, 'utf-8');
7
+ const { data: frontmatter, content: body } = matter(content);
8
+
9
+ const changeName = basename(dirname(proposalPath));
10
+ const title = extractTitle(body);
11
+ const why = extractSection(body, 'Why');
12
+ const what = extractSection(body, 'What');
13
+
14
+ return {
15
+ path: proposalPath,
16
+ changeName,
17
+ title,
18
+ frontmatter,
19
+ why,
20
+ what
21
+ };
22
+ }
23
+
24
+ function extractTitle(content) {
25
+ const match = content.match(/^#\s+(.+)$/m);
26
+ return match ? match[1].trim() : 'Untitled Proposal';
27
+ }
28
+
29
+ function extractSection(content, sectionName) {
30
+ const lines = content.split('\n');
31
+ let inSection = false;
32
+ let sectionContent = [];
33
+
34
+ for (const line of lines) {
35
+ // Match ## Why or ## What (case insensitive)
36
+ const sectionMatch = line.match(/^##\s+(.+)$/);
37
+ if (sectionMatch) {
38
+ if (sectionMatch[1].toLowerCase() === sectionName.toLowerCase()) {
39
+ inSection = true;
40
+ continue;
41
+ } else if (inSection) {
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (inSection && line.trim()) {
47
+ sectionContent.push(line.trim());
48
+ }
49
+ }
50
+
51
+ return sectionContent.join('\n') || null;
52
+ }
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename, dirname } from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ export function parseSpec(specPath) {
6
+ const content = readFileSync(specPath, 'utf-8');
7
+ const { data: frontmatter, content: body } = matter(content);
8
+
9
+ const domain = basename(dirname(specPath));
10
+ const title = extractTitle(body);
11
+ const requirements = extractRequirements(body);
12
+
13
+ return {
14
+ path: specPath,
15
+ domain,
16
+ title,
17
+ frontmatter,
18
+ requirements
19
+ };
20
+ }
21
+
22
+ function extractTitle(content) {
23
+ const match = content.match(/^#\s+(.+)$/m);
24
+ return match ? match[1].trim() : 'Untitled Specification';
25
+ }
26
+
27
+ function extractRequirements(content) {
28
+ const requirements = [];
29
+ const lines = content.split('\n');
30
+
31
+ let currentRequirement = null;
32
+ let currentScenario = null;
33
+ let inRequirementDescription = false;
34
+
35
+ for (let i = 0; i < lines.length; i++) {
36
+ const line = lines[i];
37
+
38
+ // Match ### Requirement: Name
39
+ const reqMatch = line.match(/^###\s+Requirement:\s*(.+)$/i);
40
+ if (reqMatch) {
41
+ if (currentRequirement) {
42
+ if (currentScenario) {
43
+ currentRequirement.scenarios.push(currentScenario);
44
+ }
45
+ requirements.push(currentRequirement);
46
+ }
47
+ currentRequirement = {
48
+ name: reqMatch[1].trim(),
49
+ description: '',
50
+ scenarios: []
51
+ };
52
+ currentScenario = null;
53
+ inRequirementDescription = true;
54
+ continue;
55
+ }
56
+
57
+ // Match #### Scenario: Name
58
+ const scenarioMatch = line.match(/^####\s+Scenario:\s*(.+)$/i);
59
+ if (scenarioMatch && currentRequirement) {
60
+ if (currentScenario) {
61
+ currentRequirement.scenarios.push(currentScenario);
62
+ }
63
+ currentScenario = {
64
+ name: scenarioMatch[1].trim(),
65
+ steps: []
66
+ };
67
+ inRequirementDescription = false;
68
+ continue;
69
+ }
70
+
71
+ // Stop description collection at any heading
72
+ if (line.match(/^#{1,4}\s+/)) {
73
+ inRequirementDescription = false;
74
+ continue;
75
+ }
76
+
77
+ // Collect requirement description
78
+ if (inRequirementDescription && currentRequirement && line.trim()) {
79
+ if (currentRequirement.description) {
80
+ currentRequirement.description += ' ' + line.trim();
81
+ } else {
82
+ currentRequirement.description = line.trim();
83
+ }
84
+ continue;
85
+ }
86
+
87
+ // Collect scenario steps (lines starting with -)
88
+ if (currentScenario && line.match(/^\s*-\s+/)) {
89
+ const step = line.replace(/^\s*-\s+/, '').trim();
90
+ if (step) {
91
+ currentScenario.steps.push(step);
92
+ }
93
+ }
94
+ }
95
+
96
+ // Push last requirement/scenario
97
+ if (currentRequirement) {
98
+ if (currentScenario) {
99
+ currentRequirement.scenarios.push(currentScenario);
100
+ }
101
+ requirements.push(currentRequirement);
102
+ }
103
+
104
+ return requirements;
105
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+
3
+ export function parseTasks(tasksPath, maxItems = 10) {
4
+ if (!existsSync(tasksPath)) {
5
+ return { groups: [], items: [], hasMore: false };
6
+ }
7
+
8
+ const content = readFileSync(tasksPath, 'utf-8');
9
+ return summarizeTasks(content, maxItems);
10
+ }
11
+
12
+ export function summarizeTasks(content, maxItems = 10) {
13
+ const lines = content.split('\n');
14
+ const groups = [];
15
+ const items = [];
16
+ let currentGroup = null;
17
+ let totalItems = 0;
18
+
19
+ for (const line of lines) {
20
+ // Match headings as groups
21
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
22
+ if (headingMatch) {
23
+ currentGroup = headingMatch[2].trim();
24
+ if (!groups.includes(currentGroup)) {
25
+ groups.push(currentGroup);
26
+ }
27
+ continue;
28
+ }
29
+
30
+ // Match top-level task items (- [ ] or - [x] or just -)
31
+ const taskMatch = line.match(/^-\s+(\[[ x]\])?\s*(.+)$/);
32
+ if (taskMatch && !line.match(/^\s{2,}-/)) {
33
+ totalItems++;
34
+ if (items.length < maxItems) {
35
+ const taskText = taskMatch[2].trim();
36
+ items.push(taskText);
37
+ }
38
+ }
39
+ }
40
+
41
+ return {
42
+ groups,
43
+ items,
44
+ hasMore: totalItems > maxItems
45
+ };
46
+ }
package/src/utils.js ADDED
@@ -0,0 +1,51 @@
1
+ import { mkdirSync, existsSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ export function ensureDir(filePath) {
5
+ const dir = dirname(filePath);
6
+ if (!existsSync(dir)) {
7
+ mkdirSync(dir, { recursive: true });
8
+ }
9
+ }
10
+
11
+ export function extractFirstHeading(content) {
12
+ const match = content.match(/^#\s+(.+)$/m);
13
+ return match ? match[1].trim() : null;
14
+ }
15
+
16
+ export function extractDescription(content) {
17
+ const lines = content.split('\n');
18
+ let inFrontmatter = false;
19
+ let description = [];
20
+
21
+ for (const line of lines) {
22
+ if (line.trim() === '---') {
23
+ if (inFrontmatter) {
24
+ inFrontmatter = false;
25
+ continue;
26
+ }
27
+ inFrontmatter = true;
28
+ continue;
29
+ }
30
+
31
+ if (inFrontmatter) continue;
32
+
33
+ if (line.startsWith('#')) continue;
34
+
35
+ if (line.trim() && !line.startsWith('##')) {
36
+ description.push(line.trim());
37
+ if (description.length >= 2) break;
38
+ }
39
+
40
+ if (line.startsWith('##')) break;
41
+ }
42
+
43
+ return description.join(' ').slice(0, 200) || null;
44
+ }
45
+
46
+ export function slugify(text) {
47
+ return text
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, '-')
50
+ .replace(/^-|-$/g, '');
51
+ }
package/src/version.js ADDED
@@ -0,0 +1,60 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Read version from package.json
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
11
+ export const VERSION = pkg.version;
12
+
13
+ /**
14
+ * Get version string with commit hash for dev builds
15
+ * @returns {string} e.g., "0.0.1" or "0.0.1-dev+abc1234"
16
+ */
17
+ export function getVersionString() {
18
+ const commitHash = getCommitHash();
19
+ if (commitHash && !isReleaseBuild()) {
20
+ return `${VERSION}-dev+${commitHash}`;
21
+ }
22
+ return VERSION;
23
+ }
24
+
25
+ /**
26
+ * Get short commit hash
27
+ * @returns {string|null}
28
+ */
29
+ function getCommitHash() {
30
+ try {
31
+ return execSync('git rev-parse --short HEAD', {
32
+ encoding: 'utf-8',
33
+ stdio: ['pipe', 'pipe', 'ignore']
34
+ }).trim();
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Check if this is a release build (tagged or npm published)
42
+ * @returns {boolean}
43
+ */
44
+ function isReleaseBuild() {
45
+ // Check if running from node_modules (npm installed)
46
+ if (__dirname.includes('node_modules')) {
47
+ return true;
48
+ }
49
+
50
+ // Check if current commit is tagged
51
+ try {
52
+ execSync('git describe --exact-match --tags HEAD', {
53
+ encoding: 'utf-8',
54
+ stdio: ['pipe', 'pipe', 'ignore']
55
+ });
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
@@ -0,0 +1,18 @@
1
+ version: 1
2
+ target: github
3
+ openspec: ./openspec
4
+ output: ./.github
5
+
6
+ # Optional
7
+ # project_name: my-project
8
+ # constitution: ./CONSTITUTION.md
9
+
10
+ # CI behavior
11
+ ci:
12
+ auto_commit: false
13
+ commit_message: 'chore: regenerate agents from specs'
14
+
15
+ # Agent output configuration
16
+ agents:
17
+ target: ide # 'ide' or 'sdk'
18
+ tools: null # null to use IDE defaults, or array of custom tool names
@@ -0,0 +1,13 @@
1
+ # Add Personalized Dashboard
2
+
3
+ ## Why
4
+
5
+ Users need a central place to view their activity and preferences. Currently, they must navigate through multiple pages to find relevant information.
6
+
7
+ ## What
8
+
9
+ Create a personalized dashboard that displays:
10
+ - Recent activity summary
11
+ - Quick actions
12
+ - User preferences
13
+ - Notifications
@@ -0,0 +1,19 @@
1
+ # Tasks
2
+
3
+ ## Setup
4
+
5
+ - [ ] Create dashboard component structure
6
+ - [ ] Set up routing for /dashboard
7
+
8
+ ## Implementation
9
+
10
+ - [ ] Add activity summary widget
11
+ - [ ] Add quick actions panel
12
+ - [ ] Add preferences section
13
+ - [ ] Add notifications component
14
+
15
+ ## Testing
16
+
17
+ - [ ] Write unit tests for dashboard components
18
+ - [ ] Write integration tests for data fetching
19
+ - [ ] Manual QA testing
@@ -0,0 +1,15 @@
1
+ # Project Overview
2
+
3
+ This project uses OpenSpec for specification-driven development.
4
+
5
+ ## Goals
6
+
7
+ - Define clear specifications before implementation
8
+ - Use agents and skills to guide development
9
+ - Maintain alignment between specs and code
10
+
11
+ ## Conventions
12
+
13
+ - All specifications follow OpenSpec format
14
+ - Changes go through proposal → review → implementation workflow
15
+ - Generated artifacts are kept in sync via Daedalion
@@ -0,0 +1,37 @@
1
+ # Example Specification
2
+
3
+ This is an example specification demonstrating the OpenSpec format.
4
+
5
+ ## Requirements
6
+
7
+ ### Requirement: User Greeting
8
+
9
+ The system SHALL greet users by name when they log in.
10
+
11
+ #### Scenario: Known user logs in
12
+
13
+ - GIVEN a user with name "Alice" exists
14
+ - WHEN Alice logs in
15
+ - THEN the system displays "Welcome, Alice!"
16
+
17
+ #### Scenario: Anonymous user
18
+
19
+ - GIVEN a user is not logged in
20
+ - WHEN they access the homepage
21
+ - THEN the system displays "Welcome, Guest!"
22
+
23
+ ### Requirement: Session Management
24
+
25
+ The system SHALL maintain user sessions for 24 hours.
26
+
27
+ #### Scenario: Active session
28
+
29
+ - GIVEN a user has an active session
30
+ - WHEN they make a request within 24 hours
31
+ - THEN the session remains valid
32
+
33
+ #### Scenario: Expired session
34
+
35
+ - GIVEN a user's session is older than 24 hours
36
+ - WHEN they make a request
37
+ - THEN they are redirected to login