@ctxo/cli 0.13.1 → 0.15.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 +26 -1
- package/dist/chunk-2Q76LH2H.js +186 -0
- package/dist/chunk-72CEQEQD.js +53 -0
- package/dist/{chunk-WB7WZKX4.js → chunk-A4URKKPP.js} +916 -1938
- package/dist/chunk-BBSJB4CG.js +128 -0
- package/dist/{chunk-DT3ZH4AU.js → chunk-EC6INGPV.js} +6 -1
- package/dist/chunk-I7RPXT6L.js +1241 -0
- package/dist/chunk-IT426P3F.js +1036 -0
- package/dist/chunk-JDL6GGTO.js +87 -0
- package/dist/chunk-LAX6VVBU.js +98 -0
- package/dist/{cli-router-DDHH4WQH.js → cli-router-W56INOQ3.js} +980 -504
- package/dist/index.js +13 -2874
- package/dist/internal.d.ts +834 -0
- package/dist/internal.js +13 -0
- package/dist/{json-index-reader-OXTRTGE6.js → json-index-reader-DKEIP2QR.js} +6 -4
- package/dist/mcp-server-IYCCE74E.js +2336 -0
- package/dist/module-host-KTGDODES.js +13 -0
- package/dist/module-loader-B3LYF3W7.js +14 -0
- package/dist/plugin-discovery-XRXI3IJF.js +13 -0
- package/llms-full.txt +121 -2
- package/llms.txt +24 -1
- package/package.json +15 -3
- package/dist/atomic-write-BAFH464Q.js.map +0 -1
- package/dist/chunk-5DSKZJNN.js.map +0 -1
- package/dist/chunk-DT3ZH4AU.js.map +0 -1
- package/dist/chunk-PSKDDLSF.js.map +0 -1
- package/dist/chunk-RFEXMBTG.js.map +0 -1
- package/dist/chunk-WB7WZKX4.js.map +0 -1
- package/dist/cli-router-DDHH4WQH.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/json-index-reader-OXTRTGE6.js.map +0 -1
- package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ It's not a skill gap. It's a **sensory gap**. The agent is navigating your codeb
|
|
|
36
36
|
|
|
37
37
|
The core shift: your agent stops reacting to files it stumbles into and starts planning from a complete map. **Blast radius before the edit. Git intent before the bug fix. Importer list before the rename.**
|
|
38
38
|
|
|
39
|
-
Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools
|
|
39
|
+
Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools, plus 2 domain tools with `@ctxo/domain`.
|
|
40
40
|
|
|
41
41
|
One `get_blast_radius` call replaces an entire ripgrep/read spiral. One `get_pr_impact` replaces a full review session of "wait, what calls this?"
|
|
42
42
|
|
|
@@ -141,6 +141,30 @@ ctxo contexts --json # the full contexts-view.json
|
|
|
141
141
|
|
|
142
142
|
Agents get the same view from `get_architectural_overlay(mode: "contexts")`, and one candidate in full with `candidate: <id>`.
|
|
143
143
|
|
|
144
|
+
## Cards that go stale
|
|
145
|
+
|
|
146
|
+
An agent changes the lending rules in code, and the card that describes them stays as it was. Name your bounded contexts in a registry, and Ctxo checks the structure, never the meaning: a context's code changed in the session without a card update or a `Domain-Unchanged` commit trailer, a rule ID that no card defines, a card link that no longer resolves, an edge that crosses a context boundary outside its contracts. The opt-in `@ctxo/domain` package adds the commands and a Claude Code guard; Ctxo never writes the cards.
|
|
147
|
+
|
|
148
|
+
```shell
|
|
149
|
+
ctxo install domain # the opt-in @ctxo/domain package
|
|
150
|
+
ctxo init --domain # registry skeleton, fold guard, skill, domain config
|
|
151
|
+
ctxo contexts-touched --since origin/main # which named contexts a branch changed, and which are resolved
|
|
152
|
+
ctxo verify-domain --since origin/main # CI gate: registry, cards, rule IDs, loader rules, boundaries
|
|
153
|
+
ctxo domain rules --write # path-scoped rules that point the agent at the card
|
|
154
|
+
ctxo domain discover # propose tracker prefixes and test conventions from the repo
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
With cards in place, `ctxo index` links each rule to the tests that pin it and the code that implements it, and the agent sees the rules while it works: `get_blast_radius` lists the rules a change may break and the cards to update, `get_why_context` the decisions behind them, `get_context_for_task` a card excerpt, `get_pr_impact` the contexts touched and the rules without tests. Two tools answer domain questions directly:
|
|
158
|
+
|
|
159
|
+
| Tool | Answers |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `get_domain_context` | The card of the context that owns a symbol or file: terms, rules with their tests and code, contracts, decisions |
|
|
162
|
+
| `get_rule` | One rule by ID: the tests that pin it (and how each was found), the symbols that implement it, the decisions behind it |
|
|
163
|
+
|
|
164
|
+
`get_ranked_context` with `scope: "domain"` searches the cards and the documents they link to.
|
|
165
|
+
|
|
166
|
+
See [Ctxo domain conventions v1](docs/features/domain-knowledge.md#ctxo-domain-conventions-v1) for the registry, the card template and the adoption ladder.
|
|
167
|
+
|
|
144
168
|
## Supported Languages
|
|
145
169
|
|
|
146
170
|
| Language | Plugin | Tier | Notes |
|
|
@@ -156,6 +180,7 @@ Install via the CLI shortcut:
|
|
|
156
180
|
```bash
|
|
157
181
|
ctxo install typescript go csharp # syntax or full tier auto-detected
|
|
158
182
|
ctxo install sql # T-SQL plugin (alias tsql)
|
|
183
|
+
ctxo install domain # opt-in domain knowledge layer (@ctxo/domain)
|
|
159
184
|
ctxo install java # installs syntax tier; adds full-tier analyzer when JRE 11+ detected
|
|
160
185
|
ctxo install java --full-tier # force full-tier analyzer install
|
|
161
186
|
ctxo install java --syntax-only # skip analyzer, syntax tier only
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createLogger
|
|
4
|
+
} from "./chunk-5DSKZJNN.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/language/plugin-discovery.ts
|
|
7
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { SUPPORTED_API_VERSIONS } from "@ctxo/plugin-api";
|
|
12
|
+
var log = createLogger("ctxo:plugin-discovery");
|
|
13
|
+
var OFFICIAL_PREFIX = "@ctxo/lang-";
|
|
14
|
+
var COMMUNITY_PREFIX = "ctxo-lang-";
|
|
15
|
+
function readManifestDeps(manifestPath) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
18
|
+
const manifest = JSON.parse(raw);
|
|
19
|
+
const all = {
|
|
20
|
+
...manifest.dependencies,
|
|
21
|
+
...manifest.devDependencies,
|
|
22
|
+
...manifest.peerDependencies
|
|
23
|
+
};
|
|
24
|
+
return Object.keys(all).filter(
|
|
25
|
+
(name) => name.startsWith(OFFICIAL_PREFIX) || name.startsWith(COMMUNITY_PREFIX)
|
|
26
|
+
);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
log.warn(`Failed to read manifest ${manifestPath}: ${err.message}`);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isLocalSpecifier(spec) {
|
|
33
|
+
return spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec);
|
|
34
|
+
}
|
|
35
|
+
function pickEsmEntry(exportsField) {
|
|
36
|
+
if (!exportsField) return void 0;
|
|
37
|
+
if (typeof exportsField === "string") return exportsField;
|
|
38
|
+
if (typeof exportsField !== "object") return void 0;
|
|
39
|
+
const table = exportsField;
|
|
40
|
+
const dot = "." in table ? table["."] : exportsField;
|
|
41
|
+
if (typeof dot === "string") return dot;
|
|
42
|
+
if (!dot || typeof dot !== "object") return void 0;
|
|
43
|
+
for (const key of ["import", "node", "default"]) {
|
|
44
|
+
const nested = dot[key];
|
|
45
|
+
if (typeof nested === "string") return nested;
|
|
46
|
+
if (nested && typeof nested === "object") {
|
|
47
|
+
for (const inner of ["default", "import", "node"]) {
|
|
48
|
+
const v = nested[inner];
|
|
49
|
+
if (typeof v === "string") return v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
var cliFallbackRequire = createRequire(import.meta.url);
|
|
56
|
+
function resolvePluginPackageJson(specifier, projectRequire) {
|
|
57
|
+
try {
|
|
58
|
+
return projectRequire.resolve(`${specifier}/package.json`);
|
|
59
|
+
} catch (primary) {
|
|
60
|
+
try {
|
|
61
|
+
return cliFallbackRequire.resolve(`${specifier}/package.json`);
|
|
62
|
+
} catch {
|
|
63
|
+
throw primary;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readPackageJsonVia(specifier, req) {
|
|
68
|
+
const files = [];
|
|
69
|
+
try {
|
|
70
|
+
files.push(req.resolve(`${specifier}/package.json`));
|
|
71
|
+
} catch {
|
|
72
|
+
for (const base of req.resolve.paths(specifier) ?? []) {
|
|
73
|
+
files.push(join(base, specifier, "package.json"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
try {
|
|
78
|
+
return { pkgDir: dirname(file), raw: readFileSync(file, "utf-8") };
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
function resolvePackageCandidates(specifier, projectRoot, cliAnchor) {
|
|
85
|
+
const lookups = [
|
|
86
|
+
{ source: "project", req: createRequire(join(projectRoot, "package.json")) },
|
|
87
|
+
{
|
|
88
|
+
source: "cli",
|
|
89
|
+
req: cliAnchor === void 0 ? cliFallbackRequire : createRequire(join(cliAnchor, "package.json"))
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
const found = [];
|
|
93
|
+
const seen = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const { source, req } of lookups) {
|
|
95
|
+
const read = readPackageJsonVia(specifier, req);
|
|
96
|
+
if (!read) continue;
|
|
97
|
+
try {
|
|
98
|
+
const real = realpathSync(read.pkgDir);
|
|
99
|
+
if (seen.has(real)) continue;
|
|
100
|
+
seen.add(real);
|
|
101
|
+
const pkg = JSON.parse(read.raw);
|
|
102
|
+
const entry = pickEsmEntry(pkg.exports) ?? pkg.module ?? pkg.main ?? "index.js";
|
|
103
|
+
found.push({
|
|
104
|
+
name: pkg.name ?? specifier,
|
|
105
|
+
version: pkg.version ?? "0.0.0",
|
|
106
|
+
pkgDir: real,
|
|
107
|
+
entryUrl: pathToFileURL(resolve(real, entry)).href,
|
|
108
|
+
source
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log.warn(`Skipped ${specifier} candidate in ${read.pkgDir}: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
function resolvePluginEntryUrl(specifier, manifestRequire) {
|
|
117
|
+
const pkgJsonPath = resolvePluginPackageJson(specifier, manifestRequire);
|
|
118
|
+
const pkgDir = dirname(pkgJsonPath);
|
|
119
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
120
|
+
const entry = pickEsmEntry(pkgJson.exports) ?? pkgJson.module ?? pkgJson.main ?? "index.js";
|
|
121
|
+
return pathToFileURL(resolve(pkgDir, entry)).href;
|
|
122
|
+
}
|
|
123
|
+
async function tryLoad(specifier, baseDir, manifestRequire) {
|
|
124
|
+
try {
|
|
125
|
+
const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : resolvePluginEntryUrl(specifier, manifestRequire);
|
|
126
|
+
const mod = await import(target);
|
|
127
|
+
const candidate = mod.default ?? mod.plugin ?? mod;
|
|
128
|
+
if (!candidate || typeof candidate !== "object") {
|
|
129
|
+
return { specifier, reason: "module export is not an object" };
|
|
130
|
+
}
|
|
131
|
+
if (!SUPPORTED_API_VERSIONS.includes(candidate.apiVersion)) {
|
|
132
|
+
return {
|
|
133
|
+
specifier,
|
|
134
|
+
reason: `unsupported apiVersion ${String(candidate.apiVersion)} (supported: ${SUPPORTED_API_VERSIONS.join(", ")})`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (typeof candidate.createAdapter !== "function") {
|
|
138
|
+
return { specifier, reason: "missing createAdapter()" };
|
|
139
|
+
}
|
|
140
|
+
if (typeof candidate.id !== "string" || candidate.id.length === 0) {
|
|
141
|
+
return { specifier, reason: "missing or empty id" };
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(candidate.extensions)) {
|
|
144
|
+
return { specifier, reason: "extensions must be an array" };
|
|
145
|
+
}
|
|
146
|
+
if (candidate.tier !== "syntax" && candidate.tier !== "full") {
|
|
147
|
+
return { specifier, reason: `invalid tier ${String(candidate.tier)}` };
|
|
148
|
+
}
|
|
149
|
+
return candidate;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { specifier, reason: err.message };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function discoverPlugins(options) {
|
|
155
|
+
const { manifestPath } = options;
|
|
156
|
+
if (!existsSync(manifestPath)) {
|
|
157
|
+
log.info(`Manifest not found at ${manifestPath} \u2014 no plugins discovered`);
|
|
158
|
+
return { plugins: [], failures: [] };
|
|
159
|
+
}
|
|
160
|
+
const baseDir = dirname(manifestPath);
|
|
161
|
+
const specifiers = options.explicit ?? readManifestDeps(manifestPath);
|
|
162
|
+
const manifestRequire = createRequire(pathToFileURL(manifestPath).href);
|
|
163
|
+
const plugins = [];
|
|
164
|
+
const failures = [];
|
|
165
|
+
for (const spec of specifiers) {
|
|
166
|
+
if (options.shouldSkipSpecifier?.(spec)) {
|
|
167
|
+
log.info(`Skipping plugin ${spec} (matched ignoreProjects)`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const result = await tryLoad(spec, baseDir, manifestRequire);
|
|
171
|
+
if ("apiVersion" in result) {
|
|
172
|
+
plugins.push({ plugin: result, specifier: spec });
|
|
173
|
+
} else {
|
|
174
|
+
failures.push(result);
|
|
175
|
+
log.warn(`Skipped plugin ${spec}: ${result.reason}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { plugins, failures };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export {
|
|
182
|
+
resolvePluginPackageJson,
|
|
183
|
+
resolvePackageCandidates,
|
|
184
|
+
discoverPlugins
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=chunk-2Q76LH2H.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/cli-paths.ts
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
function findCliPackageJson() {
|
|
7
|
+
let dir = import.meta.dirname;
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
const pkg = join(dir, "package.json");
|
|
10
|
+
if (existsSync(pkg)) return pkg;
|
|
11
|
+
const parent = join(dir, "..");
|
|
12
|
+
if (parent === dir) break;
|
|
13
|
+
dir = parent;
|
|
14
|
+
}
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
function getVersion() {
|
|
18
|
+
const pkg = findCliPackageJson();
|
|
19
|
+
if (pkg === void 0) return "unknown";
|
|
20
|
+
try {
|
|
21
|
+
const json = JSON.parse(readFileSync(pkg, "utf-8"));
|
|
22
|
+
return json.version ?? "unknown";
|
|
23
|
+
} catch {
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getCliPackageRoot() {
|
|
28
|
+
const pkg = findCliPackageJson();
|
|
29
|
+
return pkg !== void 0 ? dirname(pkg) : dirname(import.meta.dirname);
|
|
30
|
+
}
|
|
31
|
+
function locateGitRoot(startDir) {
|
|
32
|
+
let dir = startDir;
|
|
33
|
+
for (let i = 0; i < 12; i++) {
|
|
34
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
35
|
+
const parent = dirname(dir);
|
|
36
|
+
if (parent === dir) break;
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/core/compare-codepoint.ts
|
|
43
|
+
function compareCodepoint(a, b) {
|
|
44
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
compareCodepoint,
|
|
49
|
+
getVersion,
|
|
50
|
+
getCliPackageRoot,
|
|
51
|
+
locateGitRoot
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=chunk-72CEQEQD.js.map
|