@c4a/context-cli 0.5.29-alpha.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.
Files changed (38) hide show
  1. package/README.md +122 -0
  2. package/cli.js +37711 -0
  3. package/package.json +18 -0
  4. package/plugin/.claude-plugin/plugin.json +16 -0
  5. package/plugin/.claude-plugin/plugin.json.template +16 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/.codex-plugin/plugin.json.template +35 -0
  8. package/plugin/commands/align.md +54 -0
  9. package/plugin/commands/capture-aspect.md +17 -0
  10. package/plugin/commands/capture-code.md +25 -0
  11. package/plugin/commands/capture.md +67 -0
  12. package/plugin/commands/compile.md +89 -0
  13. package/plugin/commands/context.md +30 -0
  14. package/plugin/commands/drop.md +17 -0
  15. package/plugin/commands/extract.md +18 -0
  16. package/plugin/commands/init.md +95 -0
  17. package/plugin/commands/purge.md +18 -0
  18. package/plugin/commands/query.md +13 -0
  19. package/plugin/commands/status.md +21 -0
  20. package/plugin/skills/align-finalize/SKILL.md +137 -0
  21. package/plugin/skills/align-propose/SKILL.md +142 -0
  22. package/plugin/skills/align-scan/SKILL.md +161 -0
  23. package/plugin/skills/align-scan/references/data-model.md +343 -0
  24. package/plugin/skills/align-scan/references/user-question-contract.md +155 -0
  25. package/plugin/skills/compile-close/SKILL.md +122 -0
  26. package/plugin/skills/compile-draft/SKILL.md +246 -0
  27. package/plugin/skills/context-query/SKILL.md +166 -0
  28. package/plugin/skills/drop/SKILL.md +163 -0
  29. package/plugin/skills/semantic-reconcile/SKILL.md +106 -0
  30. package/scripts/build-plugin.ts +70 -0
  31. package/scripts/postinstall.mjs +183 -0
  32. package/templates/aspects/code/prompt.md +29 -0
  33. package/templates/aspects/design-system/prompt.md +27 -0
  34. package/templates/aspects/graphql/prompt.md +24 -0
  35. package/templates/aspects/openapi/prompt.md +24 -0
  36. package/wasm/tree-sitter-tsx.wasm +0 -0
  37. package/wasm/tree-sitter-typescript.wasm +0 -0
  38. package/wasm/tree-sitter.wasm +0 -0
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: semantic-reconcile
3
+ description: >
4
+ Internal skill for `/context:compile` and `/context:drop`. Consumes only
5
+ `context reconcile prepare` output, judges semantic relation/action for each
6
+ item, and emits a schema_version 1.0 semantic decision document for
7
+ `context reconcile review` / `context reconcile apply`.
8
+ tools:
9
+ - Bash
10
+ ---
11
+
12
+ # semantic-reconcile — decide Section reconciliation
13
+
14
+ Judge duplicate, merge, conflict, reanchor, and unsupported cases from the
15
+ CLI-prepared context. Emit decisions only; the CLI performs every write.
16
+
17
+ ## TL;DR — Non-negotiables
18
+
19
+ - Input is only the `context reconcile prepare` payload. Do not Read, Glob, Grep, or Write workspace `raw/`, `knowledge/`, `archive/`, or `decisions/`.
20
+ - Output exactly one YAML or JSON document with `schema_version: "1.0"` and `decisions[]`.
21
+ - Use canonical relations: `exact_duplicate`, `strong_equivalent`, `near_duplicate`, `complement`, `supersedes`, `conflicts`, `reanchor`, `unsupported`, `keep_separate`, `ask_user`.
22
+ - Use canonical actions: `duplicate_skip`, `merge_update`, `supersede`, `reanchor`, `remove_unsupported`, `keep_separate`, `split_then_reanchor`, `ask_user`.
23
+ - `no-op` is not a decision action. For unchanged refresh items, emit no decision or `duplicate_skip`.
24
+ - Never expose `src-N`, Section ids, or source refs as the user-facing choice. They may appear only in the structured payload.
25
+ - `merge_update` is legal only when the final Section content is supported by one valid `source_ref`; otherwise use `keep_separate`, `split_then_reanchor`, or `ask_user`.
26
+ - Unresolved conflicts and low-confidence support questions must be `action: ask_user`.
27
+
28
+ <reference>
29
+
30
+ ## Decision Shape
31
+
32
+ ```yaml
33
+ schema_version: "1.0"
34
+ decisions:
35
+ - item_id: claim-001
36
+ relation: near_duplicate
37
+ action: merge_update
38
+ target:
39
+ node: payment-runtime
40
+ section_id: section-3
41
+ proposed:
42
+ content: "Runtime isolation uses sandboxing to avoid state pollution."
43
+ confidence: confirmed
44
+ source_ref: "src-2#runtime L12-14"
45
+ source_ref_text: "sandboxing to avoid state pollution"
46
+ rationale: "The new evidence refines the same claim."
47
+ user_confirmation:
48
+ required: true
49
+ question_type: merge_confirmation
50
+ ```
51
+
52
+ ## Action Rules
53
+
54
+ | action | Required fields |
55
+ |---|---|
56
+ | `duplicate_skip` | `target` |
57
+ | `merge_update` | `target`, `proposed.content`, `proposed.source_ref` |
58
+ | `supersede` | `target`, `proposed.kind`, `proposed.content`, `proposed.source_ref` |
59
+ | `reanchor` | `target`, `proposed.source_ref`, `proposed.confidence` |
60
+ | `remove_unsupported` | `target` |
61
+ | `keep_separate` | `target`, `proposed.kind`, `proposed.content`, `proposed.source_ref` |
62
+ | `split_then_reanchor` | `target`, `proposed.sections[]` |
63
+ | `ask_user` | `user_confirmation.required: true` |
64
+
65
+ </reference>
66
+
67
+ <procedures>
68
+
69
+ ### Step 1 — Read Prepared Context
70
+
71
+ Use the caller-provided `context reconcile prepare` output. For each item,
72
+ compare `proposed`, `candidates`, `previous_decisions`, and `evidence`.
73
+
74
+ ### Step 2 — Reuse Stable Priors
75
+
76
+ If `previous_decisions[]` shows the same boundary and the prepared item still
77
+ matches, emit the same final action. If the target changed, treat the previous
78
+ decision as a prior only and continue judging.
79
+
80
+ ### Step 3 — Decide Relation And Action
81
+
82
+ Classify each item:
83
+
84
+ - Exact same claim already active -> `exact_duplicate` + `duplicate_skip`.
85
+ - Same claim with safer wording and one supporting source -> `strong_equivalent` or `near_duplicate` + `merge_update`.
86
+ - Additional but separate boundary -> `complement` + `keep_separate`.
87
+ - New material replaces old rule -> `supersedes` + `supersede`.
88
+ - Direct contradiction -> `conflicts` + `ask_user`.
89
+ - Drop item still fully supported elsewhere -> `reanchor` + `reanchor`.
90
+ - Drop item partly supported -> `reanchor` + `split_then_reanchor`.
91
+ - No surviving support -> `unsupported` + `remove_unsupported`.
92
+ - Evidence is close but not enough -> `ask_user`.
93
+
94
+ ### Step 4 — Emit Decisions
95
+
96
+ Emit only executable final decisions plus unresolved `ask_user` questions.
97
+ Do not include prose outside the document.
98
+
99
+ ### Step 5 — Self-verify
100
+
101
+ - [ ] Every decision uses canonical relation/action values. If not, **Step 3**.
102
+ - [ ] No `no-op` action appears. If not, **Step 3**.
103
+ - [ ] Every write action has the required fields from [Action Rules](#action-rules). If not, **Step 4**.
104
+ - [ ] No workspace files were read or written directly. If violated, restart from **Step 1**.
105
+
106
+ </procedures>
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * build-plugin.ts — materialize plugin manifests from templates by
4
+ * substituting `__VERSION__` with the value from package.json.
5
+ *
6
+ * Template sources:
7
+ * - plugin/.claude-plugin/plugin.json.template
8
+ * - plugin/.codex-plugin/plugin.json.template
9
+ *
10
+ * Outputs:
11
+ * - plugin/.claude-plugin/plugin.json
12
+ * - plugin/.codex-plugin/plugin.json
13
+ *
14
+ * Run after `bun run build` (or as part of it) so packaged tarballs ship
15
+ * version-correct plugin manifests. Idempotent — overwrites on every run.
16
+ */
17
+
18
+ import { readFile, writeFile } from "node:fs/promises";
19
+ import { dirname, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const pkgRoot = resolve(__dirname, "..");
24
+
25
+ async function main(): Promise<void> {
26
+ const pkgRaw = await readFile(resolve(pkgRoot, "package.json"), "utf-8");
27
+ const pkg = JSON.parse(pkgRaw) as { version?: string };
28
+ const version = pkg.version;
29
+ if (!version) {
30
+ throw new Error("package.json is missing `version`");
31
+ }
32
+
33
+ const manifests = [
34
+ {
35
+ label: "claude",
36
+ templatePath: resolve(pkgRoot, "plugin/.claude-plugin/plugin.json.template"),
37
+ outputPath: resolve(pkgRoot, "plugin/.claude-plugin/plugin.json"),
38
+ },
39
+ {
40
+ label: "codex",
41
+ templatePath: resolve(pkgRoot, "plugin/.codex-plugin/plugin.json.template"),
42
+ outputPath: resolve(pkgRoot, "plugin/.codex-plugin/plugin.json"),
43
+ },
44
+ ] as const;
45
+
46
+ for (const manifest of manifests) {
47
+ const template = await readFile(manifest.templatePath, "utf-8");
48
+ const rendered = template.replace(/__VERSION__/g, version);
49
+
50
+ await writeFile(manifest.outputPath, rendered);
51
+
52
+ const parsed = JSON.parse(rendered) as { name?: string; version?: string; skills?: string };
53
+ if (parsed.name !== "context") {
54
+ throw new Error(`${manifest.label} plugin.json name must be "context", got "${parsed.name}"`);
55
+ }
56
+ if (parsed.version !== version) {
57
+ throw new Error(`${manifest.label} plugin.json version mismatch: ${parsed.version} vs ${version}`);
58
+ }
59
+ if (manifest.label === "codex" && parsed.skills !== "./skills/") {
60
+ throw new Error(`codex plugin.json must point skills to "./skills/", got "${parsed.skills}"`);
61
+ }
62
+
63
+ process.stdout.write(`${manifest.label} plugin.json generated (name=${parsed.name}, version=${parsed.version})\n`);
64
+ }
65
+ }
66
+
67
+ main().catch((err) => {
68
+ process.stderr.write(`build-plugin failed: ${err instanceof Error ? err.message : String(err)}\n`);
69
+ process.exit(1);
70
+ });
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall hook for @c4a/context-cli.
4
+ *
5
+ * The goal is narrow: when someone installs this package as a Claude Code
6
+ * plugin, the `context` bin should end up on PATH so `/context:*` slash
7
+ * commands can Bash out to it. In every other install scenario (library
8
+ * dependency, `bun install -g`, CI, local `bun link`) we do nothing.
9
+ *
10
+ * Dangerous things this script used to do (review findings 高-1 / 高-2):
11
+ * - Recursion: the child `bun install -g @c4a/context-cli` inherits the
12
+ * same Claude env vars that triggered us, runs postinstall again, and
13
+ * loops. Fixed by (a) hard re-entry guard via CONTEXT_CLI_POSTINSTALL
14
+ * in the child env, and (b) skipping entirely when we are already
15
+ * inside a global install (`npm_config_global === "true"`).
16
+ * - Dev mode break: pinning the registry spec `@c4a/context-cli@<version>`
17
+ * ignores the actual install source (local tarball, file:, github:,
18
+ * unpublished pre-release). Fixed by never spawning a child install;
19
+ * we print a single actionable hint with the registry fallback command.
20
+ * The plugin tarball already contains the whole CLI, so the user can
21
+ * just `bun install -g <same source>` themselves — we can't guess the
22
+ * source reliably from inside postinstall.
23
+ *
24
+ * Structure: all decision logic lives in the pure, exported `decide()`
25
+ * function. The top-level side-effect wrapper is the only thing that reads
26
+ * `process.env`, spawns the PATH probe, or writes to stderr. This split
27
+ * lets a sibling `.test.ts` exercise every branch without spawning
28
+ * subprocesses (Bun test's subprocess sandboxing has been unreliable in
29
+ * this package, so in-process tests are the practical way to cover the
30
+ * script).
31
+ */
32
+
33
+ import { spawnSync } from "node:child_process";
34
+ import { realpathSync } from "node:fs";
35
+ import process from "node:process";
36
+ import { pathToFileURL } from "node:url";
37
+
38
+ export const RE_ENTRY_ENV = "CONTEXT_CLI_POSTINSTALL";
39
+
40
+ /**
41
+ * Pure decision function. Given an env snapshot and a "does `context` bin
42
+ * exist on PATH?" predicate result, return either a hint to print or a
43
+ * "do nothing" sentinel.
44
+ *
45
+ * Shape:
46
+ * { action: "skip", reason: "<why>" }
47
+ * | { action: "hint", lines: string[] }
48
+ *
49
+ * Every branch that reads process state in the real run is modeled here so
50
+ * tests can pass synthetic envs. The caller decides whether to actually
51
+ * print or set the re-entry guard.
52
+ */
53
+ export function decide(env, hasContextBin) {
54
+ if (env[RE_ENTRY_ENV] === "1") {
55
+ return { action: "skip", reason: "re-entry guard" };
56
+ }
57
+ if (env.CI === "true" || env.CI === "1") {
58
+ return { action: "skip", reason: "CI" };
59
+ }
60
+ if (env.npm_config_global === "true") {
61
+ return { action: "skip", reason: "already global install" };
62
+ }
63
+ if (env.CONTEXT_CLI_SKIP_AUTO_LINK === "1") {
64
+ return { action: "skip", reason: "explicit opt-out" };
65
+ }
66
+ if (!looksLikeClaudePluginInstall(env)) {
67
+ return { action: "skip", reason: "not a Claude plugin install" };
68
+ }
69
+ if (hasContextBin) {
70
+ return { action: "skip", reason: "context already on PATH" };
71
+ }
72
+ const version = env.npm_package_version ?? "latest";
73
+ return {
74
+ action: "hint",
75
+ lines: [
76
+ "Claude plugin installed. To enable `/context:*` slash commands, make " +
77
+ "the `context` bin available on PATH by running one of:",
78
+ ` bun install -g @c4a/context-cli@${version} # install from npm registry`,
79
+ " bun link # if installing a local build",
80
+ ],
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Heuristic: does the caller's env smell like a Claude plugin install?
86
+ * We require the env var's *value* to contain "claude" (case-insensitive),
87
+ * not just the var to be set — `npm_config_user_agent` is set by every
88
+ * install, and `CLAUDE_PLUGIN_INSTALL=0` should not trigger us either.
89
+ */
90
+ export function looksLikeClaudePluginInstall(env) {
91
+ const hints = ["CLAUDE_PLUGIN_INSTALL", "CLAUDE_CODE_PLUGIN", "npm_config_user_agent"];
92
+ for (const key of hints) {
93
+ const value = env[key];
94
+ if (!value) continue;
95
+ if (value.toLowerCase().includes("claude")) return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * Real-world `hasContextBin` probe: shell out to `command -v` / `where`.
102
+ * Kept separate from `decide()` so tests can inject a synthetic value.
103
+ */
104
+ export function probeContextBin() {
105
+ const lookup = process.platform === "win32"
106
+ ? spawnSync("where", ["context"], { stdio: "ignore" })
107
+ : spawnSync("sh", ["-c", "command -v context"], { stdio: "ignore" });
108
+ return lookup.status === 0;
109
+ }
110
+
111
+ function log(msg) {
112
+ process.stderr.write(`[context-cli postinstall] ${msg}\n`);
113
+ }
114
+
115
+ function main() {
116
+ const env = process.env;
117
+ // First pass with hasContextBin=false tells us whether every *other* gate
118
+ // was passed. Only if the decision is "hint" do we need the real PATH
119
+ // probe (avoids shelling out to sh/where in the common skip cases).
120
+ const provisional = decide(env, false);
121
+ if (provisional.action === "skip") return;
122
+
123
+ // Decision said "hint" — now probe the real PATH. If context IS on PATH,
124
+ // the user doesn't need the hint.
125
+ const final = decide(env, probeContextBin());
126
+ if (final.action === "hint") {
127
+ for (const line of final.lines) log(line);
128
+ }
129
+ // Set the re-entry guard so any child process we may spawn in the future
130
+ // (we currently don't, but belt-and-suspenders against recursion) will
131
+ // inherit it and skip their own postinstall.
132
+ env[RE_ENTRY_ENV] = "1";
133
+ }
134
+
135
+ // Side-effect wrapper only runs when this file is executed directly as a
136
+ // script (the postinstall lifecycle does that). When the file is imported
137
+ // by a test, `main()` is not invoked.
138
+ //
139
+ // IMPORTANT — two quirks that the obvious
140
+ // `import.meta.url === \`file://${process.argv[1]}\``
141
+ // check silently fails on, producing an invisible-no-op postinstall:
142
+ //
143
+ // 1. **Percent-encoding**: `import.meta.url` is always WHATWG-encoded
144
+ // (`%20` for spaces, %-hex for many punctuation chars, uppercase
145
+ // Windows drive letters, UNC `\\?\` prefixes). Raw `argv[1]` isn't.
146
+ // A package install under `/Users/Dev Tools/...` reproducibly no-ops
147
+ // with the interpolated check; going through `pathToFileURL` fixes
148
+ // it.
149
+ // 2. **Symlink resolution**: on macOS `/tmp` is a symlink to
150
+ // `/private/tmp`, and `import.meta.url` resolves that; `argv[1]`
151
+ // does not. npm stashes packages under symlinked paths on some
152
+ // setups (npm's cache, nvm prefixes, corporate homedir mounts). We
153
+ // realpath `argv[1]` before URL-encoding so both sides compare the
154
+ // canonical target.
155
+ //
156
+ // Either quirk alone turns the postinstall script into a silent no-op —
157
+ // exactly the invisible-failure mode npm postinstall scripts are
158
+ // infamous for. The combined check below is validated by the subprocess
159
+ // tests in postinstall.test.ts.
160
+ export function isDirectRun(metaUrl, argv1) {
161
+ if (!argv1) return false;
162
+ try {
163
+ let resolved = argv1;
164
+ try {
165
+ resolved = realpathSync(argv1);
166
+ } catch {
167
+ // argv[1] may not exist yet (weird, but possible under npm install
168
+ // lifecycle timing). Fall back to the raw path — pathToFileURL
169
+ // still normalizes encoding, so spaces-but-no-symlinks still works.
170
+ }
171
+ return metaUrl === pathToFileURL(resolved).href;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ if (isDirectRun(import.meta.url, process.argv[1])) {
178
+ try {
179
+ main();
180
+ } catch (err) {
181
+ log(`postinstall error: ${err instanceof Error ? err.message : String(err)}`);
182
+ }
183
+ }
@@ -0,0 +1,29 @@
1
+ # aspect: code
2
+
3
+ > Code structure aspect. The `context capture-code` command invokes the
4
+ > `@c4a/extract` / `@c4a/extract-ts` pipeline directly — it does not read
5
+ > this file. The prompt exists as a placeholder so the init flow is
6
+ > uniform and so a future scripted-aspect mechanism can take over without
7
+ > reshuffling directory structure.
8
+
9
+ ## Target
10
+
11
+ Extract `Package` / `Symbol` nodes and `imports` / `calls` / `extends` edges
12
+ from the current git worktree.
13
+
14
+ ## Schema
15
+
16
+ Payloads are written by the CLI as JSONL files under each dated bucket:
17
+
18
+ - `packages.jsonl` — one `Package` per line
19
+ - `symbols.jsonl` — one `Symbol` per line (recursive `members` flattened)
20
+ - `edges.jsonl` — one relation per line (optional)
21
+ - `_meta.yaml` — bucket metadata (`commit`, `captured_at`, `script_hash`,
22
+ `content_hash`)
23
+
24
+ ## Notes
25
+
26
+ - Currently driven by the CLI, not by a per-aspect script. The scripted
27
+ `capture.ts` contract for non-code aspects is planned for a later release.
28
+ - source-id is always `aspect:code`.
29
+ - Bucket name is `YYYY-MM-DD-<sha7>` where `<sha7>` is `git rev-parse --short HEAD`.
@@ -0,0 +1,27 @@
1
+ # aspect: design-system
2
+
3
+ > Design-system aspect placeholder. The current CLI does not yet provide
4
+ > a scripted capture driver for this aspect — `context capture-aspect
5
+ > design-system` will report that scripted aspects beyond `code` are not
6
+ > implemented. This file exists so the init flow keeps the aspect slot
7
+ > consistent for a future scripted-capture release.
8
+
9
+ ## Target
10
+
11
+ Extract design tokens (color / spacing / typography) and component usage
12
+ patterns from the codebase and design docs.
13
+
14
+ ## Schema (planned contract)
15
+
16
+ When scripted capture lands, payloads will be written into dated buckets
17
+ under `raw/aspect/design-system/<YYYY-MM-DD>-<hash7>/`:
18
+
19
+ - `facts.jsonl` — one design-system fact per line (token definitions, usage
20
+ rules, deprecations)
21
+ - `_meta.yaml` — bucket metadata (`token_count`, `source_files`,
22
+ `captured_at`, `script_hash`, `content_hash`)
23
+
24
+ ## Notes
25
+
26
+ - Not executable by the current CLI.
27
+ - source-id reserved as `aspect:design-system`.
@@ -0,0 +1,24 @@
1
+ # aspect: graphql
2
+
3
+ > GraphQL aspect placeholder. The current CLI does not yet provide a
4
+ > scripted capture driver — `context capture-aspect graphql` will report
5
+ > that scripted aspects beyond `code` are not implemented.
6
+
7
+ ## Target
8
+
9
+ Extract GraphQL type definitions, resolver mappings, and usage patterns
10
+ from schema files and server code.
11
+
12
+ ## Schema (planned contract)
13
+
14
+ Payloads produced by the future scripted capture:
15
+
16
+ - `facts.jsonl` — GraphQL facts (type definitions, field descriptions,
17
+ resolver docs, deprecations)
18
+ - `_meta.yaml` — bucket metadata (`schema_file`, `type_count`,
19
+ `captured_at`, `script_hash`, `content_hash`)
20
+
21
+ ## Notes
22
+
23
+ - Not executable by the current CLI.
24
+ - source-id reserved as `aspect:graphql`.
@@ -0,0 +1,24 @@
1
+ # aspect: openapi
2
+
3
+ > OpenAPI aspect placeholder. The current CLI does not yet provide a
4
+ > scripted capture driver — `context capture-aspect openapi` will report
5
+ > that scripted aspects beyond `code` are not implemented.
6
+
7
+ ## Target
8
+
9
+ Extract HTTP endpoints, request/response schemas, and auth requirements
10
+ from OpenAPI spec files in the repo.
11
+
12
+ ## Schema (planned contract)
13
+
14
+ Payloads produced by the future scripted capture will land under dated
15
+ buckets and contain:
16
+
17
+ - `facts.jsonl` — endpoint-level facts (path, method, auth, deprecation)
18
+ - `_meta.yaml` — bucket metadata (`spec_version`, `endpoint_count`,
19
+ `captured_at`, `script_hash`, `content_hash`)
20
+
21
+ ## Notes
22
+
23
+ - Not executable by the current CLI.
24
+ - source-id reserved as `aspect:openapi`.
Binary file
Binary file
Binary file