@dnai/dynamicllm 0.1.0 → 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.
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} +22 -33
  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,45 +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: "npx",
36
- args: ["-y", "dynamicllm"],
37
- env: {
38
- DYNAMICLLM_NETWORK_DIR: networkDir,
39
- },
40
- },
33
+ dynamicllm: getMcpServerConfig(networkDir),
41
34
  },
42
35
  };
43
36
  }
44
37
  function writeMcpJsonFile(dir, networkDir) {
45
38
  const mcpPath = join(dir, "mcp.json");
46
39
  const mcpConfig = getMcpJson(networkDir);
47
- // If file exists, merge rather than overwrite
48
40
  if (existsSync(mcpPath)) {
49
41
  try {
50
42
  const existing = JSON.parse(readFileSync(mcpPath, "utf-8"));
51
43
  existing.mcpServers = existing.mcpServers ?? {};
52
- existing.mcpServers.dynamicllm =
53
- mcpConfig["mcpServers"];
44
+ existing.mcpServers.dynamicllm = getMcpServerConfig(networkDir);
54
45
  writeFileSync(mcpPath, JSON.stringify(existing, null, 2), "utf-8");
55
46
  return true;
56
47
  }
@@ -58,7 +49,6 @@ function writeMcpJsonFile(dir, networkDir) {
58
49
  return false;
59
50
  }
60
51
  }
61
- // Create directory and file
62
52
  if (!existsSync(dir))
63
53
  mkdirSync(dir, { recursive: true });
64
54
  writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
@@ -69,13 +59,13 @@ async function registerAgent(agent, networkDir, rl) {
69
59
  case "claude": {
70
60
  const { execSync } = await import("node:child_process");
71
61
  try {
72
- execSync(`claude mcp add dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- npx -y dynamicllm`, { stdio: "inherit" });
73
- console.log(" Registered with Claude Code.");
62
+ execSync(`claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm mcp`, { stdio: "inherit" });
63
+ console.log(" Registered with Claude Code (user-level).");
74
64
  }
75
65
  catch {
76
66
  console.log(" Could not auto-register with Claude Code.");
77
67
  console.log(" Run manually:");
78
- console.log(` claude mcp add dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- npx -y dynamicllm`);
68
+ console.log(` claude mcp add --scope user dynamicllm -e DYNAMICLLM_NETWORK_DIR="${networkDir}" -- dynamicllm mcp`);
79
69
  }
80
70
  break;
81
71
  }
@@ -121,17 +111,16 @@ async function registerAgent(agent, networkDir, rl) {
121
111
  }
122
112
  }
123
113
  }
124
- export async function setup() {
114
+ export async function init() {
125
115
  const rl = createInterface({ input: stdin, output: stdout });
126
- const existing = loadConfig();
116
+ const { networkDir: defaultDir } = bootstrap();
127
117
  console.log("");
128
- console.log(" DynamicLLM Setup");
129
- console.log(" ───────────────");
118
+ console.log(" DynamicLLM Init");
119
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
130
120
  console.log("");
131
121
  // 1. Network directory
132
- const defaultDir = existing?.networkDir ?? DEFAULT_NETWORK_DIR;
133
122
  const dirAnswer = await rl.question(` Network directory [${defaultDir}]: `);
134
- const networkDir = dirAnswer.trim() || defaultDir;
123
+ const networkDir = dirAnswer.trim().replace(/^['"]|['"]$/g, "") || defaultDir;
135
124
  if (!existsSync(networkDir)) {
136
125
  const create = await rl.question(` Directory doesn't exist. Create it? [Y/n]: `);
137
126
  if (create.trim().toLowerCase() !== "n") {
@@ -159,7 +148,7 @@ export async function setup() {
159
148
  console.log("");
160
149
  await registerAgent(selectedAgent.key, networkDir, rl);
161
150
  console.log("");
162
- console.log(" Setup complete. Start using DynamicLLM with your AI agent.");
151
+ console.log(" Init complete. Start using DynamicLLM with your AI agent.");
163
152
  console.log("");
164
153
  rl.close();
165
154
  }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReportOptions } from "./types.js";
2
+ export declare function generateReport(opts: ReportOptions): {
3
+ filePath: string;
4
+ content: string;
5
+ };