@opvs-ai/cli 0.23.0 → 0.25.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 +73 -0
- package/claude-plugin/.claude-plugin/marketplace.json +14 -0
- package/claude-plugin/.claude-plugin/plugin.json +9 -0
- package/claude-plugin/.mcp.json +8 -0
- package/claude-plugin/README.md +24 -0
- package/claude-plugin/hooks/hooks.json +50 -0
- package/claude-plugin/skills/remember/SKILL.md +31 -0
- package/claude-plugin/skills/remember/scripts/remember.mjs +121 -0
- package/dist/.build-stamp.json +4 -4
- package/dist/commands/code-graph.d.ts +17 -0
- package/dist/commands/code-graph.js +64 -0
- package/dist/commands/code-graph.js.map +1 -0
- package/dist/commands/code.d.ts +13 -3
- package/dist/commands/code.js +191 -3
- package/dist/commands/code.js.map +1 -1
- package/dist/commands/memory-capture.d.ts +76 -0
- package/dist/commands/memory-capture.js +592 -0
- package/dist/commands/memory-capture.js.map +1 -0
- package/dist/commands/memory-hook.d.ts +1 -0
- package/dist/commands/memory-hook.js +35 -11
- package/dist/commands/memory-hook.js.map +1 -1
- package/dist/commands/memory-install.d.ts +17 -0
- package/dist/commands/memory-install.js +349 -0
- package/dist/commands/memory-install.js.map +1 -0
- package/dist/commands/memory-lessons.d.ts +8 -0
- package/dist/commands/memory-lessons.js +135 -0
- package/dist/commands/memory-lessons.js.map +1 -0
- package/dist/commands/memory.d.ts +1 -9
- package/dist/commands/memory.js +17 -85
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/orgChart.d.ts +3 -0
- package/dist/commands/orgChart.js +88 -0
- package/dist/commands/orgChart.js.map +1 -0
- package/dist/commands/tasks.js +45 -3
- package/dist/commands/tasks.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/redact.d.ts +16 -0
- package/dist/lib/redact.js +120 -0
- package/dist/lib/redact.js.map +1 -0
- package/memory-services.d.ts +9 -0
- package/memory-services.js +118 -0
- package/package.json +5 -1
- package/runtime-plugins/cline/opvs-memory.ts +99 -0
- package/runtime-plugins/opencode/opvs-memory.ts +94 -0
package/README.md
CHANGED
|
@@ -288,6 +288,79 @@ Needs the `code:read` scope, which is in the default ceiling. There is no
|
|
|
288
288
|
`opvs code index` command on purpose — writing an index is `code:write`, which
|
|
289
289
|
a worker PAT does not carry, and the ingest caller is the sidecar on the desk.
|
|
290
290
|
|
|
291
|
+
### Skill lessons
|
|
292
|
+
|
|
293
|
+
Pull and contribute the lessons a skill's collectors left in the typed marketplace
|
|
294
|
+
(AS-590). The reference is `<vendor>/<name>` (a leading `@` on the vendor is
|
|
295
|
+
optional); the CLI validates it locally before any request.
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
opvs memory lessons get opvs-ai/opvs-vibe # Pull a skill's lessons
|
|
299
|
+
opvs memory lessons get opvs-ai/opvs-vibe --query retry # FTS over body + tags
|
|
300
|
+
opvs memory lessons get opvs-ai/opvs-vibe --limit 50 --since cur-1
|
|
301
|
+
|
|
302
|
+
opvs memory lessons add opvs-ai/opvs-vibe \
|
|
303
|
+
--kind learning \
|
|
304
|
+
--body "Retry on 429 with jitter; never under 200ms"
|
|
305
|
+
|
|
306
|
+
opvs memory lessons add opvs-ai/opvs-vibe \
|
|
307
|
+
--kind pattern --body-file ./note.md --tags "retry,http"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`add` ships five keys exactly — `kind`, `body`, `tags`, `source_engagement_hash`,
|
|
311
|
+
`craft_id` — because the server-side `CraftContribute` schema sets
|
|
312
|
+
`extra="forbid"` (AS-445 sealed anything that looked like a credential). The
|
|
313
|
+
provenance hash is `sha256("cli:" + workspace_slug + ":" + body)` and NEVER
|
|
314
|
+
carries a brand id.
|
|
315
|
+
|
|
316
|
+
### Runtime memory hooks
|
|
317
|
+
|
|
318
|
+
Install the SessionStart memory hook for Codex, the complete checked-in plugin
|
|
319
|
+
hook set for Claude Code, the OPVS MCP read path and capture hooks for Cursor,
|
|
320
|
+
or a managed capture plugin for OpenCode or Cline. Existing JSON entries are
|
|
321
|
+
merged and repeated installs are no-ops.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
opvs memory install codex
|
|
325
|
+
opvs memory install claude-code
|
|
326
|
+
opvs memory install cursor
|
|
327
|
+
opvs memory install opencode
|
|
328
|
+
opvs memory install cline
|
|
329
|
+
opvs memory install codex --project # writes ./.codex/hooks.json
|
|
330
|
+
opvs memory install cursor --project # writes ./.cursor/{mcp,hooks}.json
|
|
331
|
+
opvs memory install opencode --project # writes ./.opencode/plugins/opvs-memory.ts
|
|
332
|
+
opvs memory uninstall codex # removes only OPVS memory hooks
|
|
333
|
+
opvs memory uninstall cursor # removes only OPVS MCP/hooks entries
|
|
334
|
+
opvs memory uninstall cline # removes only the managed OPVS plugin
|
|
335
|
+
opvs memory uninstall codex --project
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
User-level Codex hooks use `$CODEX_HOME/hooks.json` (default
|
|
339
|
+
`~/.codex/hooks.json`). User-level Claude Code hooks use
|
|
340
|
+
`$CLAUDE_CONFIG_DIR/settings.json` (default `~/.claude/settings.json`), with
|
|
341
|
+
the plugin hook set merged under its `hooks` key. `--project` uses
|
|
342
|
+
`./.codex/hooks.json` or `./.claude/settings.json` in the current project.
|
|
343
|
+
Installing the bundled Claude Code plugin through Claude Code's plugin
|
|
344
|
+
installer is an alternative to `opvs memory install claude-code`; plugins load
|
|
345
|
+
their own `hooks/hooks.json` as part of plugin installation.
|
|
346
|
+
|
|
347
|
+
Cursor uses `~/.cursor/mcp.json` and `~/.cursor/hooks.json`, or the matching
|
|
348
|
+
project files with `--project`. The MCP entry starts `opvs-mcp`, which provides
|
|
349
|
+
the OPVS memory read tools. The installed `postToolUse`, `stop`, and
|
|
350
|
+
`sessionEnd` hooks feed the same local capture pipeline as Claude Code.
|
|
351
|
+
|
|
352
|
+
OpenCode installs `opvs-memory.ts` under
|
|
353
|
+
`~/.config/opencode/plugins/` (or `./.opencode/plugins/` with `--project`).
|
|
354
|
+
Cline installs it under `~/.cline/plugins/` (or `./.cline/plugins/` with
|
|
355
|
+
`--project`). Each file is marked as OPVS-managed, so uninstall will not remove
|
|
356
|
+
an unmarked file or any other plugin in the directory. The plugins forward
|
|
357
|
+
tool-after and idle/run-end events to `opvs memory hook` with a two-second
|
|
358
|
+
timeout and fail open. They only upload capture data after the user explicitly
|
|
359
|
+
runs `opvs memory capture on`.
|
|
360
|
+
Installing hooks does **not** enable capture: run `opvs memory capture on` with
|
|
361
|
+
a dedicated PAT whose only scope is `memory:ingest`, and use
|
|
362
|
+
`opvs memory capture off` to remove that local PAT again.
|
|
363
|
+
|
|
291
364
|
### Session
|
|
292
365
|
|
|
293
366
|
```bash
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opvs",
|
|
3
|
+
"description": "OPVS plugins for agent workflows",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "OPVS"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "opvs-memory",
|
|
10
|
+
"source": "./",
|
|
11
|
+
"description": "Read relevant typed memories and record new lessons from Claude Code."
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# OPVS Memory for Claude Code
|
|
2
|
+
|
|
3
|
+
This plugin loads relevant typed memories when a Claude Code session starts, exposes the OPVS memory MCP tools, and provides the `/opvs-memory:remember` skill for recording a validated lesson in the current repository.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Install and authenticate the OPVS CLI (`opvs auth request ...`).
|
|
8
|
+
- Use a PAT with `memory:read` scope.
|
|
9
|
+
- Have Node.js available so Claude Code can launch `@opvs-ai/mcp`.
|
|
10
|
+
|
|
11
|
+
## Install from this checkout
|
|
12
|
+
|
|
13
|
+
From the repository root:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
claude plugin marketplace add ./packages/cli/claude-plugin
|
|
17
|
+
claude plugin install opvs-memory@opvs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Start a new Claude Code session after installation. The SessionStart hook calls `opvs memory hook session-start`; when memory is unavailable or the CLI is not logged in, it stays silent and does not block startup.
|
|
21
|
+
|
|
22
|
+
Use `/opvs-memory:remember` to record a lesson. The skill follows `memory/SPEC.md`, writes with exclusive-create semantics, and refuses to overwrite an existing entry.
|
|
23
|
+
|
|
24
|
+
The plugin starts the published MCP server with `npx -y @opvs-ai/mcp`. The server reads the same local OPVS authentication as the CLI; no credential is stored in this plugin.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear|compact|fork",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "opvs memory hook session-start",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PostToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "*",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "opvs memory hook post-tool",
|
|
22
|
+
"timeout": 10
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"Stop": [
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "opvs memory hook stop",
|
|
33
|
+
"timeout": 10
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"SessionEnd": [
|
|
39
|
+
{
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "opvs memory hook session-end",
|
|
44
|
+
"timeout": 10
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: remember
|
|
3
|
+
description: Record a durable, typed lesson in the repository's registered service planning folder.
|
|
4
|
+
argument-hint: "<lesson to remember>"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: Read, Grep, Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Remember a lesson
|
|
10
|
+
|
|
11
|
+
Use this workflow only for a durable lesson supported by evidence. Do not use it for a preference, guess, secret, credential, or temporary task state.
|
|
12
|
+
|
|
13
|
+
1. Read `memory/SPEC.md` sections 2, 3, and 5 and `memory/services.yaml` in the current repository.
|
|
14
|
+
2. Identify the registered service whose `planning_dir` owns the lesson. Use that service id and planning directory; do not invent either value.
|
|
15
|
+
3. Turn the lesson into one standalone core line. Collect the required `evidence`, runnable `verify`, expected result, and a control that makes verification fail. Ask the user for any required value that cannot be established from the session.
|
|
16
|
+
4. Run the writer below from the repository root. Pass the active card ref as `--card`; if there is no card, explain that this writer requires one and stop. Quote every value as a separate shell argument.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/remember/scripts/remember.mjs" \
|
|
20
|
+
--service "<service-id>" \
|
|
21
|
+
--planning-dir "<registered-planning-dir>" \
|
|
22
|
+
--card "<CARD-123>" \
|
|
23
|
+
--core "<one-line lesson>" \
|
|
24
|
+
--topic "<short topic>" \
|
|
25
|
+
--evidence "<observed evidence>" \
|
|
26
|
+
--verify "<SPEC-compliant command>" \
|
|
27
|
+
--expect "<SPEC-compliant expectation>" \
|
|
28
|
+
--control "<change that makes verification fail>"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The writer derives the UTC date and branch slug, creates one file with exclusive-create semantics, and prints its relative path. Read the generated file and check its header, routing line, and core line before reporting success. Never overwrite, move, or delete an existing memory entry.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
function required(value, name) {
|
|
8
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
9
|
+
throw new Error(`${name} is required`);
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function oneLine(value, name) {
|
|
15
|
+
const checked = required(value, name);
|
|
16
|
+
if (/\r|\n/.test(checked)) throw new Error(`${name} must be one line`);
|
|
17
|
+
return checked;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slug(value) {
|
|
21
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function yamlString(value) {
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildLesson(input) {
|
|
29
|
+
const date = required(input.date, "date");
|
|
30
|
+
const card = required(input.card, "card");
|
|
31
|
+
const service = required(input.service, "service");
|
|
32
|
+
const branchSlug = slug(required(input.branch, "branch"));
|
|
33
|
+
const core = oneLine(input.core, "core");
|
|
34
|
+
const topic = slug(required(input.topic, "topic"));
|
|
35
|
+
const evidence = oneLine(input.evidence, "evidence");
|
|
36
|
+
const verify = oneLine(input.verify, "verify");
|
|
37
|
+
const expect = oneLine(input.expect, "expect");
|
|
38
|
+
const control = oneLine(input.control, "control");
|
|
39
|
+
const confidence = input.confidence ?? 0.9;
|
|
40
|
+
|
|
41
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error("date must be YYYY-MM-DD");
|
|
42
|
+
if (!/^[A-Z][A-Z0-9]*-\d+$/.test(card)) throw new Error("card must look like ABC-123");
|
|
43
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(service)) {
|
|
44
|
+
throw new Error("service must be a lower-case service id");
|
|
45
|
+
}
|
|
46
|
+
if (!branchSlug) throw new Error("branch must produce a slug");
|
|
47
|
+
if (typeof confidence !== "number" || confidence < 0 || confidence > 1) {
|
|
48
|
+
throw new Error("confidence must be between 0 and 1");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stem = `${date}-${card}-${branchSlug}`;
|
|
52
|
+
const filename = `${stem}.md`;
|
|
53
|
+
const detail = typeof input.detail === "string" && input.detail.trim()
|
|
54
|
+
? `\n\n${input.detail.trim()}\n`
|
|
55
|
+
: "\n";
|
|
56
|
+
const text = `---
|
|
57
|
+
id: ${service}/lesson/${stem}
|
|
58
|
+
type: lesson
|
|
59
|
+
author: session:${card}
|
|
60
|
+
card: ${card}
|
|
61
|
+
evidence: ${yamlString(evidence)}
|
|
62
|
+
verify: ${yamlString(verify)}
|
|
63
|
+
expect: ${yamlString(expect)}
|
|
64
|
+
control: ${yamlString(control)}
|
|
65
|
+
---
|
|
66
|
+
@${service}>${service} . #lrn #${topic} $f:${confidence} $t:${date}
|
|
67
|
+
${core}${detail}`;
|
|
68
|
+
|
|
69
|
+
return { filename, text };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeLesson(input, options = {}) {
|
|
73
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
74
|
+
const planningDir = required(input.planningDir, "planning-dir");
|
|
75
|
+
if (isAbsolute(planningDir) || planningDir.split(/[\\/]+/).includes("..")) {
|
|
76
|
+
throw new Error("planning-dir must be a repository-relative path without '..'");
|
|
77
|
+
}
|
|
78
|
+
if (!planningDir.startsWith("docs/planning/")) {
|
|
79
|
+
throw new Error("planning-dir must name a registered docs/planning service folder");
|
|
80
|
+
}
|
|
81
|
+
const planningRoot = resolve(cwd, planningDir);
|
|
82
|
+
if (!existsSync(planningRoot) || relative(cwd, planningRoot).startsWith("..")) {
|
|
83
|
+
throw new Error("planning-dir must be an existing directory inside the repository");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
87
|
+
const branch = options.branch ?? execFileSync(
|
|
88
|
+
"git",
|
|
89
|
+
["branch", "--show-current"],
|
|
90
|
+
{ cwd, encoding: "utf8" },
|
|
91
|
+
).trim();
|
|
92
|
+
const rendered = buildLesson({ ...input, date, branch });
|
|
93
|
+
const lessonsDir = resolve(planningRoot, "learnings");
|
|
94
|
+
mkdirSync(lessonsDir, { recursive: true });
|
|
95
|
+
const target = resolve(lessonsDir, rendered.filename);
|
|
96
|
+
writeFileSync(target, rendered.text, { encoding: "utf8", flag: "wx" });
|
|
97
|
+
return relative(cwd, target);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseArgs(argv) {
|
|
101
|
+
const values = {};
|
|
102
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
103
|
+
const flag = argv[index];
|
|
104
|
+
const value = argv[index + 1];
|
|
105
|
+
if (!flag?.startsWith("--") || value === undefined) {
|
|
106
|
+
throw new Error("arguments must be --name value pairs");
|
|
107
|
+
}
|
|
108
|
+
values[flag.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
|
|
109
|
+
}
|
|
110
|
+
return values;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
114
|
+
try {
|
|
115
|
+
const output = writeLesson(parseArgs(process.argv.slice(2)));
|
|
116
|
+
process.stdout.write(`${output}\n`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/.build-stamp.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stamp_version": 1,
|
|
3
3
|
"package": "@opvs-ai/cli",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_files":
|
|
7
|
-
"built_at": "2026-10-
|
|
4
|
+
"version": "0.25.0",
|
|
5
|
+
"source_digest": "c28954975be895edf1690dae093fd79c1e0b18878735bc42d10fb7901df6b980",
|
|
6
|
+
"source_files": 54,
|
|
7
|
+
"built_at": "2026-10-04T07:22:40.592Z",
|
|
8
8
|
"node": "v22.23.3"
|
|
9
9
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opvs code graph — a node's import neighbourhood, built against the agreed route shape
|
|
3
|
+
* (TCI-113 · design §7).
|
|
4
|
+
*
|
|
5
|
+
* The CLI does not interpret the answer. It builds the URL the API contract describes
|
|
6
|
+
* (`GET /api/v1/code/{project_id}/graph`) and lets `printResponse` write the server's
|
|
7
|
+
* bytes — including a 422 when the server rejects `depth` or `limit`. That parity is
|
|
8
|
+
* what makes the "CLI and HTTP return the same YAML" claim testable rather than
|
|
9
|
+
* aspirational, the same property the `context` and `summary` subcommands have.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ Registration precondition. The `code` parent command must already be attached to
|
|
12
|
+
* the program. A silent no-op would ship a CLI without the command — the whole point
|
|
13
|
+
* of this file is to register `graph`, so a missing parent is a programmer error and
|
|
14
|
+
* we throw.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
export declare function registerCodeGraphCommand(program: Command): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opvs code graph — a node's import neighbourhood, built against the agreed route shape
|
|
3
|
+
* (TCI-113 · design §7).
|
|
4
|
+
*
|
|
5
|
+
* The CLI does not interpret the answer. It builds the URL the API contract describes
|
|
6
|
+
* (`GET /api/v1/code/{project_id}/graph`) and lets `printResponse` write the server's
|
|
7
|
+
* bytes — including a 422 when the server rejects `depth` or `limit`. That parity is
|
|
8
|
+
* what makes the "CLI and HTTP return the same YAML" claim testable rather than
|
|
9
|
+
* aspirational, the same property the `context` and `summary` subcommands have.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ Registration precondition. The `code` parent command must already be attached to
|
|
12
|
+
* the program. A silent no-op would ship a CLI without the command — the whole point
|
|
13
|
+
* of this file is to register `graph`, so a missing parent is a programmer error and
|
|
14
|
+
* we throw.
|
|
15
|
+
*/
|
|
16
|
+
import { getEffectiveConfig, createClient } from "@opvs-ai/core";
|
|
17
|
+
import { buildUrl, printResponse } from "../lib/format.js";
|
|
18
|
+
export function registerCodeGraphCommand(program) {
|
|
19
|
+
const code = program.commands.find((c) => c.name() === "code");
|
|
20
|
+
if (!code) {
|
|
21
|
+
throw new Error("registerCodeGraphCommand: 'code' command is not registered — call registerCodeCommands(program) first");
|
|
22
|
+
}
|
|
23
|
+
code
|
|
24
|
+
.command("graph <node_id>")
|
|
25
|
+
.description("A node's import neighbourhood — who imports it, what it imports, one or two hops")
|
|
26
|
+
.requiredOption("--project <id>", "Project id (board_groups.id where kind=project)")
|
|
27
|
+
.option("--direction <dir>", "in | out | both — which side of <node_id> to walk from")
|
|
28
|
+
.option("--depth <n>", "1 | 2 — server rejects 3+ with a 422")
|
|
29
|
+
.option("--limit <n>", "Max neighbours (server default 100, clamps at 500)")
|
|
30
|
+
.option("--branch <name>", "Answer from a specific indexed branch")
|
|
31
|
+
.option("--format <fmt>", "yaml | md | json — overrides the configured default")
|
|
32
|
+
.action(async (nodeId, opts) => {
|
|
33
|
+
const wsSlug = program.opts().workspace;
|
|
34
|
+
const cfg = getEffectiveConfig(wsSlug);
|
|
35
|
+
try {
|
|
36
|
+
const client = createClient(wsSlug);
|
|
37
|
+
// Send ONLY the options the user set. A `direction: undefined` would become
|
|
38
|
+
// `direction=undefined` on the wire and the server would read it as a real
|
|
39
|
+
// filter — the same `code context` defect (TCI-12) this card inherits.
|
|
40
|
+
const params = { node_id: nodeId };
|
|
41
|
+
if (opts.direction)
|
|
42
|
+
params.direction = opts.direction;
|
|
43
|
+
if (opts.depth)
|
|
44
|
+
params.depth = opts.depth;
|
|
45
|
+
if (opts.limit)
|
|
46
|
+
params.limit = opts.limit;
|
|
47
|
+
if (opts.branch)
|
|
48
|
+
params.branch = opts.branch;
|
|
49
|
+
// The server is the judge of `depth` (1/2 → 200, 3+ → 422) and `limit`
|
|
50
|
+
// (default 100, clamps at 500). Forwarding both verbatim is what lets a
|
|
51
|
+
// client discover the cap from the response, instead of having the CLI
|
|
52
|
+
// pre-mangle a request the API was built to refuse with a precise code.
|
|
53
|
+
const url = buildUrl(`/api/v1/code/${opts.project}/graph`, params, opts.format || cfg.format);
|
|
54
|
+
const res = await client.get(url);
|
|
55
|
+
await printResponse(res);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const e = err;
|
|
59
|
+
console.error(`Error: ${e.detail || "Failed to query code graph"}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=code-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-graph.js","sourceRoot":"","sources":["../../src/commands/code-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,UAAU,wBAAwB,CAAC,OAAgB;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAC;IAC/D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,uGAAuG,CACxG,CAAC;IACJ,CAAC;IAED,IAAI;SACD,OAAO,CAAC,iBAAiB,CAAC;SAC1B,WAAW,CAAC,kFAAkF,CAAC;SAC/F,cAAc,CAAC,gBAAgB,EAAE,iDAAiD,CAAC;SACnF,MAAM,CAAC,mBAAmB,EAAE,wDAAwD,CAAC;SACrF,MAAM,CAAC,aAAa,EAAE,sCAAsC,CAAC;SAC7D,MAAM,CAAC,aAAa,EAAE,oDAAoD,CAAC;SAC3E,MAAM,CAAC,iBAAiB,EAAE,uCAAuC,CAAC;SAClE,MAAM,CAAC,gBAAgB,EAAE,qDAAqD,CAAC;SAC/E,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,IAAI,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC;QACxC,MAAM,GAAG,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACpC,4EAA4E;YAC5E,2EAA2E;YAC3E,uEAAuE;YACvE,MAAM,MAAM,GAA2B,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;YAC3D,IAAI,IAAI,CAAC,SAAS;gBAAE,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YACtD,IAAI,IAAI,CAAC,KAAK;gBAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YAC1C,IAAI,IAAI,CAAC,KAAK;gBAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YAC1C,IAAI,IAAI,CAAC,MAAM;gBAAE,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAC7C,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,MAAM,GAAG,GAAG,QAAQ,CAClB,gBAAgB,IAAI,CAAC,OAAO,QAAQ,EACpC,MAAM,EACN,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAC1B,CAAC;YACF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClC,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,GAA0B,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,IAAI,4BAA4B,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/commands/code.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opvs code — the tenant code index query door (TCI · 2.2)
|
|
2
|
+
* opvs code — the tenant code index query door (TCI · 2.2 + 2.4)
|
|
3
3
|
*
|
|
4
4
|
* The question this answers is "does this already exist in the repo?", asked by
|
|
5
|
-
* an agent that is about to write something. It is the CLI face of
|
|
6
|
-
*
|
|
5
|
+
* an agent that is about to write something. It is the CLI face of every read
|
|
6
|
+
* the API serves under `/api/v1/code/{project_id}/`: context, summary, tree,
|
|
7
|
+
* files, symbols, and the stable-node reads (resolve, changes).
|
|
7
8
|
*
|
|
8
9
|
* ⚠️ THE VERDICT IS NOT THE WHOLE ANSWER.
|
|
9
10
|
* `indexed: false` and `verdict: "none"` are DIFFERENT facts and the API returns
|
|
@@ -18,6 +19,15 @@
|
|
|
18
19
|
* scope ceiling. `code:write` (the ingest lane) is deliberately not, and there is
|
|
19
20
|
* no CLI command for it: an index pass is a 50,000-row bulk load and a worker
|
|
20
21
|
* session must not be able to spend one (D5).
|
|
22
|
+
*
|
|
23
|
+
* SEND ONLY WHAT THE USER SET (TCI-112, P2.4)
|
|
24
|
+
* ---------------------------------------------
|
|
25
|
+
* Every option here is optional except `--project`. The brief is explicit: do
|
|
26
|
+
* not invent a default the server did not ask for. A `branch=undefined` on the
|
|
27
|
+
* wire is a real filter reading as "the branch named 'undefined'"; a
|
|
28
|
+
* `directory=` left out of `/files` lets the API apply its own `.` default; a
|
|
29
|
+
* `format=undefined` is a literal string on the URL. Each `if (opts.x) ...`
|
|
30
|
+
* gate exists so the request the server sees is the request the user asked for.
|
|
21
31
|
*/
|
|
22
32
|
import { Command } from "commander";
|
|
23
33
|
export declare function registerCodeCommands(program: Command): void;
|