@dnai/dynamicllm 0.1.1 → 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.
- package/README.md +64 -0
- package/dist/bin/dynamicllm.js +7 -0
- package/dist/cli/formatters.d.ts +11 -0
- package/dist/cli/formatters.js +138 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +345 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/{setup.js → cli/init.js} +20 -46
- package/dist/core/bootstrap.d.ts +14 -0
- package/dist/core/bootstrap.js +38 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/constants.js +71 -0
- package/dist/{lib → core}/fetcher.d.ts +6 -7
- package/dist/core/fetcher.js +247 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +8 -0
- package/dist/{lib → core}/lint.js +0 -5
- package/dist/core/log.d.ts +13 -0
- package/dist/core/log.js +65 -0
- package/dist/core/report.d.ts +5 -0
- package/dist/{lib → core}/report.js +7 -14
- package/dist/{lib → core}/types.d.ts +43 -0
- package/dist/core/types.js +2 -0
- package/dist/{server.js → mcp/server.js} +48 -68
- package/framework/CONVENTIONS.md +40 -0
- package/framework/SYSTEM_PROMPT.md +203 -0
- package/framework/antidotes/constructive-optimism.md +37 -0
- package/framework/antidotes/liberated-agentism.md +37 -0
- package/framework/antidotes/objective-fallibilism.md +39 -0
- package/framework/antidotes/oxidative-creativism.md +41 -0
- package/framework/antidotes/polycentric-nodalism.md +38 -0
- package/framework/antidotes/vertical-authenticism.md +39 -0
- package/framework/lenses/logos.md +32 -0
- package/framework/lenses/mythos.md +32 -0
- package/framework/lenses/pathos.md +32 -0
- package/framework/qualities/beauty.md +29 -0
- package/framework/qualities/infinity.md +29 -0
- package/framework/qualities/love.md +29 -0
- package/framework/qualities/mystery.md +29 -0
- package/framework/qualities/play.md +29 -0
- package/framework/qualities/story.md +29 -0
- package/package.json +29 -5
- package/dist/cli.js +0 -20
- package/dist/lib/fetcher.js +0 -210
- package/dist/lib/report.d.ts +0 -19
- package/dist/lib/types.js +0 -1
- package/dist/setup.d.ts +0 -1
- /package/dist/{cli.d.ts → bin/dynamicllm.d.ts} +0 -0
- /package/dist/{lib → core}/cache.d.ts +0 -0
- /package/dist/{lib → core}/cache.js +0 -0
- /package/dist/{lib → core}/context.d.ts +0 -0
- /package/dist/{lib → core}/context.js +0 -0
- /package/dist/{lib → core}/lint.d.ts +0 -0
- /package/dist/{lib → core}/search.d.ts +0 -0
- /package/dist/{lib → core}/search.js +0 -0
- /package/dist/{index.d.ts → mcp/index.d.ts} +0 -0
- /package/dist/{index.js → mcp/index.js} +0 -0
- /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,
|
|
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
|
-
|
|
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,31 +111,14 @@ async function registerAgent(agent, networkDir, rl) {
|
|
|
120
111
|
}
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
|
-
export async function
|
|
114
|
+
export async function init() {
|
|
124
115
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
125
|
-
const
|
|
116
|
+
const { networkDir: defaultDir } = bootstrap();
|
|
126
117
|
console.log("");
|
|
127
|
-
console.log(" DynamicLLM
|
|
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
122
|
const dirAnswer = await rl.question(` Network directory [${defaultDir}]: `);
|
|
149
123
|
const networkDir = dirAnswer.trim().replace(/^['"]|['"]$/g, "") || defaultDir;
|
|
150
124
|
if (!existsSync(networkDir)) {
|
|
@@ -174,7 +148,7 @@ export async function setup() {
|
|
|
174
148
|
console.log("");
|
|
175
149
|
await registerAgent(selectedAgent.key, networkDir, rl);
|
|
176
150
|
console.log("");
|
|
177
|
-
console.log("
|
|
151
|
+
console.log(" Init complete. Start using DynamicLLM with your AI agent.");
|
|
178
152
|
console.log("");
|
|
179
153
|
rl.close();
|
|
180
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,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;
|
package/dist/core/log.js
ADDED
|
@@ -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
|
+
}
|