dotdog 0.1.6 → 0.2.4

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 (2) hide show
  1. package/dist/cli.js +236 -39
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2534,9 +2534,9 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
2534
2534
  var source_default = chalk;
2535
2535
 
2536
2536
  // src/cli.ts
2537
- import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from "fs";
2538
- import { join } from "path";
2539
- import { homedir } from "os";
2537
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
2538
+ import { join as join2 } from "path";
2539
+ import { homedir as homedir2 } from "os";
2540
2540
 
2541
2541
  // src/parser.ts
2542
2542
  function parse(source) {
@@ -2894,13 +2894,173 @@ function parseInlineObject(value) {
2894
2894
  }
2895
2895
  return obj;
2896
2896
  }
2897
-
2898
- // src/cli.ts
2897
+ // src/serve.ts
2898
+ import { existsSync, readdirSync, readFileSync } from "fs";
2899
+ import { join } from "path";
2900
+ import { homedir } from "os";
2901
+ import * as readline from "readline";
2899
2902
  function resolvePath(p) {
2900
2903
  if (p.startsWith("~"))
2901
2904
  p = join(homedir(), p.slice(1));
2902
2905
  return p.startsWith("/") ? p : join(process.cwd(), p);
2903
2906
  }
2907
+ function serve(dir = ".") {
2908
+ const root = resolvePath(dir);
2909
+ const dagCache = new Map;
2910
+ function loadDags() {
2911
+ const dirs = [join(root, "projects"), join(root, "specs"), root];
2912
+ for (const dd of dirs) {
2913
+ if (!existsSync(dd))
2914
+ continue;
2915
+ const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2916
+ for (const p of projects) {
2917
+ const dagFile = join(dd, p, `${p}.dag`);
2918
+ if (existsSync(dagFile)) {
2919
+ dagCache.set(p, JSON.parse(readFileSync(dagFile, "utf-8")));
2920
+ }
2921
+ }
2922
+ }
2923
+ return [...dagCache.keys()];
2924
+ }
2925
+ function handleRequest(req) {
2926
+ const { id, method, params } = req;
2927
+ if (method === "initialize") {
2928
+ return {
2929
+ jsonrpc: "2.0",
2930
+ id,
2931
+ result: {
2932
+ protocolVersion: "0.1.0",
2933
+ serverInfo: { name: "spec-serve", version: "0.1.0" },
2934
+ capabilities: { tools: {} }
2935
+ }
2936
+ };
2937
+ }
2938
+ if (method === "tools/list") {
2939
+ return {
2940
+ jsonrpc: "2.0",
2941
+ id,
2942
+ result: {
2943
+ tools: [
2944
+ { name: "getEntity", description: "Get entity with properties, states, lifecycle", inputSchema: { type: "object", properties: { project: { type: "string" }, name: { type: "string" } }, required: ["name"] } },
2945
+ { name: "traverse", description: "Traverse graph: from node, follow edges, return subgraph", inputSchema: { type: "object", properties: { project: { type: "string" }, from: { type: "string" }, depth: { type: "number", default: 1 } }, required: ["from"] } },
2946
+ { name: "search", description: "Find entities by name or type", inputSchema: { type: "object", properties: { project: { type: "string" }, q: { type: "string" }, type: { type: "string" } }, required: ["q"] } },
2947
+ { name: "listProjects", description: "List all projects", inputSchema: { type: "object", properties: {} } },
2948
+ { name: "summary", description: "Get project summary: node count, edge count, completeness", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
2949
+ { name: "schema", description: "Get full property schema for an entity", inputSchema: { type: "object", properties: { project: { type: "string" }, entity: { type: "string" } }, required: ["entity"] } }
2950
+ ]
2951
+ }
2952
+ };
2953
+ }
2954
+ if (method === "tools/call") {
2955
+ const { name, arguments: args } = params;
2956
+ loadDags();
2957
+ if (name === "listProjects") {
2958
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify([...dagCache.keys()]) }] } };
2959
+ }
2960
+ if (name === "listNodes") {
2961
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2962
+ if (!dag)
2963
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2964
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(dag.nodes || []) }] } };
2965
+ }
2966
+ if (name === "getEntity") {
2967
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2968
+ if (!dag)
2969
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2970
+ const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === (args.name || "").toLowerCase());
2971
+ if (!node)
2972
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
2973
+ const edges = (dag.edges || []).filter((e) => e.source.toLowerCase() === node.id.toLowerCase() || e.target.toLowerCase() === node.id.toLowerCase());
2974
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
2975
+ }
2976
+ if (name === "traverse") {
2977
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2978
+ if (!dag)
2979
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2980
+ const depth = args.depth || 1;
2981
+ const visited = new Set;
2982
+ const subgraph = { nodes: [], edges: [] };
2983
+ const queue = [{ id: args.from, depth: 0 }];
2984
+ while (queue.length > 0) {
2985
+ const curr = queue.shift();
2986
+ if (visited.has(curr.id) || curr.depth > depth)
2987
+ continue;
2988
+ visited.add(curr.id);
2989
+ const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === curr.id.toLowerCase());
2990
+ if (node)
2991
+ subgraph.nodes.push(node);
2992
+ const edges = (dag.edges || []).filter((e) => e.source.toLowerCase() === curr.id.toLowerCase() || e.target.toLowerCase() === curr.id.toLowerCase());
2993
+ for (const e of edges) {
2994
+ subgraph.edges.push(e);
2995
+ const next = e.source.toLowerCase() === curr.id.toLowerCase() ? e.target : e.source;
2996
+ if (!visited.has(next))
2997
+ queue.push({ id: next, depth: curr.depth + 1 });
2998
+ }
2999
+ }
3000
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(subgraph) }] } };
3001
+ }
3002
+ if (name === "search") {
3003
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3004
+ if (!dag)
3005
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3006
+ const q = (args.q || "").toLowerCase();
3007
+ const type = (args.type || "").toLowerCase();
3008
+ const results = (dag.nodes || []).filter((n) => n.id.toLowerCase().includes(q) && (!type || (n.type || "").toLowerCase().includes(type)));
3009
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
3010
+ }
3011
+ if (name === "summary") {
3012
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3013
+ if (!dag)
3014
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3015
+ const s = {
3016
+ project: dag.project,
3017
+ nodes: (dag.nodes || []).length,
3018
+ edges: (dag.edges || []).length,
3019
+ files: dag.files || dag.count || 0,
3020
+ compiled: dag.compiled_at
3021
+ };
3022
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(s) }] } };
3023
+ }
3024
+ if (name === "schema") {
3025
+ const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3026
+ if (!dag)
3027
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3028
+ const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === (args.entity || "").toLowerCase());
3029
+ if (!node)
3030
+ return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
3031
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
3032
+ entity: node.id,
3033
+ properties: node.properties || {},
3034
+ states: node.states || [],
3035
+ lifecycle: node.lifecycle || []
3036
+ }) }] } };
3037
+ }
3038
+ return { jsonrpc: "2.0", id, error: { code: 404, message: `Unknown tool: ${name}` } };
3039
+ }
3040
+ return { jsonrpc: "2.0", id, error: { code: 404, message: `Unknown method: ${method}` } };
3041
+ }
3042
+ const rl = readline.createInterface({ input: process.stdin });
3043
+ loadDags();
3044
+ console.error(`[spec-serve] Loaded ${dagCache.size} projects`);
3045
+ rl.on("line", (line) => {
3046
+ try {
3047
+ const req = JSON.parse(line);
3048
+ const res = handleRequest(req);
3049
+ process.stdout.write(JSON.stringify(res) + `
3050
+ `);
3051
+ } catch (e) {
3052
+ process.stderr.write(`[spec-serve] Error: ${e}
3053
+ `);
3054
+ }
3055
+ });
3056
+ }
3057
+
3058
+ // src/cli.ts
3059
+ function resolvePath2(p) {
3060
+ if (p.startsWith("~"))
3061
+ p = join2(homedir2(), p.slice(1));
3062
+ return p.startsWith("/") ? p : join2(process.cwd(), p);
3063
+ }
2904
3064
  function parseSections2(markdown) {
2905
3065
  const lines = markdown.split(`
2906
3066
  `), sections = [];
@@ -2933,19 +3093,19 @@ function parseSections2(markdown) {
2933
3093
  return sections;
2934
3094
  }
2935
3095
  var program2 = new Command;
2936
- var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
3096
+ var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
2937
3097
  program2.name("spec").alias("dotdog").description("The spec dog — validate, analyze, generate .dog files").version(pkg.version);
2938
3098
  program2.command("validate [dir]").action((d = ".") => {
2939
- const dirs = [join(d, "projects"), join(d, "specs")];
3099
+ const dirs = [join2(d, "projects"), join2(d, "specs")];
2940
3100
  let found = false;
2941
3101
  for (const dd of dirs) {
2942
- if (!existsSync(dd))
3102
+ if (!existsSync2(dd))
2943
3103
  continue;
2944
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3104
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2945
3105
  for (const p of projects) {
2946
3106
  found = true;
2947
- const pd = join(dd, p, "specs");
2948
- const files = existsSync(pd) ? readdirSync(pd).filter((f) => f.endsWith(".dog")) : [];
3107
+ const pd = join2(dd, p, "specs");
3108
+ const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
2949
3109
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
2950
3110
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
2951
3111
  console.log(source_default.bold(`
@@ -2962,7 +3122,7 @@ program2.command("validate [dir]").action((d = ".") => {
2962
3122
  console.log(source_default.yellow("No projects found. Run: spec init <project>"));
2963
3123
  });
2964
3124
  program2.command("init <project>").action((p) => {
2965
- const d = join(process.cwd(), "specs", p);
3125
+ const d = join2(process.cwd(), "specs", p);
2966
3126
  mkdirSync(d, { recursive: true });
2967
3127
  const tmpl = {
2968
3128
  "SPEC.dog": `# Project
@@ -2997,7 +3157,7 @@ program2.command("init <project>").action((p) => {
2997
3157
  `
2998
3158
  };
2999
3159
  for (const [f, c] of Object.entries(tmpl)) {
3000
- writeFileSync(join(d, f), c);
3160
+ writeFileSync(join2(d, f), c);
3001
3161
  console.log(source_default.green(` ✓ ${f}`));
3002
3162
  }
3003
3163
  console.log(source_default.bold(`
@@ -3005,23 +3165,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
3005
3165
  });
3006
3166
  program2.command("list").action(() => {
3007
3167
  for (const d of ["projects", "specs"]) {
3008
- const dd = join(process.cwd(), d);
3009
- if (!existsSync(dd))
3168
+ const dd = join2(process.cwd(), d);
3169
+ if (!existsSync2(dd))
3010
3170
  continue;
3011
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3171
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3012
3172
  if (!projects.length)
3013
3173
  continue;
3014
3174
  console.log(source_default.bold(`
3015
3175
  ${d}/`));
3016
3176
  for (const p of projects) {
3017
- const sp = join(dd, p, "specs");
3018
- const n = existsSync(sp) ? readdirSync(sp).filter((f) => f.endsWith(".dog")).length : 0;
3177
+ const sp = join2(dd, p, "specs");
3178
+ const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
3019
3179
  console.log(` ${source_default.cyan(p)} — ${n} .dog files`);
3020
3180
  }
3021
3181
  }
3022
3182
  });
3023
3183
  program2.command("parse <file>").action((f) => {
3024
- const c = readFileSync(f, "utf-8");
3184
+ const c = readFileSync2(f, "utf-8");
3025
3185
  const s = parseSections2(c);
3026
3186
  console.log(source_default.bold(`
3027
3187
  ${s.length} sections`));
@@ -3029,21 +3189,21 @@ ${s.length} sections`));
3029
3189
  console.log(` ${sec.heading.padEnd(30)} ${sec.content.length} chars`);
3030
3190
  });
3031
3191
  program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
3032
- const dir = resolvePath(d);
3033
- const dirs = [join(dir, "projects"), join(dir, "specs"), dir];
3192
+ const dir = resolvePath2(d);
3193
+ const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3034
3194
  let found = false;
3035
3195
  for (const dd of dirs) {
3036
- if (!existsSync(dd))
3196
+ if (!existsSync2(dd))
3037
3197
  continue;
3038
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3198
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3039
3199
  for (const p of projects) {
3040
- const pd = join(dd, p, "specs");
3041
- if (!existsSync(pd))
3200
+ const pd = join2(dd, p, "specs");
3201
+ if (!existsSync2(pd))
3042
3202
  continue;
3043
- const files = readdirSync(pd).filter((f) => f.endsWith(".dog"));
3203
+ const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3044
3204
  const dag = { version: "1.0", project: p, compiled_at: new Date().toISOString(), nodes: [], edges: [], count: files.length };
3045
3205
  for (const f of files) {
3046
- const c = readFileSync(join(pd, f), "utf-8");
3206
+ const c = readFileSync2(join2(pd, f), "utf-8");
3047
3207
  const secs = parseSections2(c);
3048
3208
  for (const s of secs) {
3049
3209
  if (s.heading.includes("Entity:") || s.heading.includes("Relationship:")) {
@@ -3058,7 +3218,7 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3058
3218
  }
3059
3219
  }
3060
3220
  found = true;
3061
- const out = opts.output || join(pd, "..", `${p}.dag`);
3221
+ const out = opts.output || join2(pd, "..", `${p}.dag`);
3062
3222
  writeFileSync(out, JSON.stringify(dag, null, 2));
3063
3223
  console.log(source_default.green(` ✓ ${out}`));
3064
3224
  console.log(source_default.gray(` ${dag.nodes.length} nodes, ${dag.edges.length} edges, ${dag.count} files`));
@@ -3067,33 +3227,70 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3067
3227
  if (!found)
3068
3228
  console.log(source_default.yellow("No projects found."));
3069
3229
  });
3230
+ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
3231
+ const dir = resolvePath2(d);
3232
+ const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3233
+ for (const dd of dirs) {
3234
+ if (!existsSync2(dd))
3235
+ continue;
3236
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3237
+ for (const p of projects) {
3238
+ const dagFile = join2(dd, p, `${p}.dag`);
3239
+ if (!existsSync2(dagFile))
3240
+ continue;
3241
+ const dag = JSON.parse(readFileSync2(dagFile, "utf-8"));
3242
+ let out = "```mermaid\ngraph LR\n";
3243
+ for (const n of dag.nodes || [])
3244
+ out += ` ${n.id.replace(/\s+/g, "_")}[${n.id}]
3245
+ `;
3246
+ for (const e of dag.edges || [])
3247
+ out += ` ${e.source.replace(/\s+/g, "_")} -->|${e.verb || ""}| ${e.target.replace(/\s+/g, "_")}
3248
+ `;
3249
+ out += "```\n";
3250
+ if (opts.save) {
3251
+ const outFile = join2(dd, p, "..", `${p}.md`);
3252
+ writeFileSync(outFile, `# ${p} — Spec Graph
3253
+
3254
+ ${out}`);
3255
+ console.log(source_default.green(` ✓ ${outFile}`));
3256
+ }
3257
+ console.log(out);
3258
+ }
3259
+ }
3260
+ });
3261
+ program2.command("serve [dir]").description("MCP server — expose .dag graph to AI agents over stdio").action((d = ".") => serve(d));
3070
3262
  program2.command("staleness [dir]").action((d = ".") => {
3071
- const dir = resolvePath(d);
3072
- const dirs = [join(dir, "projects"), join(dir, "specs"), dir];
3263
+ const dir = resolvePath2(d);
3264
+ const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3073
3265
  console.log(source_default.bold(`Staleness Audit
3074
3266
  `));
3075
3267
  for (const dd of dirs) {
3076
- if (!existsSync(dd))
3268
+ if (!existsSync2(dd))
3077
3269
  continue;
3078
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3270
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3079
3271
  for (const p of projects) {
3080
- const pd = join(dd, p, "specs");
3081
- if (!existsSync(pd))
3272
+ const pd = join2(dd, p, "specs");
3273
+ if (!existsSync2(pd))
3082
3274
  continue;
3083
- const planFile = join(pd, "plan.dog");
3084
- if (!existsSync(planFile)) {
3275
+ const planFile = join2(pd, "plan.dog");
3276
+ if (!existsSync2(planFile)) {
3085
3277
  console.log(source_default.yellow(` ${p}: No plan.dog`));
3086
3278
  continue;
3087
3279
  }
3088
- const plan = readFileSync(planFile, "utf-8");
3280
+ const plan = readFileSync2(planFile, "utf-8");
3089
3281
  const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
3090
3282
  let issues = 0;
3091
3283
  for (const m of tasks) {
3092
3284
  const done = m[1] === "x";
3093
3285
  const text = m[2].toLowerCase();
3286
+ const precedingText = plan.substring(Math.max(0, m.index - 200), m.index);
3287
+ const phaseMatch = precedingText.match(/Phase\s+(\d+)/);
3288
+ const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
3289
+ if (phase > 3)
3290
+ continue;
3094
3291
  if (text.includes("npm publish") || text.includes("npm install")) {
3095
3292
  try {
3096
- const pkg2 = JSON.parse(readFileSync(join(resolvePath("."), "packages/dotdog/package.json"), "utf-8"));
3293
+ const pkg2 = JSON.parse(readFileSync2(join2(resolvePath2("."), "packages/dotdog/package.json"), "utf-8"));
3097
3294
  if (pkg2.version && !done) {
3098
3295
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3099
3296
  issues++;
@@ -3107,7 +3304,7 @@ program2.command("staleness [dir]").action((d = ".") => {
3107
3304
  }
3108
3305
  }
3109
3306
  if (text.includes("generate") && !done) {
3110
- if (existsSync(join(dir, "packages/dotdog/src/cli.ts")) || existsSync(join(dir, "packages/spec-cli/src/generate.ts"))) {
3307
+ if (existsSync2(join2(dir, "packages/dotdog/src/cli.ts")) || existsSync2(join2(dir, "packages/spec-cli/src/generate.ts"))) {
3111
3308
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3112
3309
  issues++;
3113
3310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotdog",
3
- "version": "0.1.6",
3
+ "version": "0.2.4",
4
4
  "description": "The spec dog \u2014 validate, analyze, parse, and generate .dog spec genome files",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "license": "MIT",
27
27
  "author": "specdog",
28
- "repository": "github:specdog/spec",
28
+ "repository": "github:specdog/dotdog",
29
29
  "dependencies": {
30
30
  "commander": "^15.0.0",
31
31
  "chalk": "^5.6.0"