oioxo-mcp 0.1.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 +57 -0
- package/dist/cli/agents.d.ts +9 -0
- package/dist/cli/agents.js +155 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +86 -0
- package/dist/cli/login.d.ts +1 -0
- package/dist/cli/login.js +125 -0
- package/dist/core/bm25.d.ts +31 -0
- package/dist/core/bm25.js +103 -0
- package/dist/core/capsule.d.ts +34 -0
- package/dist/core/capsule.js +50 -0
- package/dist/core/code-graph.d.ts +29 -0
- package/dist/core/code-graph.js +105 -0
- package/dist/core/files.d.ts +17 -0
- package/dist/core/files.js +88 -0
- package/dist/core/memory.d.ts +12 -0
- package/dist/core/memory.js +55 -0
- package/dist/core/skeleton.d.ts +21 -0
- package/dist/core/skeleton.js +106 -0
- package/dist/gate/account.d.ts +32 -0
- package/dist/gate/account.js +97 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +122 -0
- package/dist/test/core.test.d.ts +1 -0
- package/dist/test/core.test.js +109 -0
- package/dist/test/e2e.test.d.ts +1 -0
- package/dist/test/e2e.test.js +100 -0
- package/package.json +47 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIOXO code graph — ported from the web IDE's Context Compiler. Deterministic,
|
|
3
|
+
* on-device, no model. Builds import/dependency edges between project files so
|
|
4
|
+
* retrieval can EXPAND from task-relevant "anchor" files to the files they
|
|
5
|
+
* actually depend on — graph expansion beats flat term-overlap retrieval.
|
|
6
|
+
* Resolves relative imports against the project's own files (node_modules are
|
|
7
|
+
* not editable and would be noise).
|
|
8
|
+
*/
|
|
9
|
+
import type { ProjectFile } from './files.js';
|
|
10
|
+
/** Pull import/require/dynamic-import specifiers from a source file. */
|
|
11
|
+
export declare function importSpecifiers(content: string): string[];
|
|
12
|
+
/** Resolve a relative import to a project file path (extension + /index tries). */
|
|
13
|
+
export declare function resolveImport(spec: string, fromFile: string, fileSet: Set<string>): string | null;
|
|
14
|
+
export interface CodeGraph {
|
|
15
|
+
/** file → the in-project files it imports (direct dependencies). */
|
|
16
|
+
deps: Map<string, string[]>;
|
|
17
|
+
/** file → the in-project files that import it (dependents / reverse edges). */
|
|
18
|
+
dependents: Map<string, string[]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildGraph(files: ProjectFile[]): CodeGraph;
|
|
21
|
+
/**
|
|
22
|
+
* Expand anchors along graph edges up to `hops` deep — anchors PLUS their
|
|
23
|
+
* (transitive) dependencies, optionally one hop of dependents. Capped.
|
|
24
|
+
*/
|
|
25
|
+
export declare function expandContext(graph: CodeGraph, anchors: string[], opts?: {
|
|
26
|
+
hops?: number;
|
|
27
|
+
includeDependents?: boolean;
|
|
28
|
+
max?: number;
|
|
29
|
+
}): string[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const CODE_RE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
2
|
+
/** Pull import/require/dynamic-import specifiers from a source file. */
|
|
3
|
+
export function importSpecifiers(content) {
|
|
4
|
+
const specs = [];
|
|
5
|
+
const patterns = [
|
|
6
|
+
/import\s+[^'"]*?from\s*['"]([^'"]+)['"]/g,
|
|
7
|
+
/import\s*['"]([^'"]+)['"]/g,
|
|
8
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
9
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
10
|
+
/export\s+[^'"]*?from\s*['"]([^'"]+)['"]/g,
|
|
11
|
+
];
|
|
12
|
+
for (const re of patterns) {
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = re.exec(content)))
|
|
15
|
+
specs.push(m[1]);
|
|
16
|
+
}
|
|
17
|
+
return specs;
|
|
18
|
+
}
|
|
19
|
+
function normalize(p) {
|
|
20
|
+
const parts = p.split('/');
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const part of parts) {
|
|
23
|
+
if (part === '' || part === '.')
|
|
24
|
+
continue;
|
|
25
|
+
if (part === '..')
|
|
26
|
+
out.pop();
|
|
27
|
+
else
|
|
28
|
+
out.push(part);
|
|
29
|
+
}
|
|
30
|
+
return out.join('/');
|
|
31
|
+
}
|
|
32
|
+
const dirOf = (p) => p.split('/').slice(0, -1).join('/');
|
|
33
|
+
/** Resolve a relative import to a project file path (extension + /index tries). */
|
|
34
|
+
export function resolveImport(spec, fromFile, fileSet) {
|
|
35
|
+
if (!spec.startsWith('.'))
|
|
36
|
+
return null;
|
|
37
|
+
const base = normalize(dirOf(fromFile) + '/' + spec);
|
|
38
|
+
const exts = ['', '.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
|
|
39
|
+
for (const e of exts)
|
|
40
|
+
if (fileSet.has(base + e))
|
|
41
|
+
return base + e;
|
|
42
|
+
for (const e of exts.slice(1))
|
|
43
|
+
if (fileSet.has(base + '/index' + e))
|
|
44
|
+
return base + '/index' + e;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export function buildGraph(files) {
|
|
48
|
+
const code = files.filter((f) => CODE_RE.test(f.path));
|
|
49
|
+
const fileSet = new Set(code.map((f) => f.path));
|
|
50
|
+
const deps = new Map();
|
|
51
|
+
const dependents = new Map();
|
|
52
|
+
for (const f of code) {
|
|
53
|
+
const resolved = [];
|
|
54
|
+
for (const spec of importSpecifiers(f.content)) {
|
|
55
|
+
const r = resolveImport(spec, f.path, fileSet);
|
|
56
|
+
if (r && r !== f.path && !resolved.includes(r))
|
|
57
|
+
resolved.push(r);
|
|
58
|
+
}
|
|
59
|
+
deps.set(f.path, resolved);
|
|
60
|
+
for (const r of resolved) {
|
|
61
|
+
const arr = dependents.get(r) ?? [];
|
|
62
|
+
if (!arr.includes(f.path))
|
|
63
|
+
arr.push(f.path);
|
|
64
|
+
dependents.set(r, arr);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { deps, dependents };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Expand anchors along graph edges up to `hops` deep — anchors PLUS their
|
|
71
|
+
* (transitive) dependencies, optionally one hop of dependents. Capped.
|
|
72
|
+
*/
|
|
73
|
+
export function expandContext(graph, anchors, opts = {}) {
|
|
74
|
+
const hops = opts.hops ?? 2;
|
|
75
|
+
const max = opts.max ?? 12;
|
|
76
|
+
const seen = new Set(anchors);
|
|
77
|
+
let frontier = [...anchors];
|
|
78
|
+
for (let h = 0; h < hops && seen.size < max; h++) {
|
|
79
|
+
const next = [];
|
|
80
|
+
for (const f of frontier) {
|
|
81
|
+
for (const d of graph.deps.get(f) ?? []) {
|
|
82
|
+
if (!seen.has(d)) {
|
|
83
|
+
seen.add(d);
|
|
84
|
+
next.push(d);
|
|
85
|
+
if (seen.size >= max)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (opts.includeDependents && h === 0) {
|
|
90
|
+
for (const d of graph.dependents.get(f) ?? []) {
|
|
91
|
+
if (!seen.has(d)) {
|
|
92
|
+
seen.add(d);
|
|
93
|
+
next.push(d);
|
|
94
|
+
if (seen.size >= max)
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (seen.size >= max)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
frontier = next;
|
|
103
|
+
}
|
|
104
|
+
return [...anchors, ...Array.from(seen).filter((f) => !anchors.includes(f))].slice(0, max);
|
|
105
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ProjectFile {
|
|
2
|
+
/** Workspace-relative path with forward slashes (the engine's canonical form). */
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const MAX_FILES = 1500;
|
|
7
|
+
export declare const MAX_FILE_BYTES: number;
|
|
8
|
+
/** Enumerate indexable files under `root` (breadth-first, bounded). */
|
|
9
|
+
export declare function listProjectFiles(root: string, opts?: {
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
}): Promise<string[]>;
|
|
12
|
+
/** Read one project file (size/binary guarded). Returns null when skipped. */
|
|
13
|
+
export declare function readProjectFile(root: string, rel: string): Promise<ProjectFile | null>;
|
|
14
|
+
/** Load the whole indexable project (bounded). */
|
|
15
|
+
export declare function loadProject(root: string, opts?: {
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
}): Promise<ProjectFile[]>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace file enumeration for the OIOXO context engine. Walks the project
|
|
3
|
+
* tree with the same include/exclude policy as the OIOXO IDEs (source-ish files
|
|
4
|
+
* only, skip build output / deps / media), bounded so a huge monorepo can't
|
|
5
|
+
* exhaust memory. 100% local — file contents never leave the machine.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
export const MAX_FILES = 1500;
|
|
10
|
+
export const MAX_FILE_BYTES = 256 * 1024;
|
|
11
|
+
const INCLUDE_EXT = new Set([
|
|
12
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
|
|
13
|
+
'.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.h', '.cpp', '.cs',
|
|
14
|
+
'.swift', '.kt', '.vue', '.svelte', '.css', '.scss', '.html', '.json',
|
|
15
|
+
'.md', '.yml', '.yaml', '.sql', '.sh',
|
|
16
|
+
]);
|
|
17
|
+
const EXCLUDE_DIRS = new Set([
|
|
18
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', 'coverage',
|
|
19
|
+
'vendor', '.oioxo', '.vscode', '.cursor', '.idea', '__pycache__', '.venv', 'venv',
|
|
20
|
+
]);
|
|
21
|
+
/** A NUL byte in the first 1KB reliably marks a binary file. */
|
|
22
|
+
function isBinary(text) {
|
|
23
|
+
const n = Math.min(text.length, 1024);
|
|
24
|
+
for (let i = 0; i < n; i++)
|
|
25
|
+
if (text.charCodeAt(i) === 0)
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const looksMinified = (name) => /\.min\.[a-z]+$/.test(name);
|
|
30
|
+
/** Enumerate indexable files under `root` (breadth-first, bounded). */
|
|
31
|
+
export async function listProjectFiles(root, opts = {}) {
|
|
32
|
+
const maxFiles = opts.maxFiles ?? MAX_FILES;
|
|
33
|
+
const found = [];
|
|
34
|
+
const queue = [''];
|
|
35
|
+
while (queue.length && found.length < maxFiles) {
|
|
36
|
+
const rel = queue.shift();
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await fs.readdir(path.join(root, rel), { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Stable order so retrieval is deterministic across runs.
|
|
45
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
+
for (const e of entries) {
|
|
47
|
+
if (found.length >= maxFiles)
|
|
48
|
+
break;
|
|
49
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
50
|
+
if (e.isDirectory()) {
|
|
51
|
+
if (!EXCLUDE_DIRS.has(e.name) && !e.name.startsWith('.'))
|
|
52
|
+
queue.push(childRel);
|
|
53
|
+
}
|
|
54
|
+
else if (e.isFile()) {
|
|
55
|
+
if (INCLUDE_EXT.has(path.extname(e.name).toLowerCase()) && !looksMinified(e.name))
|
|
56
|
+
found.push(childRel);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
/** Read one project file (size/binary guarded). Returns null when skipped. */
|
|
63
|
+
export async function readProjectFile(root, rel) {
|
|
64
|
+
const abs = path.join(root, rel);
|
|
65
|
+
try {
|
|
66
|
+
const stat = await fs.stat(abs);
|
|
67
|
+
if (stat.size > MAX_FILE_BYTES)
|
|
68
|
+
return null;
|
|
69
|
+
const content = await fs.readFile(abs, 'utf8');
|
|
70
|
+
if (!content || isBinary(content))
|
|
71
|
+
return null;
|
|
72
|
+
return { path: rel.replace(/\\/g, '/'), content };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Load the whole indexable project (bounded). */
|
|
79
|
+
export async function loadProject(root, opts = {}) {
|
|
80
|
+
const rels = await listProjectFiles(root, opts);
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const rel of rels) {
|
|
83
|
+
const f = await readProjectFile(root, rel);
|
|
84
|
+
if (f)
|
|
85
|
+
out.push(f);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const OIOXO_DIR = ".oioxo";
|
|
2
|
+
export declare const RULES_FILE = "rules.md";
|
|
3
|
+
export declare const MEMORY_FILE = "memory.md";
|
|
4
|
+
export interface ProjectContext {
|
|
5
|
+
rules: string;
|
|
6
|
+
memory: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function readProjectContext(root: string): Promise<ProjectContext>;
|
|
9
|
+
/** Append one learned fact as a bullet — deduped, bounded FIFO. */
|
|
10
|
+
export declare function rememberFact(root: string, fact: string): Promise<void>;
|
|
11
|
+
/** Format rules+memory as an injectable block (mirrors the IDE injection). */
|
|
12
|
+
export declare function contextBlock(ctx: ProjectContext): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIOXO project memory — the same `.oioxo/` contract the OIOXO IDEs use
|
|
3
|
+
* (rules.md = durable conventions the user writes; memory.md = bounded facts
|
|
4
|
+
* the agent learns). One format across desktop, web and MCP, so a project
|
|
5
|
+
* carries its knowledge between every surface. 100% on-device.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
export const OIOXO_DIR = '.oioxo';
|
|
10
|
+
export const RULES_FILE = 'rules.md';
|
|
11
|
+
export const MEMORY_FILE = 'memory.md';
|
|
12
|
+
// Bounds match the desktop service so no surface ever bloats the file.
|
|
13
|
+
const MAX_MEMORY_BULLETS = 40;
|
|
14
|
+
const MAX_MEMORY_CHARS = 4000;
|
|
15
|
+
async function readIfExists(p) {
|
|
16
|
+
try {
|
|
17
|
+
return (await fs.readFile(p, 'utf8')).trim();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function readProjectContext(root) {
|
|
24
|
+
const dir = path.join(root, OIOXO_DIR);
|
|
25
|
+
return {
|
|
26
|
+
rules: await readIfExists(path.join(dir, RULES_FILE)),
|
|
27
|
+
memory: await readIfExists(path.join(dir, MEMORY_FILE)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Append one learned fact as a bullet — deduped, bounded FIFO. */
|
|
31
|
+
export async function rememberFact(root, fact) {
|
|
32
|
+
const clean = fact.replace(/\s+/g, ' ').trim();
|
|
33
|
+
if (!clean)
|
|
34
|
+
return;
|
|
35
|
+
const dir = path.join(root, OIOXO_DIR);
|
|
36
|
+
await fs.mkdir(dir, { recursive: true });
|
|
37
|
+
const file = path.join(dir, MEMORY_FILE);
|
|
38
|
+
const existing = await readIfExists(file);
|
|
39
|
+
let bullets = existing.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- '));
|
|
40
|
+
const lower = clean.toLowerCase();
|
|
41
|
+
bullets = bullets.filter((b) => b.slice(2).toLowerCase() !== lower);
|
|
42
|
+
bullets.push(`- ${clean}`);
|
|
43
|
+
while (bullets.length > MAX_MEMORY_BULLETS || bullets.join('\n').length > MAX_MEMORY_CHARS)
|
|
44
|
+
bullets.shift();
|
|
45
|
+
await fs.writeFile(file, bullets.join('\n') + '\n', 'utf8');
|
|
46
|
+
}
|
|
47
|
+
/** Format rules+memory as an injectable block (mirrors the IDE injection). */
|
|
48
|
+
export function contextBlock(ctx) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (ctx.rules)
|
|
51
|
+
parts.push(`PROJECT RULES (from .oioxo/rules.md — always follow these):\n${ctx.rules}`);
|
|
52
|
+
if (ctx.memory)
|
|
53
|
+
parts.push(`PROJECT MEMORY (facts OIOXO learned about this project, on-device):\n${ctx.memory}`);
|
|
54
|
+
return parts.join('\n\n');
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIOXO skeletonizer — the API surface of a file without implementation bodies.
|
|
3
|
+
* Returning signatures instead of bodies is where most of the token saving
|
|
4
|
+
* comes from: the agent learns WHAT exists and how to call it for ~5-10% of the
|
|
5
|
+
* tokens of the full file, and only pulls full bodies for the files it edits.
|
|
6
|
+
*
|
|
7
|
+
* Regex-based (no parser dependency) across the common languages, tuned for
|
|
8
|
+
* top-level declarations. Good-enough by design — when a signature is missed
|
|
9
|
+
* the agent simply falls back to reading the file.
|
|
10
|
+
*/
|
|
11
|
+
export interface SymbolSig {
|
|
12
|
+
file: string;
|
|
13
|
+
name: string;
|
|
14
|
+
kind: string;
|
|
15
|
+
signature: string;
|
|
16
|
+
line: number;
|
|
17
|
+
}
|
|
18
|
+
/** Extract top-level-ish declarations from one file. */
|
|
19
|
+
export declare function extractSignatures(path: string, content: string, maxPerFile?: number): SymbolSig[];
|
|
20
|
+
/** A compact skeleton block for one file: path header + signatures only. */
|
|
21
|
+
export declare function fileSkeleton(path: string, content: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIOXO skeletonizer — the API surface of a file without implementation bodies.
|
|
3
|
+
* Returning signatures instead of bodies is where most of the token saving
|
|
4
|
+
* comes from: the agent learns WHAT exists and how to call it for ~5-10% of the
|
|
5
|
+
* tokens of the full file, and only pulls full bodies for the files it edits.
|
|
6
|
+
*
|
|
7
|
+
* Regex-based (no parser dependency) across the common languages, tuned for
|
|
8
|
+
* top-level declarations. Good-enough by design — when a signature is missed
|
|
9
|
+
* the agent simply falls back to reading the file.
|
|
10
|
+
*/
|
|
11
|
+
const TSJS = {
|
|
12
|
+
test: /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/,
|
|
13
|
+
patterns: [
|
|
14
|
+
{ re: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(?<name>[A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\([^)]*\)?[^{;]*/, kind: 'function' },
|
|
15
|
+
{ re: /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(?<name>[A-Za-z_$][\w$]*)[^{]*/, kind: 'class' },
|
|
16
|
+
{ re: /^\s*(?:export\s+)?interface\s+(?<name>[A-Za-z_$][\w$]*)[^{]*/, kind: 'interface' },
|
|
17
|
+
{ re: /^\s*(?:export\s+)?type\s+(?<name>[A-Za-z_$][\w$]*)\s*=?.*/, kind: 'type' },
|
|
18
|
+
{ re: /^\s*(?:export\s+)?enum\s+(?<name>[A-Za-z_$][\w$]*).*/, kind: 'enum' },
|
|
19
|
+
{ re: /^\s*export\s+(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\s*(?::[^=]+)?=?.*/, kind: 'const' },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
const PYTHON = {
|
|
23
|
+
test: /\.py$/,
|
|
24
|
+
patterns: [
|
|
25
|
+
{ re: /^\s*(?:async\s+)?def\s+(?<name>\w+)\s*\([^)]*\)?[^:]*/, kind: 'function' },
|
|
26
|
+
{ re: /^\s*class\s+(?<name>\w+).*/, kind: 'class' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
const GO = {
|
|
30
|
+
test: /\.go$/,
|
|
31
|
+
patterns: [
|
|
32
|
+
{ re: /^\s*func\s+(?:\([^)]+\)\s*)?(?<name>\w+)\s*\([^)]*\)?[^{]*/, kind: 'func' },
|
|
33
|
+
{ re: /^\s*type\s+(?<name>\w+)\s+(?:struct|interface).*/, kind: 'type' },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const RUST = {
|
|
37
|
+
test: /\.rs$/,
|
|
38
|
+
patterns: [
|
|
39
|
+
{ re: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(?<name>\w+)\s*(?:<[^>]*>)?\([^)]*\)?[^{]*/, kind: 'fn' },
|
|
40
|
+
{ re: /^\s*(?:pub\s+)?(?:struct|enum|trait)\s+(?<name>\w+).*/, kind: 'type' },
|
|
41
|
+
{ re: /^\s*impl(?:<[^>]*>)?\s+(?<name>[\w:]+).*/, kind: 'impl' },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
const CLIKE = {
|
|
45
|
+
test: /\.(java|cs|kt|swift|c|h|cpp|php|rb)$/,
|
|
46
|
+
patterns: [
|
|
47
|
+
{ re: /^\s*(?:public|private|protected|internal|static|final|abstract|override|\s)*\s*(?:class|interface|struct|enum|trait|module)\s+(?<name>\w+).*/, kind: 'class' },
|
|
48
|
+
{ re: /^\s*(?:public|private|protected|internal|static|final|virtual|override|async|func|def|\s)+[\w<>\[\],\s*&]*?\b(?<name>\w+)\s*\([^)]*\)?\s*[^{;]*/, kind: 'method' },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
const RULES = [TSJS, PYTHON, GO, RUST, CLIKE];
|
|
52
|
+
const clean = (s) => s.replace(/\s+/g, ' ').trim().slice(0, 240);
|
|
53
|
+
/** Extract top-level-ish declarations from one file. */
|
|
54
|
+
export function extractSignatures(path, content, maxPerFile = 80) {
|
|
55
|
+
const rule = RULES.find((r) => r.test.test(path));
|
|
56
|
+
if (!rule)
|
|
57
|
+
return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
for (let i = 0; i < lines.length && out.length < maxPerFile; i++) {
|
|
61
|
+
const line = lines[i];
|
|
62
|
+
if (!line.trim() || line.trim().startsWith('//') || line.trim().startsWith('#') || line.trim().startsWith('*'))
|
|
63
|
+
continue;
|
|
64
|
+
for (const { re, kind } of rule.patterns) {
|
|
65
|
+
const m = re.exec(line);
|
|
66
|
+
const name = m?.groups?.name;
|
|
67
|
+
if (m && name) {
|
|
68
|
+
// For multi-line signatures, pull continuation lines until ')' balance or 3 lines.
|
|
69
|
+
let sig = line;
|
|
70
|
+
if (sig.includes('(') && !sig.includes(')')) {
|
|
71
|
+
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
|
72
|
+
sig += ' ' + lines[j];
|
|
73
|
+
if (lines[j].includes(')'))
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Cut the body: everything before the first '{' (or trailing ':').
|
|
78
|
+
const brace = sig.indexOf('{');
|
|
79
|
+
if (brace > 0)
|
|
80
|
+
sig = sig.slice(0, brace);
|
|
81
|
+
out.push({ file: path, name, kind, signature: clean(sig), line: i + 1 });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// De-dup (overloads / pattern overlap) by name+line.
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
return out.filter((s) => {
|
|
89
|
+
const key = `${s.name}:${s.line}`;
|
|
90
|
+
if (seen.has(key))
|
|
91
|
+
return false;
|
|
92
|
+
seen.add(key);
|
|
93
|
+
return true;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** A compact skeleton block for one file: path header + signatures only. */
|
|
97
|
+
export function fileSkeleton(path, content) {
|
|
98
|
+
const sigs = extractSignatures(path, content);
|
|
99
|
+
if (!sigs.length) {
|
|
100
|
+
// Non-code or nothing matched: head excerpt is better than nothing for
|
|
101
|
+
// configs/docs, still far cheaper than the full file.
|
|
102
|
+
const head = content.split('\n').slice(0, 12).join('\n');
|
|
103
|
+
return `// ${path} (excerpt)\n${head}`;
|
|
104
|
+
}
|
|
105
|
+
return `// ${path} — API surface (bodies omitted)\n${sigs.map((s) => `${s.kind} ${s.signature} [L${s.line}]`).join('\n')}`;
|
|
106
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const CONTROL_PLANE: string;
|
|
2
|
+
export interface StoredCredentials {
|
|
3
|
+
token: string;
|
|
4
|
+
/** Epoch ms the bearer expires. */
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
device: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function readCredentials(): Promise<StoredCredentials | null>;
|
|
10
|
+
export declare function writeCredentials(creds: StoredCredentials): Promise<void>;
|
|
11
|
+
export declare function clearCredentials(): Promise<void>;
|
|
12
|
+
/** Stable per-machine device id (persisted beside the credentials). */
|
|
13
|
+
export declare function deviceId(): Promise<string>;
|
|
14
|
+
/** Mirror of the backend's SaveState (lib/usage/save.ts). */
|
|
15
|
+
export interface SaveState {
|
|
16
|
+
optimize: boolean;
|
|
17
|
+
unlimited: boolean;
|
|
18
|
+
tier: 'anon' | 'free' | 'pro';
|
|
19
|
+
savedThisMonth: number;
|
|
20
|
+
capTokens: number | null;
|
|
21
|
+
remainingTokens: number | null;
|
|
22
|
+
resetsAt: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ask the server whether the engine may run; optionally report saved tokens
|
|
26
|
+
* first. Returns null on network failure (caller decides the message).
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveMeter(action: 'check' | 'report', savedTokens?: number): Promise<SaveState | null>;
|
|
29
|
+
/** True when a non-expired bearer is stored (NOT a plan check — server does that). */
|
|
30
|
+
export declare function isLoggedIn(): Promise<boolean>;
|
|
31
|
+
export declare const UPGRADE_URL = "https://oioxo.com/?upgrade=pro";
|
|
32
|
+
export declare const LOGIN_HINT = "Run `oioxo-mcp login` in a terminal to connect your OIOXO account.";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIOXO account + plan gate for the MCP context engine.
|
|
3
|
+
*
|
|
4
|
+
* The #1 platform rule applies here too: PERMISSION LIVES ON THE SERVER. This
|
|
5
|
+
* module only (a) stores the 30-day device bearer that `oioxo-mcp login` minted
|
|
6
|
+
* via oioxo.com, and (b) asks the existing server-authoritative save-tokens
|
|
7
|
+
* meter (/api/usage/save) whether the engine may run and reports what it saved.
|
|
8
|
+
* A patched client can lie to itself but the server's running total decides:
|
|
9
|
+
* anon → engine off (sign in) [registered-user feature]
|
|
10
|
+
* free → monthly saved-tokens allowance [taste → upgrade]
|
|
11
|
+
* pro → unlimited
|
|
12
|
+
*
|
|
13
|
+
* Network failures FAIL TOWARD THE GATE for 'check' (no free-riding offline)
|
|
14
|
+
* but never crash the agent: tools degrade to a clear OIOXO message.
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
export const CONTROL_PLANE = process.env.OIOXO_CONTROL_PLANE?.replace(/\/$/, '') || 'https://oioxo.com';
|
|
21
|
+
// OIOXO_HOME override keeps tests hermetic and lets ops relocate credentials.
|
|
22
|
+
const HOME_DIR = process.env.OIOXO_HOME || path.join(os.homedir(), '.oioxo');
|
|
23
|
+
const CREDS_FILE = path.join(HOME_DIR, 'credentials.json');
|
|
24
|
+
export async function readCredentials() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(await fs.readFile(CREDS_FILE, 'utf8'));
|
|
27
|
+
return raw?.token ? raw : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function writeCredentials(creds) {
|
|
34
|
+
await fs.mkdir(HOME_DIR, { recursive: true });
|
|
35
|
+
await fs.writeFile(CREDS_FILE, JSON.stringify(creds, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
36
|
+
}
|
|
37
|
+
export async function clearCredentials() {
|
|
38
|
+
try {
|
|
39
|
+
await fs.unlink(CREDS_FILE);
|
|
40
|
+
}
|
|
41
|
+
catch { /* already gone */ }
|
|
42
|
+
}
|
|
43
|
+
/** Stable per-machine device id (persisted beside the credentials). */
|
|
44
|
+
export async function deviceId() {
|
|
45
|
+
const existing = await readCredentials();
|
|
46
|
+
if (existing?.device)
|
|
47
|
+
return existing.device;
|
|
48
|
+
try {
|
|
49
|
+
const raw = await fs.readFile(path.join(HOME_DIR, 'device'), 'utf8');
|
|
50
|
+
if (raw.trim())
|
|
51
|
+
return raw.trim();
|
|
52
|
+
}
|
|
53
|
+
catch { /* mint below */ }
|
|
54
|
+
const id = randomBytes(16).toString('base64url');
|
|
55
|
+
await fs.mkdir(HOME_DIR, { recursive: true });
|
|
56
|
+
await fs.writeFile(path.join(HOME_DIR, 'device'), id, 'utf8');
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
async function authHeaders() {
|
|
60
|
+
const creds = await readCredentials();
|
|
61
|
+
const device = await deviceId();
|
|
62
|
+
const h = {
|
|
63
|
+
'content-type': 'application/json',
|
|
64
|
+
// First-party non-browser client — same self-declaration the desktop uses.
|
|
65
|
+
'x-oioxo-client': 'desktop',
|
|
66
|
+
'x-oioxo-device': device,
|
|
67
|
+
};
|
|
68
|
+
if (creds?.token && creds.expiresAt > Date.now())
|
|
69
|
+
h.authorization = `Bearer ${creds.token}`;
|
|
70
|
+
return h;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ask the server whether the engine may run; optionally report saved tokens
|
|
74
|
+
* first. Returns null on network failure (caller decides the message).
|
|
75
|
+
*/
|
|
76
|
+
export async function saveMeter(action, savedTokens) {
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(`${CONTROL_PLANE}/api/usage/save`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: await authHeaders(),
|
|
81
|
+
body: JSON.stringify(action === 'report' ? { action, savedTokens } : { action }),
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
return null;
|
|
85
|
+
return (await res.json());
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** True when a non-expired bearer is stored (NOT a plan check — server does that). */
|
|
92
|
+
export async function isLoggedIn() {
|
|
93
|
+
const creds = await readCredentials();
|
|
94
|
+
return !!creds && creds.expiresAt > Date.now();
|
|
95
|
+
}
|
|
96
|
+
export const UPGRADE_URL = 'https://oioxo.com/?upgrade=pro';
|
|
97
|
+
export const LOGIN_HINT = 'Run `oioxo-mcp login` in a terminal to connect your OIOXO account.';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oioxo-mcp — OIOXO context engine for AI coding agents.
|
|
3
|
+
* Public programmatic API (the CLI/MCP server are the primary surfaces).
|
|
4
|
+
*/
|
|
5
|
+
export { loadProject, listProjectFiles, readProjectFile, type ProjectFile } from './core/files.js';
|
|
6
|
+
export { Bm25Index, tokenize, type RetrievedChunk, type IndexStatus } from './core/bm25.js';
|
|
7
|
+
export { buildGraph, expandContext, importSpecifiers, resolveImport, type CodeGraph } from './core/code-graph.js';
|
|
8
|
+
export { extractSignatures, fileSkeleton, type SymbolSig } from './core/skeleton.js';
|
|
9
|
+
export { buildCapsule, type Capsule, type CapsuleOptions } from './core/capsule.js';
|
|
10
|
+
export { readProjectContext, rememberFact, contextBlock, OIOXO_DIR } from './core/memory.js';
|
|
11
|
+
export { startServer } from './mcp/server.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oioxo-mcp — OIOXO context engine for AI coding agents.
|
|
3
|
+
* Public programmatic API (the CLI/MCP server are the primary surfaces).
|
|
4
|
+
*/
|
|
5
|
+
export { loadProject, listProjectFiles, readProjectFile } from './core/files.js';
|
|
6
|
+
export { Bm25Index, tokenize } from './core/bm25.js';
|
|
7
|
+
export { buildGraph, expandContext, importSpecifiers, resolveImport } from './core/code-graph.js';
|
|
8
|
+
export { extractSignatures, fileSkeleton } from './core/skeleton.js';
|
|
9
|
+
export { buildCapsule } from './core/capsule.js';
|
|
10
|
+
export { readProjectContext, rememberFact, contextBlock, OIOXO_DIR } from './core/memory.js';
|
|
11
|
+
export { startServer } from './mcp/server.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startServer(root: string): Promise<void>;
|