@dnai/dynamicllm 0.1.1 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +64 -0
  2. package/dist/bin/dynamicllm.js +7 -0
  3. package/dist/cli/formatters.d.ts +11 -0
  4. package/dist/cli/formatters.js +138 -0
  5. package/dist/cli/index.d.ts +1 -0
  6. package/dist/cli/index.js +345 -0
  7. package/dist/cli/init.d.ts +1 -0
  8. package/dist/{setup.js → cli/init.js} +29 -48
  9. package/dist/core/bootstrap.d.ts +14 -0
  10. package/dist/core/bootstrap.js +38 -0
  11. package/dist/core/constants.d.ts +4 -0
  12. package/dist/core/constants.js +71 -0
  13. package/dist/{lib → core}/fetcher.d.ts +6 -7
  14. package/dist/core/fetcher.js +247 -0
  15. package/dist/core/index.d.ts +9 -0
  16. package/dist/core/index.js +8 -0
  17. package/dist/{lib → core}/lint.js +0 -5
  18. package/dist/core/log.d.ts +13 -0
  19. package/dist/core/log.js +65 -0
  20. package/dist/core/report.d.ts +5 -0
  21. package/dist/{lib → core}/report.js +7 -14
  22. package/dist/{lib → core}/types.d.ts +43 -0
  23. package/dist/core/types.js +2 -0
  24. package/dist/{server.js → mcp/server.js} +48 -68
  25. package/framework/CONVENTIONS.md +40 -0
  26. package/framework/SYSTEM_PROMPT.md +203 -0
  27. package/framework/antidotes/constructive-optimism.md +37 -0
  28. package/framework/antidotes/liberated-agentism.md +37 -0
  29. package/framework/antidotes/objective-fallibilism.md +39 -0
  30. package/framework/antidotes/oxidative-creativism.md +41 -0
  31. package/framework/antidotes/polycentric-nodalism.md +38 -0
  32. package/framework/antidotes/vertical-authenticism.md +39 -0
  33. package/framework/lenses/logos.md +32 -0
  34. package/framework/lenses/mythos.md +32 -0
  35. package/framework/lenses/pathos.md +32 -0
  36. package/framework/qualities/beauty.md +29 -0
  37. package/framework/qualities/infinity.md +29 -0
  38. package/framework/qualities/love.md +29 -0
  39. package/framework/qualities/mystery.md +29 -0
  40. package/framework/qualities/play.md +29 -0
  41. package/framework/qualities/story.md +29 -0
  42. package/package.json +29 -5
  43. package/dist/cli.js +0 -20
  44. package/dist/lib/fetcher.js +0 -210
  45. package/dist/lib/report.d.ts +0 -19
  46. package/dist/lib/types.js +0 -1
  47. package/dist/setup.d.ts +0 -1
  48. /package/dist/{cli.d.ts → bin/dynamicllm.d.ts} +0 -0
  49. /package/dist/{lib → core}/cache.d.ts +0 -0
  50. /package/dist/{lib → core}/cache.js +0 -0
  51. /package/dist/{lib → core}/context.d.ts +0 -0
  52. /package/dist/{lib → core}/context.js +0 -0
  53. /package/dist/{lib → core}/lint.d.ts +0 -0
  54. /package/dist/{lib → core}/search.d.ts +0 -0
  55. /package/dist/{lib → core}/search.js +0 -0
  56. /package/dist/{index.d.ts → mcp/index.d.ts} +0 -0
  57. /package/dist/{index.js → mcp/index.js} +0 -0
  58. /package/dist/{server.d.ts → mcp/server.d.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { stdin, stdout } from "node:process";
6
- const DEFAULT_NETWORK_DIR = join(homedir(), ".dynamicllm", "network");
6
+ import { bootstrap } from "../core/bootstrap.js";
7
7
  const CONFIG_PATH = join(homedir(), ".dynamicllm", "config.json");
8
8
  const AGENTS = [
9
9
  { name: "Claude Code", key: "claude" },
@@ -12,44 +12,36 @@ const AGENTS = [
12
12
  { name: "Codex", key: "codex" },
13
13
  { name: "Other (show config)", key: "other" },
14
14
  ];
15
- function loadConfig() {
16
- if (!existsSync(CONFIG_PATH))
17
- return null;
18
- try {
19
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
20
- }
21
- catch {
22
- return null;
23
- }
24
- }
25
15
  function saveConfig(config) {
26
16
  const dir = join(homedir(), ".dynamicllm");
27
17
  if (!existsSync(dir))
28
18
  mkdirSync(dir, { recursive: true });
29
19
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
30
20
  }
21
+ function getMcpServerConfig(networkDir) {
22
+ return {
23
+ command: "dynamicllm",
24
+ args: ["mcp"],
25
+ env: {
26
+ DYNAMICLLM_NETWORK_DIR: networkDir,
27
+ },
28
+ };
29
+ }
31
30
  function getMcpJson(networkDir) {
32
31
  return {
33
32
  mcpServers: {
34
- dynamicllm: {
35
- command: "dynamicllm",
36
- env: {
37
- DYNAMICLLM_NETWORK_DIR: networkDir,
38
- },
39
- },
33
+ dynamicllm: getMcpServerConfig(networkDir),
40
34
  },
41
35
  };
42
36
  }
43
37
  function writeMcpJsonFile(dir, networkDir) {
44
38
  const mcpPath = join(dir, "mcp.json");
45
39
  const mcpConfig = getMcpJson(networkDir);
46
- // If file exists, merge rather than overwrite
47
40
  if (existsSync(mcpPath)) {
48
41
  try {
49
42
  const existing = JSON.parse(readFileSync(mcpPath, "utf-8"));
50
43
  existing.mcpServers = existing.mcpServers ?? {};
51
- existing.mcpServers.dynamicllm =
52
- mcpConfig["mcpServers"];
44
+ existing.mcpServers.dynamicllm = getMcpServerConfig(networkDir);
53
45
  writeFileSync(mcpPath, JSON.stringify(existing, null, 2), "utf-8");
54
46
  return true;
55
47
  }
@@ -57,7 +49,6 @@ function writeMcpJsonFile(dir, networkDir) {
57
49
  return false;
58
50
  }
59
51
  }
60
- // Create directory and file
61
52
  if (!existsSync(dir))
62
53
  mkdirSync(dir, { recursive: true });
63
54
  writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
@@ -68,13 +59,13 @@ async function registerAgent(agent, networkDir, rl) {
68
59
  case "claude": {
69
60
  const { execSync } = await import("node:child_process");
70
61
  try {
71
- execSync(`claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm`, { stdio: "inherit" });
62
+ execSync(`claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm mcp`, { stdio: "inherit" });
72
63
  console.log(" Registered with Claude Code (user-level).");
73
64
  }
74
65
  catch {
75
66
  console.log(" Could not auto-register with Claude Code.");
76
67
  console.log(" Run manually:");
77
- console.log(` claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm`);
68
+ console.log(` claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm mcp`);
78
69
  }
79
70
  break;
80
71
  }
@@ -120,33 +111,23 @@ async function registerAgent(agent, networkDir, rl) {
120
111
  }
121
112
  }
122
113
  }
123
- export async function setup() {
114
+ export async function init() {
124
115
  const rl = createInterface({ input: stdin, output: stdout });
125
- const existing = loadConfig();
116
+ const { networkDir: defaultDir } = bootstrap();
126
117
  console.log("");
127
- console.log(" DynamicLLM Setup");
128
- console.log(" ───────────────");
118
+ console.log(" DynamicLLM Init");
119
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
129
120
  console.log("");
130
- // Ensure global install so `dynamicllm` command is available
131
- const { execSync } = await import("node:child_process");
132
- try {
133
- execSync("which dynamicllm", { stdio: "ignore" });
134
- }
135
- catch {
136
- console.log(" Installing DynamicLLM globally...");
137
- try {
138
- execSync("npm install -g @dnai/dynamicllm", { stdio: "inherit" });
139
- console.log("");
140
- }
141
- catch {
142
- console.log(" Warning: Could not install globally. Run: npm install -g @dnai/dynamicllm");
143
- console.log("");
144
- }
145
- }
146
121
  // 1. Network directory
147
- const defaultDir = existing?.networkDir ?? DEFAULT_NETWORK_DIR;
148
- const dirAnswer = await rl.question(` Network directory [${defaultDir}]: `);
149
- const networkDir = dirAnswer.trim().replace(/^['"]|['"]$/g, "") || defaultDir;
122
+ console.log(` Your network nodes will be stored in:`);
123
+ console.log(` ${defaultDir}`);
124
+ console.log("");
125
+ const changeDir = await rl.question(" Change location? [y/N]: ");
126
+ let networkDir = defaultDir;
127
+ if (changeDir.trim().toLowerCase() === "y") {
128
+ const dirAnswer = await rl.question(` New directory: `);
129
+ networkDir = dirAnswer.trim().replace(/^['"]|['"]$/g, "") || defaultDir;
130
+ }
150
131
  if (!existsSync(networkDir)) {
151
132
  const create = await rl.question(` Directory doesn't exist. Create it? [Y/n]: `);
152
133
  if (create.trim().toLowerCase() !== "n") {
@@ -174,7 +155,7 @@ export async function setup() {
174
155
  console.log("");
175
156
  await registerAgent(selectedAgent.key, networkDir, rl);
176
157
  console.log("");
177
- console.log(" Setup complete. Start using DynamicLLM with your AI agent.");
158
+ console.log(" Init complete. Start using DynamicLLM with your AI agent.");
178
159
  console.log("");
179
160
  rl.close();
180
161
  }
@@ -0,0 +1,14 @@
1
+ export interface BootstrapResult {
2
+ networkDir: string;
3
+ /** True when this invocation created the default config (first run). */
4
+ created: boolean;
5
+ }
6
+ /**
7
+ * Resolves the network directory, creating defaults on first use if needed.
8
+ *
9
+ * Precedence:
10
+ * 1. DYNAMICLLM_NETWORK_DIR env var — used as-is, no config written
11
+ * 2. ~/.dynamicllm/config.json — existing valid config
12
+ * 3. Create defaults — ~/.dynamicllm/, ~/.dynamicllm/network/, config.json
13
+ */
14
+ export declare function bootstrap(): BootstrapResult;
@@ -0,0 +1,38 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONFIG_DIR = join(homedir(), ".dynamicllm");
5
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
+ const DEFAULT_NETWORK_DIR = join(CONFIG_DIR, "network");
7
+ /**
8
+ * Resolves the network directory, creating defaults on first use if needed.
9
+ *
10
+ * Precedence:
11
+ * 1. DYNAMICLLM_NETWORK_DIR env var — used as-is, no config written
12
+ * 2. ~/.dynamicllm/config.json — existing valid config
13
+ * 3. Create defaults — ~/.dynamicllm/, ~/.dynamicllm/network/, config.json
14
+ */
15
+ export function bootstrap() {
16
+ // 1. Env var takes absolute precedence
17
+ if (process.env.DYNAMICLLM_NETWORK_DIR) {
18
+ return { networkDir: process.env.DYNAMICLLM_NETWORK_DIR, created: false };
19
+ }
20
+ // 2. Existing valid config
21
+ if (existsSync(CONFIG_PATH)) {
22
+ try {
23
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
24
+ if (typeof config.networkDir === "string" && config.networkDir) {
25
+ return { networkDir: config.networkDir, created: false };
26
+ }
27
+ }
28
+ catch {
29
+ // Invalid JSON — fall through to create defaults
30
+ }
31
+ }
32
+ // 3. Create defaults
33
+ if (!existsSync(DEFAULT_NETWORK_DIR)) {
34
+ mkdirSync(DEFAULT_NETWORK_DIR, { recursive: true });
35
+ }
36
+ writeFileSync(CONFIG_PATH, JSON.stringify({ networkDir: DEFAULT_NETWORK_DIR }, null, 2), "utf-8");
37
+ return { networkDir: DEFAULT_NETWORK_DIR, created: true };
38
+ }
@@ -0,0 +1,4 @@
1
+ import type { QualityDefinition, AntidoteDefinition } from "./types.js";
2
+ /** Web demo UI rendering constants — also exported for dna-web consumption */
3
+ export declare const QUALITIES: QualityDefinition[];
4
+ export declare const ANTIDOTES: AntidoteDefinition[];
@@ -0,0 +1,71 @@
1
+ /** Web demo UI rendering constants — also exported for dna-web consumption */
2
+ export const QUALITIES = [
3
+ {
4
+ id: "play",
5
+ name: "Play",
6
+ abbreviation: "PLY",
7
+ question: "Where do you want to add >Play today?",
8
+ },
9
+ {
10
+ id: "beauty",
11
+ name: "Beauty",
12
+ abbreviation: "BTY",
13
+ question: "Where do you want to add >Beauty today?",
14
+ },
15
+ {
16
+ id: "mystery",
17
+ name: "Mystery",
18
+ abbreviation: "MTY",
19
+ question: "Where do you want to add >Mystery today?",
20
+ },
21
+ {
22
+ id: "love",
23
+ name: "Love",
24
+ abbreviation: "LVE",
25
+ question: "Where do you want to add >Love today?",
26
+ },
27
+ {
28
+ id: "infinity",
29
+ name: "Infinity",
30
+ abbreviation: "INF",
31
+ question: "Where do you want to add >Infinity today?",
32
+ },
33
+ {
34
+ id: "story",
35
+ name: "Story",
36
+ abbreviation: "STY",
37
+ question: "Where do you want to add >Story today?",
38
+ },
39
+ ];
40
+ export const ANTIDOTES = [
41
+ {
42
+ id: "agentism",
43
+ dynamicPole: "Liberated Agentism",
44
+ staticPole: "Directed Interventionism",
45
+ },
46
+ {
47
+ id: "optimism",
48
+ dynamicPole: "Constructive Optimism",
49
+ staticPole: "Constrictive Pessimism",
50
+ },
51
+ {
52
+ id: "fallibilism",
53
+ dynamicPole: "Objective Fallibilism",
54
+ staticPole: "Authoritative Justificationism",
55
+ },
56
+ {
57
+ id: "nodalism",
58
+ dynamicPole: "Polycentric Nodalism",
59
+ staticPole: "Monocentric Globalism",
60
+ },
61
+ {
62
+ id: "authenticism",
63
+ dynamicPole: "Vertical Authenticism",
64
+ staticPole: "Horizontal Mimeticism",
65
+ },
66
+ {
67
+ id: "creativism",
68
+ dynamicPole: "Oxidative Creativism",
69
+ staticPole: "Reductive Parochialism",
70
+ },
71
+ ];
@@ -1,22 +1,21 @@
1
1
  import type { NetworkIndex } from "./types.js";
2
+ /** Slugs that are part of the bundled framework and cannot be overridden by user files */
3
+ export declare const FRAMEWORK_SLUGS: Set<string>;
2
4
  export declare class NetworkFetcher {
3
- private cdnUrl;
4
- private cache;
5
5
  private networkDir;
6
+ private frameworkDir;
6
7
  private localContents;
8
+ private frameworkContents;
7
9
  private mergedIndex;
8
10
  constructor(opts?: {
9
- cdnUrl?: string;
10
- cacheDir?: string;
11
11
  networkDir?: string;
12
12
  });
13
13
  fetchIndex(): Promise<NetworkIndex>;
14
+ /** Load bundled framework files from the framework/ directory */
15
+ private loadFramework;
14
16
  /** Invalidate cached merged index (call after saving new files) */
15
17
  invalidateIndex(): void;
16
18
  fetchNode(slug: string): Promise<string>;
17
19
  fetchSource(slug: string): Promise<string>;
18
20
  fetchMeta(slug: string): Promise<string>;
19
- private fetchCdnIndex;
20
- private fetchFile;
21
- private fetchRemote;
22
21
  }
@@ -0,0 +1,247 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, } from "node:fs";
2
+ import { join, basename, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createHash } from "node:crypto";
5
+ /** Resolve the bundled framework/ directory relative to this package */
6
+ function resolveFrameworkDir() {
7
+ const thisFile = fileURLToPath(import.meta.url);
8
+ // dist/core/fetcher.js → up two levels to package root → framework/
9
+ return join(dirname(thisFile), "..", "..", "framework");
10
+ }
11
+ /** Recursively collect all .md files under a directory, returning absolute paths */
12
+ function collectMarkdownFiles(dir) {
13
+ const results = [];
14
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
15
+ const fullPath = join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ results.push(...collectMarkdownFiles(fullPath));
18
+ }
19
+ else if (entry.name.endsWith(".md")) {
20
+ results.push(fullPath);
21
+ }
22
+ }
23
+ return results;
24
+ }
25
+ /** Slugs that are part of the bundled framework and cannot be overridden by user files */
26
+ export const FRAMEWORK_SLUGS = new Set([
27
+ "system-prompt", "conventions",
28
+ "logos", "pathos", "mythos",
29
+ "play", "beauty", "story", "infinity", "mystery", "love",
30
+ "liberated-agentism", "constructive-optimism", "objective-fallibilism",
31
+ "polycentric-nodalism", "vertical-authenticism", "oxidative-creativism",
32
+ ]);
33
+ /** Parse YAML frontmatter from a markdown string */
34
+ function parseFrontmatter(content) {
35
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
36
+ if (!match)
37
+ return {};
38
+ const fm = {};
39
+ for (const line of match[1].split("\n")) {
40
+ const colon = line.indexOf(":");
41
+ if (colon < 0)
42
+ continue;
43
+ const key = line.slice(0, colon).trim();
44
+ let val = line.slice(colon + 1).trim();
45
+ // Parse YAML arrays: [a, b, c]
46
+ if (typeof val === "string" && val.startsWith("[") && val.endsWith("]")) {
47
+ val = val
48
+ .slice(1, -1)
49
+ .split(",")
50
+ .map((s) => s.trim())
51
+ .filter(Boolean);
52
+ }
53
+ fm[key] = val;
54
+ }
55
+ return fm;
56
+ }
57
+ /** Scan a local directory of .md files and build index entries.
58
+ * If `protectFramework` is true, throws on slug collisions with framework slugs. */
59
+ function scanLocalDir(dir, protectFramework) {
60
+ const nodes = [];
61
+ const sources = [];
62
+ const fileContents = new Map();
63
+ if (!existsSync(dir))
64
+ return { nodes, sources, fileContents };
65
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
66
+ for (const file of files) {
67
+ const filePath = join(dir, file);
68
+ const content = readFileSync(filePath, "utf-8");
69
+ const slug = basename(file, ".md").toLowerCase().replace(/[_\s]+/g, "-");
70
+ // Protect framework slugs from user override
71
+ if (protectFramework && FRAMEWORK_SLUGS.has(slug)) {
72
+ throw new Error(`Slug collision: "${slug}" is a protected framework slug and cannot be overridden by user network files. ` +
73
+ `Remove or rename "${file}" in your network directory.`);
74
+ }
75
+ const fm = parseFrontmatter(content);
76
+ const size = statSync(filePath).size;
77
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
78
+ const type = fm.type;
79
+ const name = fm.name ?? slug;
80
+ const tags = fm.tags ?? [];
81
+ const VALID_TYPES = new Set(["person", "idea", "antidote", "lens", "quality", "source", "synthesis"]);
82
+ if (type && !VALID_TYPES.has(type)) {
83
+ console.warn(`[dynamicllm] Skipping ${file}: invalid type "${type}"`);
84
+ continue;
85
+ }
86
+ if (type === "source") {
87
+ sources.push({
88
+ slug,
89
+ displayName: name,
90
+ type: "source",
91
+ author: fm.author ?? "",
92
+ tags,
93
+ hash,
94
+ size,
95
+ });
96
+ fileContents.set(`sources/${slug}.md`, content);
97
+ }
98
+ else if (type) {
99
+ const entry = {
100
+ slug,
101
+ displayName: name,
102
+ type: type,
103
+ tags,
104
+ hash,
105
+ size,
106
+ };
107
+ if (type === "antidote" && fm.mind_virus) {
108
+ entry.mindVirus = fm.mind_virus;
109
+ }
110
+ if (fm.derived_from) {
111
+ entry.derivedFrom = fm.derived_from;
112
+ }
113
+ nodes.push(entry);
114
+ fileContents.set(`nodes/${slug}.md`, content);
115
+ }
116
+ }
117
+ return { nodes, sources, fileContents };
118
+ }
119
+ export class NetworkFetcher {
120
+ networkDir;
121
+ frameworkDir;
122
+ localContents = new Map();
123
+ frameworkContents = new Map();
124
+ mergedIndex = null;
125
+ constructor(opts) {
126
+ this.networkDir = opts?.networkDir ?? null;
127
+ this.frameworkDir = resolveFrameworkDir();
128
+ }
129
+ async fetchIndex() {
130
+ if (this.mergedIndex)
131
+ return this.mergedIndex;
132
+ // 1. Load bundled framework files
133
+ const framework = this.loadFramework();
134
+ // 2. Scan local user network directory (with framework slug protection)
135
+ if (this.networkDir) {
136
+ const local = scanLocalDir(this.networkDir, true);
137
+ this.localContents = local.fileContents;
138
+ // Merge: framework + local user content
139
+ this.mergedIndex = {
140
+ schemaVersion: 1,
141
+ networkVersion: new Date().toISOString().slice(0, 10),
142
+ nodes: [...framework.nodes, ...local.nodes],
143
+ sources: [...framework.sources, ...local.sources],
144
+ meta: framework.meta,
145
+ };
146
+ }
147
+ else {
148
+ this.mergedIndex = {
149
+ schemaVersion: 1,
150
+ networkVersion: new Date().toISOString().slice(0, 10),
151
+ nodes: framework.nodes,
152
+ sources: framework.sources,
153
+ meta: framework.meta,
154
+ };
155
+ }
156
+ return this.mergedIndex;
157
+ }
158
+ /** Load bundled framework files from the framework/ directory */
159
+ loadFramework() {
160
+ const nodes = [];
161
+ const sources = [];
162
+ const meta = [];
163
+ if (!existsSync(this.frameworkDir)) {
164
+ console.warn(`[dynamicllm] Framework directory not found: ${this.frameworkDir}`);
165
+ return { nodes, sources, meta };
166
+ }
167
+ const files = collectMarkdownFiles(this.frameworkDir);
168
+ for (const filePath of files) {
169
+ const content = readFileSync(filePath, "utf-8");
170
+ const slug = basename(filePath, ".md").toLowerCase().replace(/[_\s]+/g, "-");
171
+ const fm = parseFrontmatter(content);
172
+ const size = statSync(filePath).size;
173
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
174
+ if (fm.kind === "meta") {
175
+ meta.push({
176
+ slug,
177
+ displayName: fm.name ?? slug,
178
+ kind: "meta",
179
+ });
180
+ this.frameworkContents.set(`meta/${slug}.md`, content);
181
+ }
182
+ else if (fm.type === "source") {
183
+ sources.push({
184
+ slug,
185
+ displayName: fm.name ?? slug,
186
+ type: "source",
187
+ author: fm.author ?? "",
188
+ tags: fm.tags ?? [],
189
+ hash,
190
+ size,
191
+ });
192
+ this.frameworkContents.set(`sources/${slug}.md`, content);
193
+ }
194
+ else if (fm.type) {
195
+ const entry = {
196
+ slug,
197
+ displayName: fm.name ?? slug,
198
+ type: fm.type,
199
+ tags: fm.tags ?? [],
200
+ hash,
201
+ size,
202
+ };
203
+ if (fm.type === "antidote" && fm.mind_virus) {
204
+ entry.mindVirus = fm.mind_virus;
205
+ }
206
+ if (fm.derived_from) {
207
+ entry.derivedFrom = fm.derived_from;
208
+ }
209
+ nodes.push(entry);
210
+ this.frameworkContents.set(`nodes/${slug}.md`, content);
211
+ }
212
+ }
213
+ return { nodes, sources, meta };
214
+ }
215
+ /** Invalidate cached merged index (call after saving new files) */
216
+ invalidateIndex() {
217
+ this.mergedIndex = null;
218
+ this.localContents.clear();
219
+ }
220
+ async fetchNode(slug) {
221
+ const key = `nodes/${slug}.md`;
222
+ const local = this.localContents.get(key);
223
+ if (local)
224
+ return local;
225
+ const framework = this.frameworkContents.get(key);
226
+ if (framework)
227
+ return framework;
228
+ throw new Error(`Node "${slug}" not found in framework or local network.`);
229
+ }
230
+ async fetchSource(slug) {
231
+ const key = `sources/${slug}.md`;
232
+ const local = this.localContents.get(key);
233
+ if (local)
234
+ return local;
235
+ const framework = this.frameworkContents.get(key);
236
+ if (framework)
237
+ return framework;
238
+ throw new Error(`Source "${slug}" not found in framework or local network.`);
239
+ }
240
+ async fetchMeta(slug) {
241
+ const key = `meta/${slug}.md`;
242
+ const framework = this.frameworkContents.get(key);
243
+ if (framework)
244
+ return framework;
245
+ throw new Error(`Meta "${slug}" not found in framework.`);
246
+ }
247
+ }
@@ -0,0 +1,9 @@
1
+ export { NetworkFetcher, FRAMEWORK_SLUGS } from "./fetcher.js";
2
+ export { search } from "./search.js";
3
+ export { selectStartingFiles } from "./context.js";
4
+ export { lint } from "./lint.js";
5
+ export { generateReport } from "./report.js";
6
+ export { appendLog, readLog, readLogRaw } from "./log.js";
7
+ export { QUALITIES, ANTIDOTES } from "./constants.js";
8
+ export type { NetworkIndex, NodeEntry, SourceEntry, MetaEntry, SearchResult, LintReport, LintIssue, ProductiveTension, DynamicLLMMode, LensId, QualityId, QualityDefinition, AntidoteDefinition, VirusAxisScore, ReportOptions, LogEntry, } from "./types.js";
9
+ export type { Mode } from "./context.js";
@@ -0,0 +1,8 @@
1
+ // Core API — re-exports for package consumers
2
+ export { NetworkFetcher, FRAMEWORK_SLUGS } from "./fetcher.js";
3
+ export { search } from "./search.js";
4
+ export { selectStartingFiles } from "./context.js";
5
+ export { lint } from "./lint.js";
6
+ export { generateReport } from "./report.js";
7
+ export { appendLog, readLog, readLogRaw } from "./log.js";
8
+ export { QUALITIES, ANTIDOTES } from "./constants.js";
@@ -51,9 +51,6 @@ export function lint(index) {
51
51
  const orphanTags = [...tagCounts.entries()]
52
52
  .filter(([, count]) => count === 1)
53
53
  .map(([tag]) => tag);
54
- // Productive Tensions: find node pairs that share multiple tags
55
- // but represent different perspectives (different types or different people/ideas).
56
- // These are features to surface, not errors to fix.
57
54
  const tensions = detectProductiveTensions(index.nodes);
58
55
  return {
59
56
  errors,
@@ -71,8 +68,6 @@ function detectProductiveTensions(nodes) {
71
68
  for (let j = i + 1; j < nodes.length; j++) {
72
69
  const a = nodes[i];
73
70
  const b = nodes[j];
74
- // Only surface tensions between different types or between
75
- // person↔person / idea↔idea pairs (where disagreement is meaningful)
76
71
  if (a.type === b.type && a.type !== "person" && a.type !== "idea" && a.type !== "synthesis") {
77
72
  continue;
78
73
  }
@@ -0,0 +1,13 @@
1
+ import type { LogEntry } from "./types.js";
2
+ /** Append a timestamped entry to the local activity log */
3
+ export declare function appendLog(action: string, slug: string, opts?: {
4
+ mode?: string;
5
+ summary?: string;
6
+ }): void;
7
+ /** Read and parse the activity log into structured entries */
8
+ export declare function readLog(opts?: {
9
+ limit?: number;
10
+ action?: string;
11
+ }): LogEntry[];
12
+ /** Get the raw log content */
13
+ export declare function readLogRaw(): string;
@@ -0,0 +1,65 @@
1
+ import { existsSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const LOG_DIR = join(homedir(), ".dynamicllm");
5
+ const LOG_PATH = join(LOG_DIR, "log.md");
6
+ /** Append a timestamped entry to the local activity log */
7
+ export function appendLog(action, slug, opts) {
8
+ if (!existsSync(LOG_DIR))
9
+ mkdirSync(LOG_DIR, { recursive: true });
10
+ const now = new Date();
11
+ const date = now.toISOString().slice(0, 10);
12
+ const timestamp = now.toISOString();
13
+ let entry = `\n## [${date}] ${action} | "${slug}"`;
14
+ if (opts?.mode)
15
+ entry += ` | mode: ${opts.mode}`;
16
+ if (opts?.summary)
17
+ entry += `\n${opts.summary}`;
18
+ entry += `\n<!-- ts:${timestamp} action:${action} slug:${slug}${opts?.mode ? ` mode:${opts.mode}` : ""} -->`;
19
+ entry += "\n";
20
+ appendFileSync(LOG_PATH, entry, "utf-8");
21
+ }
22
+ /** Read and parse the activity log into structured entries */
23
+ export function readLog(opts) {
24
+ if (!existsSync(LOG_PATH))
25
+ return [];
26
+ const content = readFileSync(LOG_PATH, "utf-8");
27
+ const entries = [];
28
+ // Parse structured comment lines
29
+ const commentRegex = /<!-- ts:(\S+) action:(\S+) slug:(\S+)(?: mode:(\S+))? -->/g;
30
+ let match;
31
+ while ((match = commentRegex.exec(content)) !== null) {
32
+ const entry = {
33
+ date: match[1].slice(0, 10),
34
+ timestamp: match[1],
35
+ action: match[2],
36
+ slug: match[3],
37
+ };
38
+ if (match[4])
39
+ entry.mode = match[4];
40
+ // Extract summary from the line before the comment
41
+ const before = content.slice(0, match.index);
42
+ const lastNewline = before.lastIndexOf("\n");
43
+ const secondLastNewline = before.lastIndexOf("\n", lastNewline - 1);
44
+ if (lastNewline > secondLastNewline + 1) {
45
+ const summaryLine = before.slice(secondLastNewline + 1, lastNewline).trim();
46
+ if (summaryLine && !summaryLine.startsWith("##")) {
47
+ entry.summary = summaryLine;
48
+ }
49
+ }
50
+ if (opts?.action && entry.action !== opts.action)
51
+ continue;
52
+ entries.push(entry);
53
+ }
54
+ // Most recent first
55
+ entries.reverse();
56
+ if (opts?.limit)
57
+ return entries.slice(0, opts.limit);
58
+ return entries;
59
+ }
60
+ /** Get the raw log content */
61
+ export function readLogRaw() {
62
+ if (!existsSync(LOG_PATH))
63
+ return "";
64
+ return readFileSync(LOG_PATH, "utf-8");
65
+ }