@10kdevs/matha 0.1.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.
@@ -0,0 +1,266 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { writeAtomic } from '../storage/writer.js';
4
+ import { readJsonOrNull } from '../storage/reader.js';
5
+ import { getIntent, getRules } from '../brain/hippocampus.js';
6
+ import { CURRENT_SCHEMA_VERSION } from '../utils/schema-version.js';
7
+ import { refreshFromGit } from '../brain/cortex.js';
8
+ const REQUIRED_DIRS = [
9
+ '.matha/hippocampus',
10
+ '.matha/hippocampus/decisions',
11
+ '.matha/cerebellum',
12
+ '.matha/cerebellum/contracts',
13
+ '.matha/cortex',
14
+ '.matha/dopamine',
15
+ '.matha/dopamine/predictions',
16
+ '.matha/dopamine/actuals',
17
+ '.matha/sessions',
18
+ ];
19
+ const IGNORE_DIRS = new Set(['.matha', '.git', 'node_modules']);
20
+ export async function runInit(projectRoot = process.cwd(), deps) {
21
+ const ask = deps?.ask ?? defaultAsk;
22
+ const log = deps?.log ?? console.log;
23
+ const now = deps?.now ?? (() => new Date());
24
+ const seed = deps?.seed ?? null;
25
+ const created = [];
26
+ const skipped = [];
27
+ for (const relDir of REQUIRED_DIRS) {
28
+ const absDir = path.join(projectRoot, relDir);
29
+ const alreadyExists = await pathExists(absDir);
30
+ await fs.mkdir(absDir, { recursive: true });
31
+ (alreadyExists ? skipped : created).push(relDir);
32
+ }
33
+ // If seed provided, print what was parsed
34
+ if (seed) {
35
+ log('Parsed from file:');
36
+ log(` WHY: ${seed.why ?? 'not found'}`);
37
+ log(` RULES: ${seed.rules.length} found`);
38
+ log(` BOUNDARIES: ${seed.boundaries.length} found`);
39
+ log(` OWNER: ${seed.owner ?? 'not found'}`);
40
+ log('');
41
+ }
42
+ // WHY prompt — pre-fill with seed.why if available
43
+ const whyPrompt = seed?.why
44
+ ? `What problem does this project solve? (The WHY, not the features)\n [default: ${seed.why}]`
45
+ : 'What problem does this project solve? (The WHY, not the features)';
46
+ const whyRaw = await safePrompt(ask, whyPrompt);
47
+ const why = whyRaw.trim() || seed?.why || '';
48
+ // RULES — start with seed rules, then ask for more
49
+ let rules = [];
50
+ if (seed && seed.rules.length > 0) {
51
+ rules = [...seed.rules];
52
+ log(`Pre-filled ${seed.rules.length} rules from file.`);
53
+ }
54
+ const moreRules = await collectLines(ask, seed && seed.rules.length > 0
55
+ ? 'Add more business rules? (Enter one per line, empty line to finish)'
56
+ : 'What are the non-negotiable business rules? (Enter one per line, empty line to finish)');
57
+ rules = [...rules, ...moreRules];
58
+ // BOUNDARIES — start with seed boundaries, then ask for more
59
+ let boundaries = [];
60
+ if (seed && seed.boundaries.length > 0) {
61
+ boundaries = [...seed.boundaries];
62
+ log(`Pre-filled ${seed.boundaries.length} boundaries from file.`);
63
+ }
64
+ const moreBoundaries = await collectLines(ask, seed && seed.boundaries.length > 0
65
+ ? 'Add more boundaries? (Enter one per line, empty line to finish)'
66
+ : 'What does this project explicitly NOT do? (Enter one per line, empty line to finish)');
67
+ boundaries = [...boundaries, ...moreBoundaries];
68
+ // OWNER prompt — pre-fill with seed.owner if available
69
+ const ownerPrompt = seed?.owner
70
+ ? `Who owns this project? (name or team, press enter to skip)\n [default: ${seed.owner}]`
71
+ : 'Who owns this project? (name or team, press enter to skip)';
72
+ const ownerRaw = await safePrompt(ask, ownerPrompt);
73
+ const owner = ownerRaw.trim() ? ownerRaw.trim() : (seed?.owner ?? null);
74
+ const mathaDir = path.join(projectRoot, '.matha');
75
+ // Hippocampus writes (idempotent: write only if file missing)
76
+ const intentPath = path.join(mathaDir, 'hippocampus', 'intent.json');
77
+ const existingIntent = await getIntent(mathaDir);
78
+ if (existingIntent === null) {
79
+ await writeAtomic(intentPath, { why });
80
+ created.push('.matha/hippocampus/intent.json');
81
+ }
82
+ else {
83
+ skipped.push('.matha/hippocampus/intent.json');
84
+ }
85
+ const rulesPath = path.join(mathaDir, 'hippocampus', 'rules.json');
86
+ const existingRules = await getRules(mathaDir);
87
+ if (existingRules.length === 0 && !(await pathExists(rulesPath))) {
88
+ await writeAtomic(rulesPath, { rules });
89
+ created.push('.matha/hippocampus/rules.json');
90
+ }
91
+ else {
92
+ skipped.push('.matha/hippocampus/rules.json');
93
+ }
94
+ const boundariesPath = path.join(mathaDir, 'cortex', 'boundaries.json');
95
+ await writeIfMissing(boundariesPath, { boundaries }, created, skipped, projectRoot);
96
+ const ownershipPath = path.join(mathaDir, 'cortex', 'ownership.json');
97
+ await writeIfMissing(ownershipPath, { owner }, created, skipped, projectRoot);
98
+ const shape = await deriveShape(projectRoot, now);
99
+ const shapePath = path.join(mathaDir, 'cortex', 'shape.json');
100
+ await writeIfMissing(shapePath, shape, created, skipped, projectRoot);
101
+ const configPath = path.join(mathaDir, 'config.json');
102
+ const existingConfig = await readJsonOrNull(configPath);
103
+ if (existingConfig && !existingConfig.schema_version) {
104
+ await writeAtomic(configPath, {
105
+ ...existingConfig,
106
+ schema_version: CURRENT_SCHEMA_VERSION,
107
+ }, { overwrite: true });
108
+ }
109
+ await writeIfMissing(configPath, {
110
+ version: '0.1.0',
111
+ schema_version: CURRENT_SCHEMA_VERSION,
112
+ initialized_at: now().toISOString(),
113
+ project_root: projectRoot,
114
+ brain_dir: '.matha',
115
+ }, created, skipped, projectRoot);
116
+ log('matha init complete');
117
+ log(`created: ${created.length}`);
118
+ log(`skipped: ${skipped.length}`);
119
+ // Cortex refresh — analyse git history if available
120
+ try {
121
+ log('\nAnalysing git history...');
122
+ const snapshot = await refreshFromGit(projectRoot, mathaDir);
123
+ if (snapshot.commitCount > 0) {
124
+ const s = snapshot.summary;
125
+ log(`Cortex built — ${snapshot.fileCount} files classified ` +
126
+ `(${s.frozen} frozen, ${s.stable} stable, ${s.volatile} volatile, ${s.disposable} disposable)`);
127
+ }
128
+ else {
129
+ log('No git history found — cortex will build as commits accumulate');
130
+ }
131
+ }
132
+ catch {
133
+ log('No git history found — cortex will build as commits accumulate');
134
+ }
135
+ return {
136
+ projectRoot,
137
+ brainDir: '.matha',
138
+ created,
139
+ skipped,
140
+ };
141
+ }
142
+ async function deriveShape(projectRoot, now) {
143
+ const directories = await listTopLevelDirectories(projectRoot);
144
+ const detected_stack = await detectStack(projectRoot);
145
+ const file_count = await countFiles(projectRoot);
146
+ return {
147
+ directories,
148
+ detected_stack,
149
+ file_count,
150
+ derived_at: now().toISOString(),
151
+ };
152
+ }
153
+ async function listTopLevelDirectories(projectRoot) {
154
+ try {
155
+ const entries = await fs.readdir(projectRoot, { withFileTypes: true });
156
+ return entries
157
+ .filter((e) => e.isDirectory() && !IGNORE_DIRS.has(e.name))
158
+ .map((e) => e.name)
159
+ .sort((a, b) => a.localeCompare(b));
160
+ }
161
+ catch {
162
+ return [];
163
+ }
164
+ }
165
+ async function detectStack(projectRoot) {
166
+ const detected = new Set();
167
+ if (await pathExists(path.join(projectRoot, 'package.json'))) {
168
+ // malformed package.json must never crash init
169
+ try {
170
+ const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8');
171
+ JSON.parse(raw);
172
+ }
173
+ catch {
174
+ // ignore parse issues; still detected as node
175
+ }
176
+ detected.add('node');
177
+ }
178
+ if (await pathExists(path.join(projectRoot, 'tsconfig.json')))
179
+ detected.add('typescript');
180
+ if (await pathExists(path.join(projectRoot, 'requirements.txt')))
181
+ detected.add('python');
182
+ if (await pathExists(path.join(projectRoot, 'Cargo.toml')))
183
+ detected.add('rust');
184
+ if (await pathExists(path.join(projectRoot, 'go.mod')))
185
+ detected.add('go');
186
+ if (await pathExists(path.join(projectRoot, 'pom.xml')))
187
+ detected.add('java');
188
+ return Array.from(detected);
189
+ }
190
+ async function countFiles(projectRoot) {
191
+ let count = 0;
192
+ const stack = [projectRoot];
193
+ while (stack.length > 0) {
194
+ const current = stack.pop();
195
+ if (!current)
196
+ continue;
197
+ let entries;
198
+ try {
199
+ entries = await fs.readdir(current, { withFileTypes: true });
200
+ }
201
+ catch {
202
+ continue;
203
+ }
204
+ for (const entry of entries) {
205
+ if (entry.isDirectory() && IGNORE_DIRS.has(entry.name))
206
+ continue;
207
+ const fullPath = path.join(current, entry.name);
208
+ let stat;
209
+ try {
210
+ stat = await fs.lstat(fullPath);
211
+ }
212
+ catch {
213
+ continue;
214
+ }
215
+ if (stat.isSymbolicLink())
216
+ continue;
217
+ if (stat.isDirectory()) {
218
+ stack.push(fullPath);
219
+ }
220
+ else if (stat.isFile()) {
221
+ count += 1;
222
+ }
223
+ }
224
+ }
225
+ return count;
226
+ }
227
+ async function writeIfMissing(filePath, data, created, skipped, projectRoot) {
228
+ const existing = await readJsonOrNull(filePath);
229
+ if (existing !== null) {
230
+ skipped.push(path.relative(projectRoot, filePath).replaceAll('\\', '/'));
231
+ return;
232
+ }
233
+ await writeAtomic(filePath, data);
234
+ created.push(path.relative(projectRoot, filePath).replaceAll('\\', '/'));
235
+ }
236
+ async function pathExists(targetPath) {
237
+ try {
238
+ await fs.access(targetPath);
239
+ return true;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ async function collectLines(ask, message) {
246
+ const lines = [];
247
+ while (true) {
248
+ const line = (await safePrompt(ask, message)).trim();
249
+ if (!line)
250
+ break;
251
+ lines.push(line);
252
+ }
253
+ return lines;
254
+ }
255
+ async function safePrompt(ask, message) {
256
+ try {
257
+ return await ask(message);
258
+ }
259
+ catch {
260
+ return '';
261
+ }
262
+ }
263
+ async function defaultAsk(message) {
264
+ const prompts = await import('@inquirer/prompts');
265
+ return prompts.input({ message });
266
+ }
@@ -0,0 +1,16 @@
1
+ const CURRENT_SCHEMA = '0.1.0';
2
+ export async function runMigrate(deps) {
3
+ const log = deps?.log ?? console.log;
4
+ log('matha migrate — schema migration');
5
+ log('');
6
+ log('This command will migrate your .matha/ directory');
7
+ log('to the current schema version.');
8
+ log('');
9
+ log(`Current schema: ${CURRENT_SCHEMA}`);
10
+ log(`Status: Migration not required for ${CURRENT_SCHEMA}`);
11
+ log('');
12
+ log('Full migration support arrives in v0.2.0.');
13
+ log('Track progress: github.com/your-username/matha/issues');
14
+ log('');
15
+ return { exitCode: 0, message: 'ok' };
16
+ }
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { runInit } from './commands/init.js';
4
+ import { runBefore } from './commands/before.js';
5
+ import { runAfter } from './commands/after.js';
6
+ import { runMigrate } from './commands/migrate.js';
7
+ import { parseMarkdownFile } from './utils/markdown-parser.js';
8
+ const program = new Command();
9
+ program
10
+ .name('matha')
11
+ .description('MATHA: Persistent cognitive layer for AI-assisted development')
12
+ .version('0.1.0');
13
+ // ──────────────────────────────────────────────────────────────────────
14
+ // INIT COMMAND
15
+ // ──────────────────────────────────────────────────────────────────────
16
+ program
17
+ .command('init')
18
+ .description('Initialize MATHA in a project (one-time setup)')
19
+ .option('--project <path>', 'Project root path (default: current directory)')
20
+ .option('--from <filepath>', 'Parse a markdown/text file to pre-fill init prompts')
21
+ .action(async (options) => {
22
+ try {
23
+ const projectRoot = options.project || process.cwd();
24
+ let seed = undefined;
25
+ if (options.from) {
26
+ try {
27
+ seed = await parseMarkdownFile(options.from);
28
+ }
29
+ catch (err) {
30
+ console.error(err.message);
31
+ process.exit(1);
32
+ }
33
+ }
34
+ await runInit(projectRoot, { seed });
35
+ }
36
+ catch (err) {
37
+ console.error('Init failed:', err.message);
38
+ process.exit(1);
39
+ }
40
+ });
41
+ // ──────────────────────────────────────────────────────────────────────
42
+ // BEFORE COMMAND
43
+ // ──────────────────────────────────────────────────────────────────────
44
+ program
45
+ .command('before')
46
+ .description('Run gates 01-06: pre-session context gathering')
47
+ .option('--project <path>', 'Project root path (default: current directory)')
48
+ .action(async (options) => {
49
+ try {
50
+ const projectRoot = options.project || process.cwd();
51
+ await runBefore(projectRoot, {});
52
+ }
53
+ catch (err) {
54
+ console.error('Before failed:', err.message);
55
+ process.exit(1);
56
+ }
57
+ });
58
+ // ──────────────────────────────────────────────────────────────────────
59
+ // AFTER COMMAND
60
+ // ──────────────────────────────────────────────────────────────────────
61
+ program
62
+ .command('after')
63
+ .description('Run gate 08: post-session write-back and loop closure')
64
+ .option('--project <path>', 'Project root path (default: current directory)')
65
+ .action(async (options) => {
66
+ try {
67
+ const projectRoot = options.project || process.cwd();
68
+ await runAfter(projectRoot, {});
69
+ }
70
+ catch (err) {
71
+ console.error('After failed:', err.message);
72
+ process.exit(1);
73
+ }
74
+ });
75
+ // ──────────────────────────────────────────────────────────────────────
76
+ // MIGRATE COMMAND
77
+ // ──────────────────────────────────────────────────────────────────────
78
+ program
79
+ .command('migrate')
80
+ .description('Migrate .matha/ to current schema version')
81
+ .action(async () => {
82
+ const result = await runMigrate();
83
+ process.exit(result.exitCode);
84
+ });
85
+ // ──────────────────────────────────────────────────────────────────────
86
+ // SERVE COMMAND
87
+ // ──────────────────────────────────────────────────────────────────────
88
+ program
89
+ .command('serve')
90
+ .description('Start MCP server on stdio for IDE integration')
91
+ .option('--project <path>', 'Project root path (default: current directory)')
92
+ .action((options) => {
93
+ try {
94
+ const projectRoot = options.project || process.cwd();
95
+ // Import and run the server directly instead of spawning
96
+ // This keeps the stdio channel intact for MCP protocol
97
+ import('./mcp/server.js').catch((err) => {
98
+ console.error('Failed to start MCP server:', err.message);
99
+ process.exit(1);
100
+ });
101
+ }
102
+ catch (err) {
103
+ console.error('Serve failed:', err.message);
104
+ process.exit(1);
105
+ }
106
+ });
107
+ // ──────────────────────────────────────────────────────────────────────
108
+ // PARSE & EXECUTE
109
+ // ──────────────────────────────────────────────────────────────────────
110
+ program.parse(process.argv);
111
+ // Show help if no command provided
112
+ if (!process.argv.slice(2).length) {
113
+ program.outputHelp();
114
+ }