@context-os/cli 1.0.1 ā 1.6.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.
- package/dist/commands/dashboard.js +68 -0
- package/dist/commands/graph.js +41 -0
- package/dist/commands/index-cmd.js +18 -0
- package/dist/commands/search.js +24 -16
- package/dist/commands/sync.js +7 -25
- package/dist/commands/validate.js +11 -115
- package/dist/commands/watch.js +38 -0
- package/dist/index.js +9 -1
- package/dist/tests/smoke.test.js +17 -2
- package/package.json +6 -4
- package/templates/dashboard/index.html +454 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { exec } from "node:child_process";
|
|
7
|
+
import { samplingService, knowledgeGraphService } from "@context-os/core";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export function dashboardCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("dashboard")
|
|
12
|
+
.description("Launch the Visual Control Center (Web UI)")
|
|
13
|
+
.option("-p, --port <number>", "Port to run the dashboard on", "3010")
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
const port = parseInt(options.port);
|
|
16
|
+
// Determine the template path
|
|
17
|
+
// In dev: src/commands/../../templates/dashboard/index.html
|
|
18
|
+
// In dist: dist/commands/../../templates/dashboard/index.html
|
|
19
|
+
const templatePath = path.resolve(__dirname, "../../templates/dashboard/index.html");
|
|
20
|
+
if (!fs.existsSync(templatePath)) {
|
|
21
|
+
// Fallback if structure varies
|
|
22
|
+
const fallbackPath = path.resolve(process.cwd(), "templates/dashboard/index.html");
|
|
23
|
+
if (!fs.existsSync(fallbackPath)) {
|
|
24
|
+
console.error(chalk.red(`\nā Error: Dashboard template not found at ${templatePath}`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const server = http.createServer(async (req, res) => {
|
|
29
|
+
const url = req.url || "/";
|
|
30
|
+
// API endpoints
|
|
31
|
+
if (url === "/api/pulse") {
|
|
32
|
+
const pulse = await samplingService.getPulse();
|
|
33
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
34
|
+
return res.end(JSON.stringify(pulse));
|
|
35
|
+
}
|
|
36
|
+
if (url === "/api/graph") {
|
|
37
|
+
const graph = await knowledgeGraphService.getGraph();
|
|
38
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
39
|
+
return res.end(JSON.stringify(graph));
|
|
40
|
+
}
|
|
41
|
+
// Static Assets (Dashboard UI)
|
|
42
|
+
if (url === "/" || url === "/index.html") {
|
|
43
|
+
try {
|
|
44
|
+
const html = fs.readFileSync(templatePath, "utf8");
|
|
45
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
46
|
+
return res.end(html);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
res.writeHead(500);
|
|
50
|
+
return res.end("Internal Server Error Loading Dashboard");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
res.writeHead(404);
|
|
54
|
+
res.end("Not Found");
|
|
55
|
+
});
|
|
56
|
+
server.listen(port, () => {
|
|
57
|
+
const dashboardUrl = `http://localhost:${port}`;
|
|
58
|
+
console.log(chalk.bold.cyan("\nš ContextOS Visual Control Center"));
|
|
59
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
60
|
+
console.log(chalk.green(` URL: ${dashboardUrl}`));
|
|
61
|
+
console.log(chalk.dim(" Status: Monitoring Workspace..."));
|
|
62
|
+
console.log(chalk.dim(" Cmd: Press Ctrl+C to stop server\n"));
|
|
63
|
+
// Launch Browser
|
|
64
|
+
const start = (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open');
|
|
65
|
+
exec(`${start} ${dashboardUrl}`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { knowledgeGraphService } from "@context-os/core";
|
|
4
|
+
export function graphCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("graph")
|
|
7
|
+
.description("Visualize the workspace knowledge graph (explicit + semantic links)")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const spinner = ora(`Building workspace graph...`).start();
|
|
10
|
+
try {
|
|
11
|
+
const graph = await knowledgeGraphService.getGraph();
|
|
12
|
+
if (graph.nodes.length === 0) {
|
|
13
|
+
spinner.info(chalk.yellow("Graph is empty. Index some files first."));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
spinner.succeed(chalk.green(`Federated Knowledge Graph (${graph.nodes.length} nodes, ${graph.edges.length} edges):`));
|
|
17
|
+
console.log("");
|
|
18
|
+
// 1. Group by entity type
|
|
19
|
+
const docs = graph.nodes.filter(n => n.type === 'document');
|
|
20
|
+
const tags = graph.nodes.filter(n => n.type === 'tag');
|
|
21
|
+
console.log(chalk.bold.blue("--- Documents ---"));
|
|
22
|
+
docs.forEach(doc => {
|
|
23
|
+
const connections = graph.edges.filter(e => e.source === doc.id || e.target === doc.id);
|
|
24
|
+
const semanticCount = connections.filter(e => e.type === 'semantic').length;
|
|
25
|
+
const linkCount = connections.length - semanticCount;
|
|
26
|
+
console.log(`${chalk.cyan(doc.id)} ${chalk.gray(`(${linkCount} links, ${semanticCount} semantic bridges)`)}`);
|
|
27
|
+
});
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(chalk.bold.yellow("--- Top Themes (#tags) ---"));
|
|
30
|
+
tags.forEach(tag => {
|
|
31
|
+
const usage = graph.edges.filter(e => e.target === tag.id).length;
|
|
32
|
+
console.log(`${chalk.yellow(tag.label)} ${chalk.gray(`(used in ${usage} files)`)}`);
|
|
33
|
+
});
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(chalk.dim("Run `context search <query>` to explore semantic neighborhoods."));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
spinner.fail(chalk.red(`Graph generation failed: ${error.message}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { globalIndexer } from "@context-os/core";
|
|
4
|
+
export function indexCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("index")
|
|
7
|
+
.description("Rebuild the workspace intelligence index")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const spinner = ora("Indexing workspace context...").start();
|
|
10
|
+
try {
|
|
11
|
+
const index = await globalIndexer.reindex();
|
|
12
|
+
spinner.succeed(chalk.green(`Index rebuilt: ${index.records.length} files processed.`));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
spinner.fail(chalk.red(`Indexing failed: ${error.message}`));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
const execAsync = promisify(exec);
|
|
3
|
+
import { intelligenceService } from "@context-os/core";
|
|
6
4
|
export function searchCommand(program) {
|
|
7
5
|
program
|
|
8
6
|
.command("search")
|
|
9
|
-
.description("Search across the workspace")
|
|
7
|
+
.description("Search across the workspace (using metadata index)")
|
|
10
8
|
.argument("<query>", "Search query string")
|
|
11
|
-
.
|
|
9
|
+
.option("--deep", "Force deep scan using grep")
|
|
10
|
+
.action(async (query, options) => {
|
|
12
11
|
const spinner = ora(`Searching for ${chalk.cyan(query)}...`).start();
|
|
13
12
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const command = `grep -rnIE "${query}" . | head -n 20`;
|
|
17
|
-
const { stdout } = await execAsync(command, { cwd: workspaceRoot });
|
|
18
|
-
if (!stdout) {
|
|
13
|
+
const results = await intelligenceService.search(query, { deep: options.deep });
|
|
14
|
+
if (results.length === 0) {
|
|
19
15
|
spinner.info(chalk.yellow("No results found."));
|
|
20
16
|
return;
|
|
21
17
|
}
|
|
22
|
-
spinner.succeed(chalk.green(
|
|
23
|
-
console.log(
|
|
18
|
+
spinner.succeed(chalk.green(`Found ${results.length} matches:`));
|
|
19
|
+
console.log("");
|
|
20
|
+
results.forEach(res => {
|
|
21
|
+
let typeTag = chalk.cyan('[Index]');
|
|
22
|
+
if (res.type === 'deep')
|
|
23
|
+
typeTag = chalk.magenta('[Deep]');
|
|
24
|
+
if (res.type === 'semantic')
|
|
25
|
+
typeTag = chalk.bold.green('[Semantic]');
|
|
26
|
+
if (res.type === 'hybrid')
|
|
27
|
+
typeTag = chalk.bold.yellow('[Hybrid]');
|
|
28
|
+
const scoreDisplay = res.score ? chalk.green(` (${res.score.toFixed(2)})`) : '';
|
|
29
|
+
console.log(`${typeTag}${scoreDisplay} ${chalk.blue(res.path)}`);
|
|
30
|
+
if (res.title && res.title !== 'Deep Scan Result') {
|
|
31
|
+
console.log(`${chalk.gray(res.title)} ${res.tags.length ? chalk.yellow(`[${res.tags.join(', ')}]`) : ''}`);
|
|
32
|
+
}
|
|
33
|
+
console.log(`${chalk.white(res.excerpt)}...`);
|
|
34
|
+
console.log(chalk.gray('---'));
|
|
35
|
+
});
|
|
24
36
|
}
|
|
25
37
|
catch (error) {
|
|
26
|
-
if (error.code === 1) {
|
|
27
|
-
spinner.info(chalk.yellow("No results found."));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
38
|
spinner.fail(chalk.red(`Search failed: ${error.message}`));
|
|
31
39
|
}
|
|
32
40
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import
|
|
4
|
-
import path from "path";
|
|
3
|
+
import { workspaceService } from "@context-os/core";
|
|
5
4
|
export function syncCommand(program) {
|
|
6
5
|
program
|
|
7
6
|
.command("sync")
|
|
8
7
|
.description("Sync memory, changelog, and daily logs")
|
|
9
8
|
.argument("[project]", "Project name to sync context for")
|
|
10
|
-
.
|
|
9
|
+
.option("--force", "Force full re-index of the workspace")
|
|
10
|
+
.action(async (project, options) => {
|
|
11
11
|
const spinner = ora("Syncing workspace context...").start();
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (project) {
|
|
17
|
-
const projectDir = path.join(workspaceRoot, "projects", project);
|
|
18
|
-
const memoryPath = path.join(projectDir, "memory.md");
|
|
19
|
-
if (await fs.pathExists(memoryPath)) {
|
|
20
|
-
let content = await fs.readFile(memoryPath, "utf-8");
|
|
21
|
-
const syncMark = `\n> [!NOTE]\n> Last Sync: ${date} ${new Date().toLocaleTimeString()}\n`;
|
|
22
|
-
if (!content.includes("Last Sync:")) {
|
|
23
|
-
await fs.appendFile(memoryPath, syncMark);
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
content = content.replace(/> \[!NOTE\]\n> Last Sync: .*/, syncMark.trim());
|
|
27
|
-
await fs.writeFile(memoryPath, content);
|
|
28
|
-
}
|
|
29
|
-
spinner.succeed(chalk.green(`Synced memory for ${project}`));
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
spinner.fail(chalk.red(`Memory file not found for ${project}`));
|
|
33
|
-
}
|
|
13
|
+
const result = await workspaceService.sync(project, { force: options.force });
|
|
14
|
+
if (result.success) {
|
|
15
|
+
spinner.succeed(chalk.green(result.message));
|
|
34
16
|
}
|
|
35
17
|
else {
|
|
36
|
-
spinner.
|
|
18
|
+
spinner.fail(chalk.red(result.message));
|
|
37
19
|
}
|
|
38
20
|
}
|
|
39
21
|
catch (error) {
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import
|
|
4
|
-
import path from "path";
|
|
5
|
-
import AjvModule from "ajv";
|
|
6
|
-
import addFormatsModule from "ajv-formats";
|
|
7
|
-
import fm from "front-matter";
|
|
8
|
-
const Ajv = AjvModule.default || AjvModule;
|
|
9
|
-
const addFormats = addFormatsModule.default || addFormatsModule;
|
|
10
|
-
const ajv = new Ajv({ allErrors: true });
|
|
11
|
-
addFormats(ajv);
|
|
3
|
+
import { validationService } from "@context-os/core";
|
|
12
4
|
export function validateCommand(program) {
|
|
13
5
|
program
|
|
14
6
|
.command("validate")
|
|
@@ -16,80 +8,19 @@ export function validateCommand(program) {
|
|
|
16
8
|
.action(async () => {
|
|
17
9
|
const spinner = ora("Validating workspace integrity...").start();
|
|
18
10
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const root = "/";
|
|
22
|
-
while (current !== root) {
|
|
23
|
-
if (fs.existsSync(path.join(current, "root", "soul.md"))) {
|
|
24
|
-
return current;
|
|
25
|
-
}
|
|
26
|
-
current = path.dirname(current);
|
|
27
|
-
}
|
|
28
|
-
return process.cwd();
|
|
29
|
-
};
|
|
30
|
-
const workspaceRoot = findWorkspaceRoot();
|
|
31
|
-
const schemasDir = path.join(workspaceRoot, "schemas");
|
|
32
|
-
const projectsDir = path.join(workspaceRoot, "projects");
|
|
33
|
-
const starterDir = path.join(workspaceRoot, "workspace-starter");
|
|
34
|
-
if (!(await fs.pathExists(schemasDir))) {
|
|
35
|
-
spinner.fail(chalk.red(`Schemas directory not found at ${schemasDir}`));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
const validFiles = [
|
|
39
|
-
{ name: "SOUL.md", schema: "soul.schema.json", required: true },
|
|
40
|
-
{ name: "CONTEXT.md", schema: "context.schema.json", required: true },
|
|
41
|
-
{ name: "memory.md", schema: "memory.schema.json" },
|
|
42
|
-
{ name: "decisions.md", schema: "decision.schema.json" }
|
|
43
|
-
];
|
|
44
|
-
let totalIssues = 0;
|
|
45
|
-
// Find all project-like directories (projects/* and workspace-starter)
|
|
46
|
-
const projectPaths = [];
|
|
47
|
-
if (await fs.pathExists(projectsDir)) {
|
|
48
|
-
const projects = await fs.readdir(projectsDir);
|
|
49
|
-
for (const p of projects) {
|
|
50
|
-
const fullPath = path.join(projectsDir, p);
|
|
51
|
-
if ((await fs.stat(fullPath)).isDirectory()) {
|
|
52
|
-
projectPaths.push(fullPath);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (await fs.pathExists(starterDir)) {
|
|
57
|
-
projectPaths.push(starterDir);
|
|
58
|
-
}
|
|
59
|
-
for (const projectPath of projectPaths) {
|
|
60
|
-
const projectName = path.basename(projectPath);
|
|
61
|
-
const filesInDir = await fs.readdir(projectPath);
|
|
62
|
-
for (const config of validFiles) {
|
|
63
|
-
// Case-insensitive search
|
|
64
|
-
const fileName = filesInDir.find(f => f.toLowerCase() === config.name.toLowerCase());
|
|
65
|
-
if (!fileName) {
|
|
66
|
-
if (config.required) {
|
|
67
|
-
totalIssues++;
|
|
68
|
-
console.log(chalk.red(`\nā Missing required file in ${projectName}: ${config.name}`));
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const filePath = path.join(projectPath, fileName);
|
|
73
|
-
const schemaPath = path.join(schemasDir, config.schema);
|
|
74
|
-
const schema = await fs.readJson(schemaPath);
|
|
75
|
-
const validate = ajv.compile(schema);
|
|
76
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
77
|
-
const data = extractMetadata(content);
|
|
78
|
-
const valid = validate(data);
|
|
79
|
-
if (!valid) {
|
|
80
|
-
totalIssues++;
|
|
81
|
-
console.log(chalk.red(`\nā Schema error in ${projectName}/${fileName}:`));
|
|
82
|
-
validate.errors?.forEach((err) => {
|
|
83
|
-
console.log(chalk.yellow(` - ${err.instancePath || 'root'} ${err.message}`));
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (totalIssues === 0) {
|
|
11
|
+
const result = await validationService.validateWorkspace();
|
|
12
|
+
if (result.valid) {
|
|
89
13
|
spinner.succeed(chalk.green("Workspace validation successful! All files conform to schema."));
|
|
90
14
|
}
|
|
91
15
|
else {
|
|
92
|
-
spinner.fail(chalk.red(`Workspace validation failed with ${
|
|
16
|
+
spinner.fail(chalk.red(`Workspace validation failed with ${result.issues.length} issues.`));
|
|
17
|
+
console.log("");
|
|
18
|
+
result.issues.forEach(issue => {
|
|
19
|
+
console.log(chalk.red(`\nā ${issue.message} in ${issue.project}/${issue.file}`));
|
|
20
|
+
if (issue.details) {
|
|
21
|
+
// Optional: console.log(chalk.yellow(` - detail: ${JSON.stringify(issue.details)}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
93
24
|
process.exit(1);
|
|
94
25
|
}
|
|
95
26
|
}
|
|
@@ -99,38 +30,3 @@ export function validateCommand(program) {
|
|
|
99
30
|
}
|
|
100
31
|
});
|
|
101
32
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Robustly extract metadata from a markdown file.
|
|
104
|
-
* Prioritizes Frontmatter, falls back to Section mapping.
|
|
105
|
-
*/
|
|
106
|
-
function extractMetadata(content) {
|
|
107
|
-
let data = {};
|
|
108
|
-
try {
|
|
109
|
-
const parse = fm.default || fm;
|
|
110
|
-
if (content.trim().startsWith("---")) {
|
|
111
|
-
const parsed = parse(content);
|
|
112
|
-
data = parsed.attributes || {};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (e) {
|
|
116
|
-
console.error(chalk.yellow(` ! Frontmatter parse failed, falling back to sections.`));
|
|
117
|
-
}
|
|
118
|
-
// SUPPLEMENT with section parsing if frontmatter is missing fields
|
|
119
|
-
const sections = content.split(/^## /m).slice(1);
|
|
120
|
-
sections.forEach(s => {
|
|
121
|
-
const lines = s.split("\n");
|
|
122
|
-
const title = lines[0].trim();
|
|
123
|
-
const body = lines.slice(1).join("\n").trim();
|
|
124
|
-
if (!data[title] || (Array.isArray(data[title]) && data[title].length === 0)) {
|
|
125
|
-
if (["Core Principles", "Behavioral Rules", "Goals", "Capabilities", "Constraints", "Tags", "Active Tasks", "Backlog"].includes(title)) {
|
|
126
|
-
data[title] = body.split("\n")
|
|
127
|
-
.map(l => l.replace(/^[-*]\s*/, "").trim())
|
|
128
|
-
.filter(l => l.length > 0);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
data[title] = body;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return data;
|
|
136
|
-
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { watchService, globalIndexer, samplingService } from "@context-os/core";
|
|
4
|
+
export function watchCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("watch")
|
|
7
|
+
.description("Start real-time workspace monitoring and intelligence sync")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
console.log(chalk.bold.cyan("\nš” ContextOS Intelligence Monitor v1.5.0"));
|
|
10
|
+
console.log(chalk.dim("Press Ctrl+C to stop monitoring\n"));
|
|
11
|
+
const spinner = ora("Initializing workspace sweep...").start();
|
|
12
|
+
try {
|
|
13
|
+
// 1. Initial Incremental Sweep (as approved in plan)
|
|
14
|
+
await globalIndexer.reindex();
|
|
15
|
+
spinner.succeed("Initial sweep complete. Workspace is synchronized.");
|
|
16
|
+
// 2. Start Watch Service
|
|
17
|
+
watchService.start();
|
|
18
|
+
const pulse = await samplingService.getPulse();
|
|
19
|
+
console.log(chalk.green(`\nā
Intelligence Layer Active`));
|
|
20
|
+
console.log(chalk.dim(` - Health Score: ${pulse.healthScore}%`));
|
|
21
|
+
console.log(chalk.dim(` - Nodes Mapped: ${pulse.recentChanges.length}+`));
|
|
22
|
+
console.log("");
|
|
23
|
+
const statusSpinner = ora("Monitoring for changes...").start();
|
|
24
|
+
// 3. Handle graceful shutdown
|
|
25
|
+
process.on("SIGINT", () => {
|
|
26
|
+
statusSpinner.stop();
|
|
27
|
+
watchService.stop();
|
|
28
|
+
console.log(chalk.yellow("\nš Watch service stopped. Workspace remains indexed."));
|
|
29
|
+
process.exit(0);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
spinner.fail("Failed to start watch service.");
|
|
34
|
+
console.error(chalk.red(error));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,15 @@ import { healthCommand } from "./commands/health.js";
|
|
|
14
14
|
import { extractCommand } from "./commands/extract.js";
|
|
15
15
|
import { tagCommand } from "./commands/tag.js";
|
|
16
16
|
import { validateCommand } from "./commands/validate.js";
|
|
17
|
+
import { indexCommand } from "./commands/index-cmd.js";
|
|
18
|
+
import { graphCommand } from "./commands/graph.js";
|
|
19
|
+
import { watchCommand } from "./commands/watch.js";
|
|
20
|
+
import { dashboardCommand } from "./commands/dashboard.js";
|
|
17
21
|
const program = new Command();
|
|
18
22
|
program
|
|
19
23
|
.name("workspace")
|
|
20
24
|
.description("ContextOS Developer Interface Layer")
|
|
21
|
-
.version("1.0
|
|
25
|
+
.version("1.1.0");
|
|
22
26
|
// Register Commands
|
|
23
27
|
initCommand(program);
|
|
24
28
|
todayCommand(program);
|
|
@@ -34,6 +38,10 @@ healthCommand(program);
|
|
|
34
38
|
extractCommand(program);
|
|
35
39
|
tagCommand(program);
|
|
36
40
|
validateCommand(program);
|
|
41
|
+
indexCommand(program);
|
|
42
|
+
graphCommand(program);
|
|
43
|
+
watchCommand(program);
|
|
44
|
+
dashboardCommand(program);
|
|
37
45
|
program.parse(process.argv);
|
|
38
46
|
if (!process.argv.slice(2).length) {
|
|
39
47
|
program.outputHelp();
|
package/dist/tests/smoke.test.js
CHANGED
|
@@ -21,8 +21,23 @@ describe("CLI Experience Layer (Smoke Tests)", () => {
|
|
|
21
21
|
assert.ok(output.includes(cmd));
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
|
-
it("should report version 1.0
|
|
24
|
+
it("should report version 1.1.0", () => {
|
|
25
25
|
const output = execSync(`node ${cliPath} --version`).toString();
|
|
26
|
-
assert.strictEqual(output.trim(), "1.0
|
|
26
|
+
assert.strictEqual(output.trim(), "1.1.0");
|
|
27
|
+
});
|
|
28
|
+
describe("Functional Commands", () => {
|
|
29
|
+
it("should perform a workspace sync", () => {
|
|
30
|
+
const output = execSync(`node ${cliPath} sync 2>&1`).toString();
|
|
31
|
+
assert.ok(output.includes("Workspace indexed"), "Output should contain success message");
|
|
32
|
+
});
|
|
33
|
+
it("should search for indexed content", () => {
|
|
34
|
+
const output = execSync(`node ${cliPath} search "ContextOS" 2>&1`).toString();
|
|
35
|
+
assert.ok(output.includes("Found"), "Output should list search results");
|
|
36
|
+
assert.ok(output.includes("ContextOS"), "Output should contain 'ContextOS'");
|
|
37
|
+
});
|
|
38
|
+
it("should validate the workspace root", () => {
|
|
39
|
+
const output = execSync(`node ${cliPath} validate 2>&1`).toString();
|
|
40
|
+
assert.ok(output.includes("successful"), "Output should contain validation report");
|
|
41
|
+
});
|
|
27
42
|
});
|
|
28
43
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-os/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "ContextOS Developer Interface Layer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"context-os",
|
|
@@ -26,10 +26,12 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"postbuild": "node ../scripts/sync-templates.js",
|
|
30
31
|
"watch": "tsc -w",
|
|
31
32
|
"start": "node dist/index.js",
|
|
32
|
-
"test": "
|
|
33
|
+
"test": "mocha dist/tests/**/*.test.js",
|
|
34
|
+
"validate": "node dist/index.js validate"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"ajv": "^8.18.0",
|
|
@@ -40,7 +42,7 @@
|
|
|
40
42
|
"front-matter": "^4.0.2",
|
|
41
43
|
"fs-extra": "^11.2.0",
|
|
42
44
|
"ora": "^8.0.1",
|
|
43
|
-
"@context-os/core": "1.
|
|
45
|
+
"@context-os/core": "1.6.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/chai": "^5.2.3",
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ContextOS | Visual Control Center</title>
|
|
7
|
+
<!-- Premium CDNs -->
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet">
|
|
11
|
+
<script src="https://unpkg.com/3d-force-graph"></script>
|
|
12
|
+
<script src="https://unpkg.com/force-graph"></script>
|
|
13
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #0c1324;
|
|
17
|
+
--surface: rgba(12, 19, 36, 0.6);
|
|
18
|
+
--surface-bright: rgba(25, 31, 49, 0.8);
|
|
19
|
+
--primary: #00f0ff;
|
|
20
|
+
--secondary: #a855f7;
|
|
21
|
+
--text: #dce1fb;
|
|
22
|
+
--text-dim: #849495;
|
|
23
|
+
--accent: #ff0055;
|
|
24
|
+
--glow: 0 0 20px rgba(0, 240, 255, 0.3);
|
|
25
|
+
--font-display: 'Space Grotesk', sans-serif;
|
|
26
|
+
--font-body: 'Inter', sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body, html {
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 0;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
background: var(--bg);
|
|
35
|
+
color: var(--text);
|
|
36
|
+
font-family: var(--font-body);
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
background-image:
|
|
39
|
+
radial-gradient(circle at 50% -20%, #1a2a44 0%, transparent 50%),
|
|
40
|
+
radial-gradient(circle at -10% 50%, #151b2d 0%, transparent 40%);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Scanline Effect */
|
|
44
|
+
body::before {
|
|
45
|
+
content: " ";
|
|
46
|
+
display: block;
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 0; left: 0; bottom: 0; right: 0;
|
|
49
|
+
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
|
50
|
+
z-index: 100;
|
|
51
|
+
background-size: 100% 2px, 3px 100%;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
opacity: 0.15;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Glassmorphic UI Overlay */
|
|
57
|
+
#ui-layer {
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
z-index: 10;
|
|
62
|
+
display: grid;
|
|
63
|
+
grid-template-areas:
|
|
64
|
+
"header header header"
|
|
65
|
+
"sidebar content inspector"
|
|
66
|
+
"footer footer footer";
|
|
67
|
+
grid-template-rows: auto 1fr auto;
|
|
68
|
+
grid-template-columns: 320px 1fr 320px;
|
|
69
|
+
padding: 24px;
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
gap: 20px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.hud-card {
|
|
75
|
+
background: var(--surface);
|
|
76
|
+
backdrop-filter: blur(20px);
|
|
77
|
+
border: 1px solid rgba(132, 148, 149, 0.15);
|
|
78
|
+
border-radius: 12px;
|
|
79
|
+
padding: 20px;
|
|
80
|
+
pointer-events: auto;
|
|
81
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
82
|
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hud-card:hover {
|
|
86
|
+
border-color: rgba(0, 240, 255, 0.4);
|
|
87
|
+
background: var(--surface-bright);
|
|
88
|
+
transform: translateY(-2px);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
header {
|
|
92
|
+
grid-area: header;
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#brand h1 {
|
|
99
|
+
font-family: var(--font-display);
|
|
100
|
+
margin: 0;
|
|
101
|
+
font-size: 24px;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
letter-spacing: 3px;
|
|
104
|
+
text-transform: uppercase;
|
|
105
|
+
color: var(--primary);
|
|
106
|
+
text-shadow: 0 0 15px rgba(0, 240, 255, 0.5);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.health-gauge {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 20px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Sidebar & Inspector */
|
|
116
|
+
#sidebar { grid-area: sidebar; display: flex; flex-direction: column; gap: 20px; }
|
|
117
|
+
#inspector {
|
|
118
|
+
grid-area: inspector;
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 20px;
|
|
122
|
+
transform: translateX(350px);
|
|
123
|
+
transition: transform 0.5s ease;
|
|
124
|
+
}
|
|
125
|
+
#inspector.active { transform: translateX(0); }
|
|
126
|
+
|
|
127
|
+
.stat-label {
|
|
128
|
+
font-family: var(--font-display);
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
color: var(--primary);
|
|
132
|
+
letter-spacing: 2px;
|
|
133
|
+
margin-bottom: 15px;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
opacity: 0.8;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tag-pill {
|
|
141
|
+
display: inline-block;
|
|
142
|
+
padding: 5px 12px;
|
|
143
|
+
background: rgba(168, 85, 247, 0.1);
|
|
144
|
+
border: 1px solid rgba(168, 85, 247, 0.3);
|
|
145
|
+
border-radius: 20px;
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
margin: 0 6px 6px 0;
|
|
148
|
+
color: #d8b4ff;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.tag-pill:hover { background: rgba(168, 85, 247, 0.3); }
|
|
153
|
+
|
|
154
|
+
/* Bottom Status Bar */
|
|
155
|
+
footer {
|
|
156
|
+
grid-area: footer;
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: space-between;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.ticker-text {
|
|
163
|
+
font-family: var(--font-display);
|
|
164
|
+
font-size: 10px;
|
|
165
|
+
color: var(--primary);
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
width: 300px;
|
|
169
|
+
letter-spacing: 1px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* 3D Graph Container */
|
|
173
|
+
#graph-container {
|
|
174
|
+
position: absolute;
|
|
175
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
176
|
+
z-index: 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.node-tooltip {
|
|
180
|
+
background: rgba(12, 19, 36, 0.95);
|
|
181
|
+
border: 1px solid var(--primary);
|
|
182
|
+
padding: 12px;
|
|
183
|
+
border-radius: 8px;
|
|
184
|
+
backdrop-filter: blur(10px);
|
|
185
|
+
font-family: var(--font-body);
|
|
186
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.pulse-dot {
|
|
190
|
+
width: 8px;
|
|
191
|
+
height: 8px;
|
|
192
|
+
border-radius: 50%;
|
|
193
|
+
background: var(--primary);
|
|
194
|
+
box-shadow: 0 0 10px var(--primary);
|
|
195
|
+
animation: pulse 2s infinite;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes pulse {
|
|
199
|
+
0% { opacity: 1; transform: scale(1); }
|
|
200
|
+
50% { opacity: 0.5; transform: scale(0.8); }
|
|
201
|
+
100% { opacity: 1; transform: scale(1); }
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
204
|
+
</head>
|
|
205
|
+
<body>
|
|
206
|
+
<div id="graph-container"></div>
|
|
207
|
+
|
|
208
|
+
<div id="ui-layer">
|
|
209
|
+
<header>
|
|
210
|
+
<div class="hud-card" id="brand">
|
|
211
|
+
<i data-lucide="brain-circuit" style="color: var(--primary);"></i>
|
|
212
|
+
<div>
|
|
213
|
+
<h1>ContextOS</h1>
|
|
214
|
+
<div style="font-size: 10px; color: var(--text-dim); letter-spacing: 1px;">Spatial Intelligence Protocol</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div class="hud-card health-gauge">
|
|
219
|
+
<div style="text-align: right;">
|
|
220
|
+
<div class="stat-label" style="margin:0">Workspace Stability</div>
|
|
221
|
+
<div style="font-size: 18px; font-weight: 700; color: var(--primary); font-family: var(--font-display);" id="health-text">--%</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div id="health-ring" style="width:40px; height:40px; border-width: 2px;">
|
|
224
|
+
<div id="health-value" style="font-size: 12px;">--</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</header>
|
|
228
|
+
|
|
229
|
+
<aside id="sidebar">
|
|
230
|
+
<div class="hud-card">
|
|
231
|
+
<div class="stat-label"><i data-lucide="zap" size="14"></i> Trending Context</div>
|
|
232
|
+
<div id="tag-container"></div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="hud-card" style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
236
|
+
<div class="stat-label"><i data-lucide="radio" size="14"></i> Live Activity</div>
|
|
237
|
+
<div id="activity-list" style="flex:1; overflow-y: auto; padding-right: 8px;"></div>
|
|
238
|
+
</div>
|
|
239
|
+
</aside>
|
|
240
|
+
|
|
241
|
+
<section id="inspector" class="hud-card">
|
|
242
|
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
|
243
|
+
<div class="stat-label" style="margin:0"><i data-lucide="info" size="14"></i> Node Inspector</div>
|
|
244
|
+
<button onclick="closeInspector()" style="background: none; border: none; color: var(--text-dim); cursor: pointer;"><i data-lucide="x" size="16"></i></button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div id="inspector-content">
|
|
248
|
+
<div style="text-align: center; padding: 40px 20px; opacity: 0.4;">
|
|
249
|
+
<i data-lucide="mouse-pointer-2" size="32" style="margin-bottom: 16px;"></i>
|
|
250
|
+
<p style="font-size: 12px;">Select an entity to reveal its spatial soul</p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
|
|
255
|
+
<footer>
|
|
256
|
+
<div class="hud-card" style="display: flex; align-items: center; gap: 15px; padding: 8px 16px;">
|
|
257
|
+
<div class="pulse-dot"></div>
|
|
258
|
+
<div class="ticker-text" id="system-ticker">INITIALIZING AETHER CORE... READY.</div>
|
|
259
|
+
<span id="timestamp" style="font-family: var(--font-display); font-size: 12px;">--:--:--</span>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="hud-card" style="padding: 8px 16px; font-family: var(--font-display); letter-spacing: 1px; font-size: 10px;">
|
|
263
|
+
DECK: <span style="color: var(--primary)">V1.6.1-AETHER</span>
|
|
264
|
+
</div>
|
|
265
|
+
</footer>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<script>
|
|
269
|
+
lucide.createIcons();
|
|
270
|
+
|
|
271
|
+
let Graph;
|
|
272
|
+
let selectedNode = null;
|
|
273
|
+
|
|
274
|
+
async function init() {
|
|
275
|
+
console.log("Initializing Spatial Engine...");
|
|
276
|
+
const container = document.getElementById('graph-container');
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
if (window.ForceGraph3D) {
|
|
280
|
+
Graph = ForceGraph3D()(container)
|
|
281
|
+
.backgroundColor('rgba(0,0,0,0)') // Use CSS background
|
|
282
|
+
.showNavInfo(false)
|
|
283
|
+
.nodeLabel(null) // Custom tooltips instead
|
|
284
|
+
.nodeAutoColorBy('type')
|
|
285
|
+
.linkWidth(1)
|
|
286
|
+
.linkColor(() => 'rgba(255,255,255,0.1)')
|
|
287
|
+
.linkDirectionalParticles(2)
|
|
288
|
+
.linkDirectionalParticleSpeed(d => (d.weight || 1) * 0.005)
|
|
289
|
+
.linkDirectionalParticleWidth(2)
|
|
290
|
+
.onNodeClick(node => inspectNode(node))
|
|
291
|
+
.onBackgroundClick(() => closeInspector());
|
|
292
|
+
|
|
293
|
+
// Aether Custom Geometries
|
|
294
|
+
Graph.nodeThreeObject(node => {
|
|
295
|
+
let geometry;
|
|
296
|
+
let color = node.type === 'tag' ? '#a855f7' : '#00f0ff';
|
|
297
|
+
|
|
298
|
+
if (node.type === 'tag') {
|
|
299
|
+
geometry = new THREE.TetrahedronGeometry(8);
|
|
300
|
+
} else if (node.type === 'person') {
|
|
301
|
+
geometry = new THREE.OctahedronGeometry(10);
|
|
302
|
+
color = '#ffcc00';
|
|
303
|
+
} else {
|
|
304
|
+
geometry = new THREE.SphereGeometry(12, 16, 16);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const material = new THREE.MeshPhongMaterial({
|
|
308
|
+
color: color,
|
|
309
|
+
transparent: true,
|
|
310
|
+
opacity: 0.9,
|
|
311
|
+
shininess: 100,
|
|
312
|
+
emissive: color,
|
|
313
|
+
emissiveIntensity: 0.5
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return new THREE.Mesh(geometry, material);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Lighting & Atmosphere
|
|
320
|
+
const scene = Graph.scene();
|
|
321
|
+
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
|
|
322
|
+
const point = new THREE.PointLight(0x00f0ff, 1, 500);
|
|
323
|
+
point.position.set(100, 100, 100);
|
|
324
|
+
scene.add(ambient);
|
|
325
|
+
scene.add(point);
|
|
326
|
+
|
|
327
|
+
// Auto-Orbit
|
|
328
|
+
let angle = 0;
|
|
329
|
+
setInterval(() => {
|
|
330
|
+
if (!selectedNode) {
|
|
331
|
+
angle += 0.001;
|
|
332
|
+
Graph.cameraPosition({
|
|
333
|
+
x: 400 * Math.sin(angle),
|
|
334
|
+
z: 400 * Math.cos(angle)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}, 50);
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.warn("Aether 3D Engine failed, engaging 2D fallback:", e);
|
|
341
|
+
container.innerHTML = '';
|
|
342
|
+
Graph = ForceGraph()(container)
|
|
343
|
+
.backgroundColor('rgba(0,0,0,0)')
|
|
344
|
+
.nodeLabel(node => `<div class="node-tooltip"><b>${node.label}</b><br/>${node.id}</div>`)
|
|
345
|
+
.nodeAutoColorBy('type')
|
|
346
|
+
.linkDirectionalParticles(2)
|
|
347
|
+
.linkDirectionalParticleSpeed(d => (d.weight || 1) * 0.01)
|
|
348
|
+
.onNodeClick(node => inspectNode(node));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await updateData();
|
|
352
|
+
setInterval(updateData, 5000);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function inspectNode(node) {
|
|
356
|
+
selectedNode = node;
|
|
357
|
+
const inspector = document.getElementById('inspector');
|
|
358
|
+
const content = document.getElementById('inspector-content');
|
|
359
|
+
|
|
360
|
+
inspector.classList.add('active');
|
|
361
|
+
|
|
362
|
+
content.innerHTML = `
|
|
363
|
+
<div style="padding: 10px;">
|
|
364
|
+
<div style="color: var(--primary); font-family: var(--font-display); font-size: 18px; margin-bottom: 5px;">${node.label}</div>
|
|
365
|
+
<div style="font-size: 11px; opacity: 0.5; margin-bottom: 20px; font-family: monospace;">${node.id}</div>
|
|
366
|
+
|
|
367
|
+
<div class="stat-label" style="font-size: 9px;">Intelligence Metadata</div>
|
|
368
|
+
<div class="hud-card" style="margin-bottom: 15px; background: rgba(0,0,0,0.2); padding: 12px;">
|
|
369
|
+
<div style="font-size: 12px; line-height: 1.6;">
|
|
370
|
+
${node.metadata?.excerpt || 'Scanning for latent semantic patterns...'}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div class="stat-label" style="font-size: 9px;">Entity Classification</div>
|
|
375
|
+
<div style="margin-bottom: 20px;">
|
|
376
|
+
<span class="tag-pill" style="border-color: var(--primary); color: var(--primary);">Type: ${node.type}</span>
|
|
377
|
+
<span class="tag-pill">Access: Federated</span>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div style="display: flex; gap: 10px;">
|
|
381
|
+
<button class="hud-card" style="flex:1; padding: 10px; font-size: 11px; text-transform: uppercase; cursor: pointer;" onclick="copyPath('${node.id}')">
|
|
382
|
+
<i data-lucide="copy" size="12"></i> Copy Path
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
`;
|
|
387
|
+
lucide.createIcons();
|
|
388
|
+
|
|
389
|
+
// Focus camera in 3D
|
|
390
|
+
if (Graph && Graph.cameraPosition) {
|
|
391
|
+
const distance = 150;
|
|
392
|
+
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
|
|
393
|
+
Graph.cameraPosition(
|
|
394
|
+
{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio },
|
|
395
|
+
node,
|
|
396
|
+
2000
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function closeInspector() {
|
|
402
|
+
selectedNode = null;
|
|
403
|
+
document.getElementById('inspector').classList.remove('active');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function copyPath(id) {
|
|
407
|
+
navigator.clipboard.writeText(id);
|
|
408
|
+
const ticker = document.getElementById('system-ticker');
|
|
409
|
+
ticker.textContent = `SYSTEM CLASSIFIER: PATH COPIED [${id}]`;
|
|
410
|
+
setTimeout(() => { ticker.textContent = "IDLE MONITORS ACTIVE... STANDBY."; }, 3000);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function updateData() {
|
|
414
|
+
try {
|
|
415
|
+
const [graphRes, pulseRes] = await Promise.all([
|
|
416
|
+
fetch('/api/graph').then(r => r.json()),
|
|
417
|
+
fetch('/api/pulse').then(r => r.json())
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
Graph.graphData(graphRes);
|
|
421
|
+
|
|
422
|
+
document.getElementById('health-text').textContent = `${pulseRes.healthScore}%`;
|
|
423
|
+
document.getElementById('health-value').textContent = pulseRes.healthScore;
|
|
424
|
+
document.getElementById('timestamp').textContent = new Date(pulseRes.timestamp).toLocaleTimeString();
|
|
425
|
+
|
|
426
|
+
// Ticker logic
|
|
427
|
+
if (pulseRes.recentChanges.length > 0) {
|
|
428
|
+
document.getElementById('system-ticker').textContent = `PULSE DETECTED: SYNCING [${pulseRes.recentChanges[0]}]`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const tagContainer = document.getElementById('tag-container');
|
|
432
|
+
tagContainer.innerHTML = pulseRes.topTags.map(tag =>
|
|
433
|
+
`<span class="tag-pill">#${tag}</span>`
|
|
434
|
+
).join('');
|
|
435
|
+
|
|
436
|
+
const activityList = document.getElementById('activity-list');
|
|
437
|
+
activityList.innerHTML = pulseRes.recentChanges.map(change =>
|
|
438
|
+
`<div class="hud-card" style="margin-bottom: 10px; padding: 12px; font-size: 11px; box-shadow: none;">
|
|
439
|
+
<div style="color: var(--primary); margin-bottom: 4px; font-family: var(--font-display); font-size: 9px; opacity: 0.6;">RECENT SYNC</div>
|
|
440
|
+
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${change}</div>
|
|
441
|
+
</div>`
|
|
442
|
+
).join('');
|
|
443
|
+
|
|
444
|
+
lucide.createIcons();
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.error("Aether Bridge Failure:", e);
|
|
447
|
+
document.getElementById('system-ticker').textContent = "ERROR: SPATIAL BRIDGE COLLAPSED. RETRYING...";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
init();
|
|
452
|
+
</script>
|
|
453
|
+
</body>
|
|
454
|
+
</html>
|