@cairnkeep/cli 2.8.0 → 2.9.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/CHANGELOG.md +23 -1
- package/README.md +28 -2
- package/bin/cairn +13 -1
- package/docs/context-packs.md +94 -0
- package/docs/harness-compatibility.md +7 -1
- package/docs/learning/CURRICULUM-MAP.md +9 -4
- package/docs/learning/FEATURE-GUIDE.md +7 -2
- package/docs/learning/README.md +6 -2
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- package/docs/learning/tracks/operator.md +4 -2
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/operating.md +38 -0
- package/docs/privacy-and-data-flow.md +17 -0
- package/docs/storage.md +17 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/context-pack-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +830 -0
- package/mcp-memory-server/dist/eval-report.js +1 -0
- package/mcp-memory-server/dist/eval-runner.js +2 -0
- package/mcp-memory-server/dist/eval-schema.js +1 -0
- package/mcp-memory-server/dist/index.js +68 -17
- package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
- package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
- package/mcp-memory-server/dist/mcp-tool-profile.js +162 -0
- package/mcp-memory-server/package.json +3 -1
- package/package.json +1 -1
- package/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +1 -0
- package/scripts/completion.sh +24 -5
- package/scripts/doctor.sh +39 -0
- package/scripts/test-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +5 -0
- package/scripts/test-learning-docs.sh +4 -3
- package/scripts/test-package-install.sh +17 -0
- package/scripts/test-uninstall.sh +21 -0
- package/scripts/uninstall.sh +26 -1
- package/templates/env.example.template +11 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# L21 - Immutable context packs and skill approval
|
|
2
|
+
|
|
3
|
+
**Status:** Ready
|
|
4
|
+
**Tested with:** Cairnkeep 2.9.0 and Node.js 22 or newer
|
|
5
|
+
**Time:** 40 minutes
|
|
6
|
+
|
|
7
|
+
## Outcome
|
|
8
|
+
|
|
9
|
+
Build a local offline context pack, verify its immutable identity, pin it to a
|
|
10
|
+
disposable project, and explain why document enablement and skill approval are
|
|
11
|
+
different trust decisions.
|
|
12
|
+
|
|
13
|
+
## Exercise
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
lab=$(mktemp -d)
|
|
17
|
+
mkdir -p "$lab/pack" "$lab/project"
|
|
18
|
+
printf '# Local guide\n\nSynthetic offline guidance.\n' >"$lab/pack/guide.md"
|
|
19
|
+
export CAIRN_PACK_BASE_DIR="$lab/store"
|
|
20
|
+
cairn pack init "$lab/pack" --id course-guide --version 1.0.0 \
|
|
21
|
+
--title "Course guide" --description "Synthetic local reference" \
|
|
22
|
+
--license Apache-2.0
|
|
23
|
+
cairn pack lock "$lab/pack"
|
|
24
|
+
cairn pack validate "$lab/pack"
|
|
25
|
+
cairn pack install "$lab/pack" --json >"$lab/install.json"
|
|
26
|
+
digest=$(node -e 'process.stdout.write(require(process.argv[1]).digest)' "$lab/install.json")
|
|
27
|
+
cairn pack enable "$digest" --project "$lab/project"
|
|
28
|
+
cairn pack show "$digest" --json
|
|
29
|
+
cairn pack skills --project "$lab/project" --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The local lifecycle makes no network request. The pack digest covers exact
|
|
33
|
+
manifest and content bytes; `version` is a label, not identity. Objects remain
|
|
34
|
+
under `$CAIRN_PACK_BASE_DIR` after disablement and ordinary uninstall.
|
|
35
|
+
|
|
36
|
+
Add a file with `kind: skill`, refresh it with `cairn pack lock`, install the new
|
|
37
|
+
digest, and enable it. `cairn pack skills` shows the skill but the MCP list/read
|
|
38
|
+
surface does not expose it until:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cairn pack approve-skill NEW-PACK-DIGEST path/to/SKILL.md \
|
|
42
|
+
--confirm FILE-DIGEST --project "$lab/project"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Approval binds the project, pack, path, and file bytes. A later pack update
|
|
46
|
+
invalidates it. Approval makes instructions readable only; it never executes or
|
|
47
|
+
copies them into a harness.
|
|
48
|
+
|
|
49
|
+
## Common failures
|
|
50
|
+
|
|
51
|
+
- Adding a file after `init` requires declaring it in the manifest before
|
|
52
|
+
`lock`; undeclared content is rejected.
|
|
53
|
+
- Git installation without `--ref`, a stale update confirmation, and removal of
|
|
54
|
+
an enabled digest all fail closed.
|
|
55
|
+
- Enabling the pack does not expose a skill until its current file digest is
|
|
56
|
+
approved.
|
|
57
|
+
|
|
58
|
+
## Privacy and trust boundary
|
|
59
|
+
|
|
60
|
+
Local pack operations are offline. Configured semantic search can send enabled
|
|
61
|
+
chunks and a query to the explicit embedding endpoint; failure falls back to
|
|
62
|
+
substring search. A digest detects changed bytes but does not authenticate the
|
|
63
|
+
publisher. Approved instructions remain untrusted readable context, not code.
|
|
64
|
+
|
|
65
|
+
## Recovery and acceptance
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cairn pack disable course-guide --project "$lab/project"
|
|
69
|
+
cairn pack remove "$digest"
|
|
70
|
+
unset CAIRN_PACK_BASE_DIR
|
|
71
|
+
rm -rf "$lab"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Traversal, symlinks, undeclared files, invalid UTF-8, and digest tampering fail
|
|
75
|
+
validation.
|
|
76
|
+
- `update --check` never switches a project; apply requires the inspected digest.
|
|
77
|
+
- `CAIRN_CONTEXT_PACKS=1` enables local read-only tools only after restart.
|
|
78
|
+
- HTTP additionally requires `CAIRN_CONTEXT_PACK_HTTP=1` and existing auth.
|
|
79
|
+
- A digest proves integrity, not publisher authenticity.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Audience:** advanced users and platform maintainers
|
|
4
4
|
**Prerequisite:** Practitioner track; L13 before operating captured evidence
|
|
5
|
-
**Total time:** about
|
|
5
|
+
**Total time:** about 5 hours
|
|
6
6
|
|
|
7
7
|
1. [L09 - Domain knowledge with RAG](../lessons/L09-domain-knowledge.md)
|
|
8
8
|
2. [L10 - Faster context exploration](../lessons/L10-context-exploration.md)
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
4. [L12 - Managed overlays](../lessons/L12-managed-overlays.md)
|
|
11
11
|
5. [L15 - Capability governance](../lessons/L15-capability-governance.md)
|
|
12
12
|
6. [L16 - Evaluation and ablation](../lessons/L16-evaluation.md)
|
|
13
|
+
7. [L20 - Least-authority MCP tool profiles](../lessons/L20-mcp-tool-profiles.md)
|
|
14
|
+
8. [L21 - Immutable context packs and skill approval](../lessons/L21-context-packs.md)
|
|
13
15
|
|
|
14
16
|
All modules are optional. The track is complete when the operator can add and
|
|
15
17
|
remove each integration without breaking the standalone local workflow and can
|
|
16
18
|
explain its storage, network, credential, and rollback boundaries.
|
|
17
19
|
|
|
18
20
|
Hands-on spine: `course-04-operation`, `course-06-governance`, and
|
|
19
|
-
`course-07-evaluation` in the public
|
|
21
|
+
`course-07-evaluation`, and `course-10-trust-context` in the public
|
|
20
22
|
[course labs](https://github.com/cairnkeep/cairnkeep-course-labs).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# MCP tool annotations and least-authority profiles
|
|
2
|
+
|
|
3
|
+
Cairnkeep publishes a complete annotation contract for every MCP tool. Each
|
|
4
|
+
tool has a stable title and explicit `readOnlyHint`, `destructiveHint`,
|
|
5
|
+
`idempotentHint`, and `openWorldHint` values. Local observations are closed
|
|
6
|
+
world. Tools that query configured services or delegated processes are open
|
|
7
|
+
world. Any mutation that may replace or remove state is destructive; immutable
|
|
8
|
+
artifact creation is additive.
|
|
9
|
+
|
|
10
|
+
Use the profile CLI to reduce the tools visible to an MCP client:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cairn mcp-tools list
|
|
14
|
+
cairn mcp-tools status --json
|
|
15
|
+
cairn mcp-tools set read-only
|
|
16
|
+
cairn mcp-tools set custom --tool memory_read --tool memory_search
|
|
17
|
+
cairn mcp-tools reset
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Project configuration is strict versioned JSON in `.ai/mcp-tools.json`, created
|
|
21
|
+
only by `set` and written with mode `0600`. Do not hand-edit it. A process may
|
|
22
|
+
override the project with `CAIRN_MCP_TOOL_PROFILE=full|read-only|custom`; custom
|
|
23
|
+
process profiles also require a comma-separated `CAIRN_MCP_ALLOWED_TOOLS`.
|
|
24
|
+
Precedence is process environment, project configuration, then `full`.
|
|
25
|
+
|
|
26
|
+
`read-only` is derived from the central annotation catalog. `custom` is an
|
|
27
|
+
exact allowlist. Unknown names fail server startup and `cairn doctor`. Profiles
|
|
28
|
+
only remove authority: they cannot activate typed nodes, artifacts, context
|
|
29
|
+
packs, HTTP exposure, or a disabled capability. Effective discovery is:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
available feature gates ∩ capability contract ∩ MCP tool profile
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`status` includes a canonical profile digest. Evaluation reports record that
|
|
36
|
+
same digest in their evidence provenance without changing the independent
|
|
37
|
+
capability-configuration digest. Restart the memory server after changing an
|
|
38
|
+
MCP profile.
|
|
39
|
+
|
|
40
|
+
The default `full` profile preserves the same tool names, schemas, responses,
|
|
41
|
+
feature gates, and ordering as an unconfigured server. The annotations help
|
|
42
|
+
conservative clients distinguish observations from actions; they do not grant
|
|
43
|
+
permission or replace the client's approval policy.
|
package/docs/operating.md
CHANGED
|
@@ -1099,3 +1099,41 @@ truthy it launches exactly one separate `cairn notes distill --all-projects`
|
|
|
1099
1099
|
process after the wiki scan and reports its bounded status. With the flag unset
|
|
1100
1100
|
it performs no note work. Note failure does not replace wiki findings or change
|
|
1101
1101
|
their actionable exit status; no credentials are embedded in rendered units.
|
|
1102
|
+
|
|
1103
|
+
## Least-authority MCP profiles
|
|
1104
|
+
|
|
1105
|
+
Inspect the annotation-derived catalog and then choose the smallest profile the
|
|
1106
|
+
client needs:
|
|
1107
|
+
|
|
1108
|
+
```bash
|
|
1109
|
+
cairn mcp-tools list
|
|
1110
|
+
cairn mcp-tools set read-only --project .
|
|
1111
|
+
cairn mcp-tools set custom --tool memory_read --tool memory_search --project .
|
|
1112
|
+
cairn mcp-tools status --project . --json
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Restart the memory server after `set` or `reset`. Environment overrides win over
|
|
1116
|
+
the mode-`0600` project file. A profile can only remove tools; feature and
|
|
1117
|
+
capability gates still apply. Invalid names make both startup and doctor fail.
|
|
1118
|
+
See [MCP tool profiles](mcp-tool-profiles.md).
|
|
1119
|
+
|
|
1120
|
+
## Context packs (opt-in)
|
|
1121
|
+
|
|
1122
|
+
Build and validate a local pack before installing it, then enable the immutable
|
|
1123
|
+
digest for exactly one project:
|
|
1124
|
+
|
|
1125
|
+
```bash
|
|
1126
|
+
cairn pack init ./reference --id reference --version 1.0.0 \
|
|
1127
|
+
--title Reference --description "Reviewed project context" --license Apache-2.0
|
|
1128
|
+
cairn pack lock ./reference
|
|
1129
|
+
cairn pack validate ./reference
|
|
1130
|
+
cairn pack install ./reference
|
|
1131
|
+
cairn pack enable PACK-DIGEST --project .
|
|
1132
|
+
CAIRN_CONTEXT_PACKS=1 cairn memory-server
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
Git sources additionally require `--ref`. Updates are a manual `--check`, then
|
|
1136
|
+
an `--apply --confirm CANDIDATE-DIGEST`. Use `cairn pack skills` and exact-digest
|
|
1137
|
+
approval before a bundled skill can be read through MCP. HTTP needs the separate
|
|
1138
|
+
`CAIRN_CONTEXT_PACK_HTTP=1` flag and all existing authentication/Host/CORS
|
|
1139
|
+
controls. See [Immutable context packs](context-packs.md).
|
|
@@ -16,6 +16,9 @@ request unless the corresponding endpoint and credential are configured.
|
|
|
16
16
|
| `domain_knowledge_sync` | Files selected by the sync configuration | `ANYTHINGLLM_BASE_URL` |
|
|
17
17
|
| `route_check` | A health request, with no memory or prompt content | `CAIRN_ROUTE_ENDPOINT` |
|
|
18
18
|
| `context_explore` | Repository path and query are passed to the configured local executable | `CAIRN_EXPLORE_BINARY`; any further data flow is controlled by that tool |
|
|
19
|
+
| Local context-pack install/list/read/substring search | None | Verified immutable files and project pointers under `CAIRN_PACK_BASE_DIR` |
|
|
20
|
+
| Embedding-ranked context-pack search | Enabled file chunks not already cached, plus the query | The explicitly configured embedding endpoint; failure falls back locally |
|
|
21
|
+
| Authenticated HTTP context-pack retrieval | MCP requests and enabled document/approved-skill content | The explicit Cairnkeep HTTP server; requires separate pack HTTP consent |
|
|
19
22
|
| Optional Graphify workflow | The repository path, exact query/symbol names, root `graphify-out/` work directory, and local `.planning/graphs/` publication are passed to or produced by the operator-installed `graphify` executable | Local Graphify subprocess with a minimal environment and no provider credentials; managed build uses code-only `update`, never semantic document extraction |
|
|
20
23
|
| Remote HTTP memory | MCP requests and responses, including memory content | The explicitly registered Cairnkeep HTTP server |
|
|
21
24
|
| Opt-in trajectory capture | None | Local `<project>/.agentfs/trajectory.db` only; no model or HTTP path exists |
|
|
@@ -512,3 +515,17 @@ provide tenant isolation or per-scope authorization.
|
|
|
512
515
|
Before sharing diagnostics, remove credentials, private endpoints, database
|
|
513
516
|
files, memory values, local paths, and project names. Report vulnerabilities
|
|
514
517
|
through the private channel in [SECURITY.md](../SECURITY.md).
|
|
518
|
+
|
|
519
|
+
## Context-pack trust boundary
|
|
520
|
+
|
|
521
|
+
Pack validation proves that installed bytes match the manifest and immutable
|
|
522
|
+
digest. It does not authenticate a publisher. Review the source, commit, license,
|
|
523
|
+
and content before enabling a digest; signatures are not yet supported. Local
|
|
524
|
+
directory installation, listing, reading, and substring search make no network
|
|
525
|
+
request. Git installation/update contacts only the operator-supplied source and
|
|
526
|
+
is never scheduled in the background.
|
|
527
|
+
|
|
528
|
+
All enabled documents carry pack and file provenance in MCP results. Skill files
|
|
529
|
+
are excluded until the current project approves the exact pack digest, path, and
|
|
530
|
+
file digest. Updating the pack invalidates that approval. Approval only makes a
|
|
531
|
+
skill readable; Cairnkeep never copies it into a harness or executes it.
|
package/docs/storage.md
CHANGED
|
@@ -21,6 +21,9 @@ registration, memory remains on that computer.
|
|
|
21
21
|
| Opt-in session trajectories and capability callback state | `<harness project root>/.agentfs/trajectory.db` |
|
|
22
22
|
| Opt-in local artifacts and compaction revisions | `<project root>/.agentfs/artifacts.db` |
|
|
23
23
|
| Opt-in evaluation reports and note snapshots | `<project root>/.agentfs/eval/experiments/<experiment-id>/` |
|
|
24
|
+
| Immutable context-pack objects and source records | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/objects/` and `sources/` |
|
|
25
|
+
| Digest-pinned context-pack project pointers and approvals | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/projects/` |
|
|
26
|
+
| Optional context-pack embedding cache | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/cache/` (outside immutable objects) |
|
|
24
27
|
| Opt-in derived hindsight notes | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/notes/` |
|
|
25
28
|
| Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
|
|
26
29
|
|
|
@@ -493,3 +496,17 @@ bearer_token_env_var = "CAIRN_MEMORY_HTTP_TOKEN"
|
|
|
493
496
|
"X-Cairn-Scopes" = "identity,personal,project"
|
|
494
497
|
"X-Cairn-AnythingLLM-Workspaces" = "engineering-patterns"
|
|
495
498
|
```
|
|
499
|
+
|
|
500
|
+
## Context-pack storage
|
|
501
|
+
|
|
502
|
+
Context-pack objects are content-addressed, immutable directories. Installation
|
|
503
|
+
copies verified content through a temporary directory and atomically publishes
|
|
504
|
+
the digest; a failed or concurrent install never changes project enablement.
|
|
505
|
+
Project pointers are separate atomic mode-`0600` files keyed by canonical local
|
|
506
|
+
project identity or a validated remote project ID. Updates retain old objects
|
|
507
|
+
and switch only the named project after exact-digest confirmation.
|
|
508
|
+
|
|
509
|
+
`cairn doctor` verifies objects, pointers, and interrupted-install remnants.
|
|
510
|
+
It never chooses a pack version or grants a skill approval. `cairn pack remove`
|
|
511
|
+
refuses any referenced digest. Ordinary uninstall retains this directory even
|
|
512
|
+
when memory is removed; `--purge-packs` is the separate backup-first consent.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.9.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { applyContextPackUpdate, approvePackSkill, contextPackBaseDirectory, disableContextPack, doctorContextPacks, enableContextPack, initializeContextPack, inspectContextPackUpdate, installContextPack, listInstalledContextPacks, listPackSkills, lockContextPack, readProjectPointer, removeContextPack, resolveInstalledPack, revokePackSkill, validateContextPack, } from "./context-pack.js";
|
|
5
|
+
function usage() {
|
|
6
|
+
process.stderr.write(`Usage:
|
|
7
|
+
cairn pack init [DIRECTORY] [--id ID] [--version VERSION] [--title TITLE] [--description TEXT] [--license LICENSE]
|
|
8
|
+
cairn pack lock|validate [DIRECTORY]
|
|
9
|
+
cairn pack install SOURCE [--ref REF]
|
|
10
|
+
cairn pack list [--json]
|
|
11
|
+
cairn pack show SELECTOR [--json]
|
|
12
|
+
cairn pack remove SELECTOR
|
|
13
|
+
cairn pack enable|disable SELECTOR [--project PATH | --project-id ID]
|
|
14
|
+
cairn pack update SELECTOR --check [--project PATH | --project-id ID]
|
|
15
|
+
cairn pack update SELECTOR --apply --confirm DIGEST [--project PATH | --project-id ID]
|
|
16
|
+
cairn pack skills [--project PATH | --project-id ID] [--json]
|
|
17
|
+
cairn pack approve-skill SELECTOR PATH --confirm FILE_DIGEST [--project PATH | --project-id ID]
|
|
18
|
+
cairn pack revoke-skill SELECTOR PATH [--project PATH | --project-id ID]
|
|
19
|
+
`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
function take(args, name) {
|
|
23
|
+
const index = args.indexOf(name);
|
|
24
|
+
if (index < 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const value = args[index + 1];
|
|
27
|
+
if (!value || value.startsWith("--"))
|
|
28
|
+
usage();
|
|
29
|
+
args.splice(index, 2);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function flag(args, name) {
|
|
33
|
+
const index = args.indexOf(name);
|
|
34
|
+
if (index < 0)
|
|
35
|
+
return false;
|
|
36
|
+
args.splice(index, 1);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function projectOptions(args) {
|
|
40
|
+
const projectRoot = take(args, "--project");
|
|
41
|
+
const projectId = take(args, "--project-id");
|
|
42
|
+
if (projectRoot && projectId)
|
|
43
|
+
usage();
|
|
44
|
+
return projectId ? { projectId } : { projectRoot: projectRoot ?? process.cwd() };
|
|
45
|
+
}
|
|
46
|
+
function output(value, json, human) {
|
|
47
|
+
process.stdout.write(`${json ? JSON.stringify(value) : human ?? JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
const command = args.shift();
|
|
52
|
+
const json = flag(args, "--json");
|
|
53
|
+
if (command === "init") {
|
|
54
|
+
const directory = args[0] && !args[0].startsWith("--") ? args.shift() : process.cwd();
|
|
55
|
+
const manifest = await initializeContextPack(directory, {
|
|
56
|
+
id: take(args, "--id"), version: take(args, "--version"), title: take(args, "--title"),
|
|
57
|
+
description: take(args, "--description"), license: take(args, "--license"),
|
|
58
|
+
});
|
|
59
|
+
if (args.length)
|
|
60
|
+
usage();
|
|
61
|
+
output(manifest, json, `Initialized ${resolve(directory)}/context-pack.json`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command === "lock" || command === "validate") {
|
|
65
|
+
const directory = args.shift() ?? process.cwd();
|
|
66
|
+
if (args.length)
|
|
67
|
+
usage();
|
|
68
|
+
const result = command === "lock" ? await lockContextPack(directory) : await validateContextPack(directory);
|
|
69
|
+
output(result, json, command === "lock" ? `Locked ${resolve(directory)}/context-pack.json` : `Valid context pack: ${result.digest}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === "install") {
|
|
73
|
+
const source = args.shift();
|
|
74
|
+
if (!source)
|
|
75
|
+
usage();
|
|
76
|
+
const ref = take(args, "--ref");
|
|
77
|
+
if (args.length)
|
|
78
|
+
usage();
|
|
79
|
+
const result = await installContextPack(source, { ref });
|
|
80
|
+
output({ id: result.pack.manifest.id, version: result.pack.manifest.version, digest: result.pack.digest, source: result.source, existing: result.existing }, json, `${result.existing ? "Already installed" : "Installed"} ${result.pack.manifest.id}@${result.pack.manifest.version} ${result.pack.digest}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (command === "list") {
|
|
84
|
+
if (args.length)
|
|
85
|
+
usage();
|
|
86
|
+
const packs = await listInstalledContextPacks();
|
|
87
|
+
output({ schema_version: 1, packs }, json, packs.length ? packs.map((pack) => `${pack.id}@${pack.version}\t${pack.digest}`).join("\n") : "No context packs installed.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === "show") {
|
|
91
|
+
const selector = args.shift();
|
|
92
|
+
if (!selector || args.length)
|
|
93
|
+
usage();
|
|
94
|
+
const pack = await resolveInstalledPack(selector);
|
|
95
|
+
output({ ...pack.manifest, digest: pack.digest, total_bytes: pack.total_bytes }, json);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === "remove") {
|
|
99
|
+
const selector = args.shift();
|
|
100
|
+
if (!selector || args.length)
|
|
101
|
+
usage();
|
|
102
|
+
const digest = await removeContextPack(selector);
|
|
103
|
+
output({ removed: true, digest }, json, `Removed context pack ${digest}.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "enable" || command === "disable") {
|
|
107
|
+
const selector = args.shift();
|
|
108
|
+
if (!selector)
|
|
109
|
+
usage();
|
|
110
|
+
const project = projectOptions(args);
|
|
111
|
+
if (args.length)
|
|
112
|
+
usage();
|
|
113
|
+
const pointer = command === "enable" ? await enableContextPack(selector, project) : await disableContextPack(selector, project);
|
|
114
|
+
output(pointer, json, `${command === "enable" ? "Enabled" : "Disabled"} context pack ${selector}.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (command === "update") {
|
|
118
|
+
const selector = args.shift();
|
|
119
|
+
if (!selector)
|
|
120
|
+
usage();
|
|
121
|
+
const check = flag(args, "--check");
|
|
122
|
+
const apply = flag(args, "--apply");
|
|
123
|
+
const confirm = take(args, "--confirm");
|
|
124
|
+
const project = projectOptions(args);
|
|
125
|
+
if (args.length || check === apply || (apply && !confirm) || (check && confirm))
|
|
126
|
+
usage();
|
|
127
|
+
const result = check ? await inspectContextPackUpdate(selector, project) : await applyContextPackUpdate(selector, confirm, project);
|
|
128
|
+
output(result, json, check ? `${result.changed ? "Update available" : "Up to date"}: ${result.candidate_digest}` : `Applied context pack update ${result.candidate_digest}.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (command === "skills") {
|
|
132
|
+
const project = projectOptions(args);
|
|
133
|
+
if (args.length)
|
|
134
|
+
usage();
|
|
135
|
+
const skills = await listPackSkills(project);
|
|
136
|
+
output({ schema_version: 1, skills }, json, skills.length ? skills.map((row) => `${row.approved ? "approved" : "unapproved"}\t${row.pack_id}\t${row.path}\t${row.file_digest}`).join("\n") : "No context pack skills.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (command === "approve-skill" || command === "revoke-skill") {
|
|
140
|
+
const selector = args.shift();
|
|
141
|
+
const path = args.shift();
|
|
142
|
+
if (!selector || !path)
|
|
143
|
+
usage();
|
|
144
|
+
const confirm = take(args, "--confirm");
|
|
145
|
+
const project = projectOptions(args);
|
|
146
|
+
if (args.length || (command === "approve-skill" && !confirm) || (command === "revoke-skill" && confirm))
|
|
147
|
+
usage();
|
|
148
|
+
const pointer = command === "approve-skill"
|
|
149
|
+
? await approvePackSkill(selector, path, confirm, project)
|
|
150
|
+
: await revokePackSkill(selector, path, project);
|
|
151
|
+
output(pointer, json, `${command === "approve-skill" ? "Approved" : "Revoked"} ${path}.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (command === "doctor") {
|
|
155
|
+
if (args.length)
|
|
156
|
+
usage();
|
|
157
|
+
const result = await doctorContextPacks();
|
|
158
|
+
output(result, json, result.ok ? `Context packs healthy (${result.objects} objects, ${result.projects} projects).` : `Context pack problems: ${[...result.issues, ...result.temporary_remnants].join("; ")}`);
|
|
159
|
+
if (!result.ok)
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (command === "purge") {
|
|
164
|
+
if (!flag(args, "--yes") || args.length)
|
|
165
|
+
usage();
|
|
166
|
+
const base = contextPackBaseDirectory();
|
|
167
|
+
if (existsSync(base))
|
|
168
|
+
await rm(base, { recursive: true, force: true });
|
|
169
|
+
output({ purged: true, path: base }, json, `Purged context pack data at ${base}.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === "status") {
|
|
173
|
+
const project = projectOptions(args);
|
|
174
|
+
if (args.length)
|
|
175
|
+
usage();
|
|
176
|
+
const pointer = readProjectPointer(project);
|
|
177
|
+
output(pointer, json);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
usage();
|
|
181
|
+
}
|
|
182
|
+
main().catch((error) => {
|
|
183
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|