@cairnkeep/cli 2.25.0 → 2.26.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 +41 -0
- package/docs/README.md +2 -0
- package/docs/harness-compatibility.md +5 -2
- package/docs/learning/CURRICULUM-MAP.md +3 -3
- package/docs/learning/FEATURE-GUIDE.md +7 -1
- 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 +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +1 -1
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +5 -1
- package/docs/learning/lessons/L29-team-continuity.md +33 -1
- package/docs/learning/tracks/operator.md +2 -0
- package/docs/learning/video-scripts/V28-security-assurance.md +4 -0
- package/docs/learning/video-scripts/V29-team-continuity.md +19 -1
- package/docs/operating.md +6 -0
- package/docs/privacy-and-data-flow.md +21 -1
- package/docs/provider-data-flow-review.md +48 -0
- package/docs/security-assurance.md +11 -0
- package/docs/storage.md +14 -0
- package/docs/team-onboarding.md +226 -0
- package/docs/team-pilot.md +2 -0
- package/docs/team.md +11 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/capability-config.js +12 -1
- package/mcp-memory-server/dist/team-cli.js +15 -5
- package/mcp-memory-server/dist/team-client.js +71 -0
- package/mcp-memory-server/dist/team-mcp.js +8 -1
- package/mcp-memory-server/dist/team-store.js +8 -2
- package/mcp-memory-server/package.json +2 -2
- package/package.json +1 -1
- package/scripts/cairn-cli.mjs +6 -1
- package/scripts/completion.sh +11 -3
- package/scripts/lib/team-retention.mjs +1 -1
- package/scripts/team-connect.mjs +366 -0
- package/scripts/test-ci-sharding.sh +1 -1
- package/scripts/test-codeql-cohort.mjs +35 -0
- package/scripts/test-completion.sh +4 -1
- package/scripts/test-package-install.sh +1 -0
- package/scripts/test-phase18-capability-lifecycle.sh +2 -2
- package/scripts/test-security-baseline.sh +1 -0
- package/scripts/test-team-docs.sh +11 -2
- package/scripts/test-team-installed.mjs +51 -0
- package/scripts/test-team-pilot.mjs +43 -1
- package/scripts/verify-security-baseline.mjs +20 -0
- package/scripts/windows-platform.mjs +1 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Team preview: a small, explicit handoff
|
|
2
|
+
|
|
3
|
+
For Cairnkeep 2.25.0 or newer. This is **not production admission**;
|
|
4
|
+
no real-data deployment is authorized by following this guide.
|
|
5
|
+
|
|
6
|
+
## Choose the right workflow
|
|
7
|
+
|
|
8
|
+
For your own context across sessions, folders and harnesses, keep ordinary
|
|
9
|
+
`cairn-memory`. You already have that continuity; you do not need a team store.
|
|
10
|
+
Use the team preview when another person should retrieve **selected, reviewed
|
|
11
|
+
project decisions**. Personal memory stays separate. An overlay upgrade does
|
|
12
|
+
not enroll anyone, start a team service or enable sharing.
|
|
13
|
+
|
|
14
|
+
| Person | Responsibility | Keep separate |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Operator | Private service, memberships, credentials and recovery | Deployment consent |
|
|
17
|
+
| Reader / coding client | Own workload token and explicit project | Human-review token |
|
|
18
|
+
| Contributor | Select/redact and propose exact text with consent | Publication authority |
|
|
19
|
+
| Human reviewer | Inspect actual proposal, approve/reject its digest | Agent workspace |
|
|
20
|
+
|
|
21
|
+
## Operator handoff
|
|
22
|
+
|
|
23
|
+
For an explicitly approved **synthetic** rehearsal, follow the
|
|
24
|
+
[isolated pilot setup](team-pilot.md#1-prepare-a-private-lab), then start its
|
|
25
|
+
foreground loopback service. The tested recipe refuses an existing lab and
|
|
26
|
+
preserves personal MCP settings. Do not use production paths.
|
|
27
|
+
|
|
28
|
+
Give each participant a connection card: verified CLI version/path; loopback
|
|
29
|
+
or authenticated-tunnel URL; organization/project IDs; their individual private
|
|
30
|
+
workload-token file and expiry; disposable workspace; revocation contact.
|
|
31
|
+
For this lab the IDs are `trial-org` and `trial-project`.
|
|
32
|
+
|
|
33
|
+
Transfer tokens privately, never through chat, Git, arguments or recordings.
|
|
34
|
+
Keep the reviewer token outside agent workspaces. Same-OS-owner processes are
|
|
35
|
+
not mutually isolated; use separate accounts/machines when that is required.
|
|
36
|
+
|
|
37
|
+
## Reader quick-check
|
|
38
|
+
|
|
39
|
+
In a separate Bash terminal on Linux/macOS, set the operator's paths and URL:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
TEAM_CAIRN=/absolute/path/to/verified-checkout/bin/cairn
|
|
43
|
+
TEAM_PILOT_DIR=/absolute/private/new-team-pilot
|
|
44
|
+
TEAM_PILOT_URL=http://127.0.0.1:17955
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This block makes one read request. It does not create storage, issue credentials,
|
|
48
|
+
start a service or edit a harness. Expect `[]` before any human publication.
|
|
49
|
+
Stop on connection/authorization failure; never fall back to personal memory.
|
|
50
|
+
|
|
51
|
+
<!-- team-reader-check:start -->
|
|
52
|
+
```sh
|
|
53
|
+
set -e
|
|
54
|
+
: "${TEAM_CAIRN:?Set the verified CLI path}"
|
|
55
|
+
: "${TEAM_PILOT_DIR:?Set the private lab directory}"
|
|
56
|
+
: "${TEAM_PILOT_URL:?Set the loopback or authenticated-tunnel URL}"
|
|
57
|
+
team_pilot_reader() {
|
|
58
|
+
env -i PATH="$PATH" CAIRN_TEAM=1 \
|
|
59
|
+
CAIRN_TEAM_URL="$TEAM_PILOT_URL" CAIRN_TEAM_ORGANIZATION=trial-org \
|
|
60
|
+
CAIRN_TEAM_TOKEN_FILE="$TEAM_PILOT_DIR/reader.token" \
|
|
61
|
+
"$TEAM_CAIRN" team "$@" --project trial-project
|
|
62
|
+
}
|
|
63
|
+
team_pilot_reader list
|
|
64
|
+
```
|
|
65
|
+
<!-- team-reader-check:end -->
|
|
66
|
+
|
|
67
|
+
On native Windows, use equivalent explicit environment settings and
|
|
68
|
+
`cairn team list --project trial-project` from [the reference](team.md).
|
|
69
|
+
|
|
70
|
+
Add **only an additional**, disposable `cairn-team-memory` stdio entry:
|
|
71
|
+
the verified command, arguments `team mcp --project-root WORKSPACE`, and the
|
|
72
|
+
same four `CAIRN_TEAM*` settings above. For a reader, also set
|
|
73
|
+
`CAIRN_TEAM_MCP_TOOL_PROFILE=read-only`. Merge into the isolated harness profile;
|
|
74
|
+
never replace `cairn-memory` or its entire configuration. See
|
|
75
|
+
[connection semantics](team.md#harness-connection).
|
|
76
|
+
Each tool call names `trial-project`; cwd and display names grant no access.
|
|
77
|
+
The MCP profile restricts tools; server membership independently restricts data.
|
|
78
|
+
|
|
79
|
+
<a id="guided-connection-next-release--matching-source-checkout"></a>
|
|
80
|
+
|
|
81
|
+
## Guided connection (2.26 or newer)
|
|
82
|
+
|
|
83
|
+
The original preview and reader card above work on 2.25.0. The guided commands
|
|
84
|
+
below require both the client **and team API** at 2.26.0 or newer. They do not
|
|
85
|
+
change ordinary `cairn setup`, personal memory or the preview's
|
|
86
|
+
production-admission boundary.
|
|
87
|
+
|
|
88
|
+
After the operator supplies a **workload reader** credential, opt in explicitly:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
CAIRN_TEAM=1 cairn team connect --project-root /absolute/project
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The wizard asks for URL, organization/project IDs, a private credential-file
|
|
95
|
+
path and harness checkboxes. Its default tool profile is read-only. It displays
|
|
96
|
+
the planned files and asks before applying. Cancellation returns to the shell.
|
|
97
|
+
For automation, supply every choice; without `--apply` this only plans:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
CAIRN_TEAM=1 cairn team connect --project-root /absolute/project \
|
|
101
|
+
--url http://127.0.0.1:17955 --organization trial-org --project trial-project \
|
|
102
|
+
--token-file /absolute/private/reader.token --harness claude,opencode
|
|
103
|
+
# Repeat the same command with --apply --yes to connect.
|
|
104
|
+
CAIRN_TEAM=1 cairn team doctor --client --project-root /absolute/project
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Never pass a token value. Keep the credential file outside this project's
|
|
108
|
+
source tree and owner-only (POSIX 0600; native Windows private ACL). URLs must
|
|
109
|
+
be loopback HTTP or HTTPS, without credentials, query, fragment or path prefix.
|
|
110
|
+
Use the operator's encrypted authenticated tunnel, not a public unauthenticated
|
|
111
|
+
listener. The command authenticates reader access before changing a harness.
|
|
112
|
+
It refuses human-class credentials: the operator must issue a workload token
|
|
113
|
+
for the agent, keeping human review credentials in the separate human terminal.
|
|
114
|
+
|
|
115
|
+
| Harness choice | Project-local file | Additional entry |
|
|
116
|
+
|----------------|--------------------|------------------|
|
|
117
|
+
| `claude` | `.mcp.json` | `mcpServers.cairn-team-memory` |
|
|
118
|
+
| `opencode` | `opencode.json` | `mcp.cairn-team-memory` |
|
|
119
|
+
| `qwen` | `.qwen/settings.json` | `mcpServers.cairn-team-memory` |
|
|
120
|
+
| `codex` | `.codex/config.toml` | marked `mcp_servers.cairn-team-memory` block |
|
|
121
|
+
|
|
122
|
+
No global harness file is written. Existing unrelated entries are preserved;
|
|
123
|
+
an existing team entry, duplicate JSON keys, JSONC or unsupported inline TOML
|
|
124
|
+
causes refusal rather than replacement. OpenCode `opencode.jsonc` is not edited.
|
|
125
|
+
Pi/OMP/Kimi use the manual bridge recipe; this wizard does not claim automated
|
|
126
|
+
configuration for them. Harness-native trust/enablement prompts still apply.
|
|
127
|
+
Adapter regression uses real SDK stdio calls, not live model/harness UI sessions.
|
|
128
|
+
|
|
129
|
+
The generated entry invokes the verified Node/CLI paths with a private
|
|
130
|
+
`--connection` file. It pins the server project even if the harness starts in
|
|
131
|
+
another folder; each tool call still names that ID. It does not grant server
|
|
132
|
+
membership. Contributors can explicitly select `--access full` to expose
|
|
133
|
+
proposal creation, still subject to existing profile/capability restrictions
|
|
134
|
+
and server authorization. Human review/admin tools never become available.
|
|
135
|
+
Changing connection settings requires disconnect/reconnect; credentials may be
|
|
136
|
+
rotated in place without a harness restart because the bridge rereads the file.
|
|
137
|
+
|
|
138
|
+
### Client diagnosis and recovery
|
|
139
|
+
|
|
140
|
+
Client doctor verifies the active managed entries and private credential file,
|
|
141
|
+
then makes one authenticated `connection-check` request. It reports the current
|
|
142
|
+
subject, credential ID/class/expiry, project and roles, **not** memory text,
|
|
143
|
+
proposals, token values or environment snapshots. The service appends its normal
|
|
144
|
+
value-free audit event. It opens no local administrator store and cannot issue
|
|
145
|
+
credentials, grant membership or repair permissions. Wrong org/project, missing
|
|
146
|
+
reader membership and expired/revoked credentials require the operator; failed
|
|
147
|
+
team access never falls back to personal memory. Check the tunnel/TLS and API
|
|
148
|
+
version on connection failure. Store doctor remains `team doctor --data DIR`.
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
CAIRN_TEAM=1 cairn team disconnect --project-root /absolute/project
|
|
152
|
+
# Inspect the no-write plan, then:
|
|
153
|
+
CAIRN_TEAM=1 cairn team disconnect --project-root /absolute/project --apply --yes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Disconnect removes only unchanged owned entries. It restores exact original
|
|
157
|
+
bytes when nothing else changed; otherwise it preserves unrelated edits and
|
|
158
|
+
refuses an altered team entry. It keeps the token and private backups. Restart
|
|
159
|
+
existing harness sessions; disconnect is **not revocation**. The operator must
|
|
160
|
+
revoke a credential/remove membership for immediate denial in running sessions.
|
|
161
|
+
|
|
162
|
+
Connection records and original configuration snapshots are private under
|
|
163
|
+
`~/.cairnkeep/team-clients/<canonical-project-path-sha256>/`. Backups may include
|
|
164
|
+
secrets already present in your original MCP config: never commit or publish
|
|
165
|
+
them; use separately encrypted backups as appropriate. No token content is
|
|
166
|
+
copied from its file. Disconnected records are retained in uniquely named
|
|
167
|
+
`.disconnected-*` siblings. Uninstall/purge refuses to delete this retained state.
|
|
168
|
+
|
|
169
|
+
A per-project exclusive `<project-path-hash>.operation.lock` beside the state
|
|
170
|
+
directory serializes Cairnkeep writers and stays held while state is archived,
|
|
171
|
+
including on native Windows. A failed
|
|
172
|
+
multi-file connect keeps its recovery record in `preparing`; the bridge refuses
|
|
173
|
+
startup until **all** entries are written and activation is committed. Run
|
|
174
|
+
disconnect to undo an interrupted operation after checking for configuration
|
|
175
|
+
drift. An interrupted disconnect is likewise resumable. Do not restore a whole
|
|
176
|
+
backup over newer unrelated edits. If the process crashed holding its lock,
|
|
177
|
+
first verify no connection/disconnection process is running, preserve the state
|
|
178
|
+
directory, then manually remove **only that project's** sibling `.operation.lock` and
|
|
179
|
+
retry disconnect. The tool never guesses that a live lock is stale.
|
|
180
|
+
|
|
181
|
+
Configuration directories must be owned/trusted and not writable by other users.
|
|
182
|
+
Each configuration is bounded to 1 MiB and the four-file recovery record to
|
|
183
|
+
16 MiB, including both original/new bytes and base64 overhead.
|
|
184
|
+
Links and unsafe files are rejected. Writers use bounded descriptor-first reads,
|
|
185
|
+
exclusive creation and atomic replacement with a final drift check. This is not
|
|
186
|
+
an OS compare-and-swap against a hostile process running as your own identity;
|
|
187
|
+
stop concurrent editor writes before applying. No root-directory or global
|
|
188
|
+
configuration target, automatic permission changes to existing directories,
|
|
189
|
+
automatic enrollment or background synchronization is introduced.
|
|
190
|
+
|
|
191
|
+
## Contributor and reviewer
|
|
192
|
+
|
|
193
|
+
Contributors get their **own** workload credential and explicit contributor
|
|
194
|
+
membership. Start with one synthetic decision; select/redact exact text and
|
|
195
|
+
confirm its value digest using the [proposal reference](team.md#harness-connection).
|
|
196
|
+
There is no automatic personal-memory import.
|
|
197
|
+
|
|
198
|
+
In the separate human terminal, inspect `proposal-show`, then approve/reject
|
|
199
|
+
the actual stored bytes with their proposal digest. Never hand a human token
|
|
200
|
+
to a model. Automated tests of human-class tokens are not human consent.
|
|
201
|
+
Restart a reader in another folder: the approved decision/provenance must match;
|
|
202
|
+
unrelated-project access and revoked credentials must fail. Follow the
|
|
203
|
+
[pilot checks and cleanup](team-pilot.md#4-exercise-the-actual-review-boundary).
|
|
204
|
+
Native trials require separately authorized providers; no model purchase or
|
|
205
|
+
subscription use is authorized by this guide.
|
|
206
|
+
|
|
207
|
+
## Deployment readiness
|
|
208
|
+
|
|
209
|
+
Before requesting a real-data pilot, record an owner and evidence for:
|
|
210
|
+
|
|
211
|
+
- **Purpose:** project IDs, participants, data classes, one continuity use case,
|
|
212
|
+
explicit exclusions and a stop date.
|
|
213
|
+
- **Consent:** selected-data approval, independent production security review
|
|
214
|
+
and deployment authorization. Mark absent decisions pending; tests supply none.
|
|
215
|
+
- **Access:** least-privilege roles, individual expiring tokens, separate human
|
|
216
|
+
review, authenticated tunnel/TLS, private storage and trusted host ownership.
|
|
217
|
+
- **Operation:** named operator, revocation/offboarding contact, storage/audit
|
|
218
|
+
capacity, encrypted backup retention and a rehearsed fresh-only restore.
|
|
219
|
+
Restored credentials are revoked; reissue and activation remain explicit.
|
|
220
|
+
- **Acceptance/rollback:** continuity across clients/folders, denied/unapproved
|
|
221
|
+
content hidden, immediate revocation and unchanged personal config hashes.
|
|
222
|
+
Stop the separate service and remove only its additional client entry to
|
|
223
|
+
withdraw access; preserve agreed private evidence.
|
|
224
|
+
|
|
225
|
+
Until these decisions are explicitly approved, stay in the disposable synthetic
|
|
226
|
+
lab. No automatic production transition follows the rehearsal.
|
package/docs/team-pilot.md
CHANGED
|
@@ -11,6 +11,8 @@ The pilot runs beside existing personal memory: a new private store, expiring
|
|
|
11
11
|
credentials, a separate loopback port and additional isolated client profiles.
|
|
12
12
|
It never imports a personal store or replaces an existing MCP registration.
|
|
13
13
|
Use synthetic project decisions, not employer, personal or customer data.
|
|
14
|
+
For a shorter role-by-role route and reader connection card, see
|
|
15
|
+
[Team onboarding](team-onboarding.md). It does not grant deployment consent.
|
|
14
16
|
|
|
15
17
|
## 1. Prepare a private lab
|
|
16
18
|
|
package/docs/team.md
CHANGED
|
@@ -20,6 +20,8 @@ Sharing changes visibility: inspect and redact the selected text first.
|
|
|
20
20
|
For a trial alongside unchanged personal memory, use the
|
|
21
21
|
[isolated pilot guide](team-pilot.md). It keeps stores, credentials, ports and
|
|
22
22
|
client profiles separate and distinguishes actual human review from rehearsal.
|
|
23
|
+
Start with the [role-based handoff](team-onboarding.md) for operator, coding
|
|
24
|
+
client and human-reviewer tasks, a tested reader check and readiness checklist.
|
|
23
25
|
|
|
24
26
|
## Trust boundary
|
|
25
27
|
|
|
@@ -78,6 +80,15 @@ OIDC/SSO and standards-compliant remote MCP HTTP authorization are deferred.
|
|
|
78
80
|
|
|
79
81
|
## Harness connection
|
|
80
82
|
|
|
83
|
+
Cairnkeep 2.26.0 (both client and team API) adds
|
|
84
|
+
the opt-in [guided connection and recovery workflow](team-onboarding.md#guided-connection-next-release--matching-source-checkout):
|
|
85
|
+
`cairn team connect`, `cairn team doctor --client` and `cairn team disconnect`.
|
|
86
|
+
It adds only a dedicated project-local entry, defaults to read-only, references
|
|
87
|
+
a private token file and pins the server project. No global MCP replacement or
|
|
88
|
+
personal upload occurs. The manual environment recipe below remains supported
|
|
89
|
+
on 2.25.0. The diagnostics API returns only authenticated project/credential
|
|
90
|
+
metadata and records a value-free audit event, not memory text.
|
|
91
|
+
|
|
81
92
|
Set `CAIRN_TEAM_URL`, `CAIRN_TEAM_ORGANIZATION` and `CAIRN_TEAM_TOKEN_FILE` in the
|
|
82
93
|
local harness environment, alongside `CAIRN_TEAM=1`. Tokens are file-only.
|
|
83
94
|
The URL may be loopback HTTP through a tunnel or HTTPS, without embedded
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.26.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -222,7 +222,18 @@ async function acquireLock(lockPath) {
|
|
|
222
222
|
catch (error) {
|
|
223
223
|
if (error.code !== "EEXIST")
|
|
224
224
|
throw error;
|
|
225
|
-
|
|
225
|
+
let info;
|
|
226
|
+
try {
|
|
227
|
+
info = await lstat(lockPath);
|
|
228
|
+
}
|
|
229
|
+
catch (inspectionError) {
|
|
230
|
+
// The current writer may release its lock after our mkdir
|
|
231
|
+
// reports contention but before inspection. Only disappearance
|
|
232
|
+
// admits another bounded acquisition attempt, never ownership.
|
|
233
|
+
if (inspectionError.code === 'ENOENT')
|
|
234
|
+
continue;
|
|
235
|
+
throw inspectionError;
|
|
236
|
+
}
|
|
226
237
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
227
238
|
throw new Error("Capability configuration lock is unsafe.");
|
|
228
239
|
}
|
|
@@ -7,6 +7,7 @@ import { hardenPrivatePath } from './platform-security.js';
|
|
|
7
7
|
import { openTeamStore, restoreTeamSnapshot, verifyTeamSnapshot, prepareTeamPrivateDirectory, TEAM_SNAPSHOT_MAX_BYTES, TeamError } from './team-store.js';
|
|
8
8
|
import { createTeamHttpServer, teamRequest } from './team-http.js';
|
|
9
9
|
import { createTeamMcpServer, teamClientToken } from './team-mcp.js';
|
|
10
|
+
import { readTeamConnection, connectionToken } from './team-client.js';
|
|
10
11
|
const HELP = `cairn team — experimental token-first team continuity (default off)
|
|
11
12
|
|
|
12
13
|
CAIRN_TEAM=1 is required. Personal memory is never uploaded.
|
|
@@ -18,7 +19,10 @@ CAIRN_TEAM=1 is required. Personal memory is never uploaded.
|
|
|
18
19
|
cairn team credential issue SUBJECT --class human|workload --expires-at ISO --output FILE [--data DIR]
|
|
19
20
|
cairn team credential revoke ID [--data DIR]
|
|
20
21
|
cairn team serve [--data DIR] [--port PORT] # requires CAIRN_TEAM_HTTP=1; loopback only
|
|
21
|
-
cairn team
|
|
22
|
+
cairn team connect [--project-root PATH] [--url URL --organization ID --project ID --token-file FILE --harness LIST] [--access read-only|full] [--apply --yes]
|
|
23
|
+
cairn team disconnect [--project-root PATH] [--apply --yes]
|
|
24
|
+
cairn team doctor --client [--project-root PATH]
|
|
25
|
+
cairn team mcp [--project-root PATH | --connection FILE] # local stdio MCP; no review/admin tools
|
|
22
26
|
cairn team list --project ID
|
|
23
27
|
cairn team proposals --project ID
|
|
24
28
|
cairn team audit --project ID # scoped, value-free metadata
|
|
@@ -41,7 +45,7 @@ deployment-specific approved real-data pilot remain admission gates. See docs/te
|
|
|
41
45
|
function parse(args) {
|
|
42
46
|
const words = [];
|
|
43
47
|
const flags = {};
|
|
44
|
-
const known = new Set(['data', 'organization', 'project', 'roles', 'class', 'expires-at', 'output', 'port', 'project-root', 'key', 'revision', 'query', 'proposal', 'input', 'confirm', 'decision']);
|
|
48
|
+
const known = new Set(['data', 'organization', 'project', 'roles', 'class', 'expires-at', 'output', 'port', 'project-root', 'key', 'revision', 'query', 'proposal', 'input', 'confirm', 'decision', 'connection']);
|
|
45
49
|
for (let i = 0; i < args.length; i++) {
|
|
46
50
|
const arg = args[i];
|
|
47
51
|
if (arg.startsWith('--')) {
|
|
@@ -81,7 +85,7 @@ async function main(args) {
|
|
|
81
85
|
const allowed = {
|
|
82
86
|
init: ['data', 'organization'], project: ['data'], member: subcommand === 'remove' ? ['data', 'project'] : ['data', 'project', 'roles'],
|
|
83
87
|
credential: subcommand === 'issue' ? ['data', 'class', 'expires-at', 'output'] : ['data'],
|
|
84
|
-
serve: ['data', 'port'], mcp: ['project-root'], doctor: ['data'], backup: ['data', 'output'],
|
|
88
|
+
serve: ['data', 'port'], mcp: ['project-root', 'connection'], doctor: ['data'], backup: ['data', 'output'],
|
|
85
89
|
restore: ['data', 'input', 'confirm'], 'memory-delete': ['data', 'project', 'key', 'confirm'],
|
|
86
90
|
list: ['project'], proposals: ['project'], audit: ['project'], read: ['project', 'key', 'revision'],
|
|
87
91
|
history: ['project', 'key'], search: ['project', 'query'], 'proposal-show': ['project', 'proposal'],
|
|
@@ -99,8 +103,14 @@ async function main(args) {
|
|
|
99
103
|
if (command === 'mcp') {
|
|
100
104
|
if (words.length !== 1)
|
|
101
105
|
throw new TeamError('invalid');
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
if (flags.connection && flags['project-root'])
|
|
107
|
+
throw new TeamError('invalid');
|
|
108
|
+
const connection = flags.connection ? readTeamConnection(resolve(flags.connection)) : undefined;
|
|
109
|
+
const token = connection ? () => connectionToken(connection) : () => teamClientToken();
|
|
110
|
+
token(); // validate private credential before startup
|
|
111
|
+
const server = await createTeamMcpServer(connection
|
|
112
|
+
? { url: connection.url, organization: connection.organization, project: connection.project, readOnly: connection.access === 'read-only', token, projectRoot: connection.project_root }
|
|
113
|
+
: { url: process.env.CAIRN_TEAM_URL ?? '', organization: process.env.CAIRN_TEAM_ORGANIZATION ?? '', token, projectRoot: flags['project-root'] });
|
|
104
114
|
await server.connect(new StdioServerTransport());
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { readStableJson, readStableFile } from './stable-file.js';
|
|
5
|
+
import { teamClientToken } from './team-mcp.js';
|
|
6
|
+
import { teamRequest, validateTeamUrl } from './team-http.js';
|
|
7
|
+
import { TeamError } from './team-store.js';
|
|
8
|
+
const id = z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
|
|
9
|
+
const path = z.string().min(1).max(4096).refine(value => isAbsolute(value) && !/[\x00-\x1f\x7f]/.test(value));
|
|
10
|
+
const schema = z.object({
|
|
11
|
+
schema_version: z.literal(1), project_root: path,
|
|
12
|
+
organization: id, project: id, url: z.string().max(4096), token_file: path,
|
|
13
|
+
access: z.enum(['read-only', 'full']),
|
|
14
|
+
}).strict();
|
|
15
|
+
export class TeamClientError extends Error {
|
|
16
|
+
}
|
|
17
|
+
export function validateTeamConnection(value) {
|
|
18
|
+
const result = schema.safeParse(value);
|
|
19
|
+
if (!result.success)
|
|
20
|
+
throw new TeamClientError('Team connection fields are invalid; check organization/project IDs, absolute paths and access profile.');
|
|
21
|
+
try {
|
|
22
|
+
validateTeamUrl(result.data.url);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new TeamClientError('Team connection URL must be loopback HTTP or HTTPS without credentials, query, fragment or path prefix.');
|
|
26
|
+
}
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
29
|
+
export function readTeamConnection(path) {
|
|
30
|
+
const { bytes } = readStableFile(path, { label: 'Team connection', maxBytes: 32768, private: true });
|
|
31
|
+
let value;
|
|
32
|
+
try {
|
|
33
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new TeamClientError('Team connection contains invalid UTF-8 or JSON.');
|
|
37
|
+
}
|
|
38
|
+
const connection = validateTeamConnection(value);
|
|
39
|
+
const receipt = readStableJson(join(dirname(path), 'receipt.json'), { label: 'Team connection activation', maxBytes: 16 * 1024 * 1024, private: true });
|
|
40
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)
|
|
41
|
+
|| receipt.schema_version !== 1
|
|
42
|
+
|| receipt.status !== 'connected'
|
|
43
|
+
|| receipt.project_root !== connection.project_root)
|
|
44
|
+
throw new TeamClientError('Team connection is incomplete or disconnected; inspect recovery state.');
|
|
45
|
+
if (receipt.connection_digest !== createHash('sha256').update(bytes).digest('hex'))
|
|
46
|
+
throw new TeamClientError('Team connection activation changed; inspect recovery state.');
|
|
47
|
+
return connection;
|
|
48
|
+
}
|
|
49
|
+
export function connectionToken(connection) {
|
|
50
|
+
try {
|
|
51
|
+
return teamClientToken({ CAIRN_TEAM_TOKEN_FILE: connection.token_file });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new TeamClientError('Team credential file is unsafe or unreadable; use an owner-only file issued by the operator.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Authenticate and check reader membership without retrieving memory or proposals.
|
|
58
|
+
* Expiry and roles come from the service, never from guesses about opaque tokens. */
|
|
59
|
+
export async function connectionDoctor(connection) {
|
|
60
|
+
connection = validateTeamConnection(connection);
|
|
61
|
+
const token = connectionToken(connection);
|
|
62
|
+
const result = await teamRequest(connection.url, token, { organization: connection.organization, project: connection.project, operation: 'connection-check' });
|
|
63
|
+
const metadata = z.object({ schema_version: z.literal(1), organization: id, project: id,
|
|
64
|
+
subject: id, credential_id: z.string().uuid(), credential_class: z.enum(['human', 'workload']),
|
|
65
|
+
expires_at: z.iso.datetime(), roles: z.array(z.enum(['reader', 'contributor', 'reviewer', 'maintainer', 'auditor'])).max(5),
|
|
66
|
+
}).strict().safeParse(result);
|
|
67
|
+
if (!metadata.success || metadata.data.organization !== connection.organization || metadata.data.project !== connection.project
|
|
68
|
+
|| !metadata.data.roles.includes('reader') || Date.parse(metadata.data.expires_at) <= Date.now())
|
|
69
|
+
throw new TeamError('integrity');
|
|
70
|
+
return { ok: true, ...metadata.data, access: connection.access, experimental: true };
|
|
71
|
+
}
|
|
@@ -32,6 +32,8 @@ export async function createTeamMcpServer(options) {
|
|
|
32
32
|
validateTeamUrl(options.url);
|
|
33
33
|
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(options.organization))
|
|
34
34
|
throw new TeamError('invalid');
|
|
35
|
+
if (options.project !== undefined && !/^[a-z0-9][a-z0-9-]{0,63}$/.test(options.project))
|
|
36
|
+
throw new TeamError('invalid');
|
|
35
37
|
const profile = resolveMcpToolProfile({ projectRoot: options.projectRoot });
|
|
36
38
|
const capability = isCapabilityContractEnabled() ? await resolveCapabilityStatus({ projectRoot: options.projectRoot }) : undefined;
|
|
37
39
|
if (capability?.issues.length)
|
|
@@ -57,6 +59,8 @@ export async function createTeamMcpServer(options) {
|
|
|
57
59
|
const enabled = Object.keys(TEAM_TOOL_CATALOG).filter(name => {
|
|
58
60
|
if (!profileAllowsTool(profile, coreNames[name]))
|
|
59
61
|
return false;
|
|
62
|
+
if (options.readOnly && !TEAM_TOOL_CATALOG[name].annotations.readOnlyHint)
|
|
63
|
+
return false;
|
|
60
64
|
if (mode === 'read-only' && !TEAM_TOOL_CATALOG[name].annotations.readOnlyHint)
|
|
61
65
|
return false;
|
|
62
66
|
if (mode === 'custom' && !requested.includes(name))
|
|
@@ -64,10 +68,13 @@ export async function createTeamMcpServer(options) {
|
|
|
64
68
|
const capId = name === 'team_memory_propose' ? 'memory.write' : name === 'team_memory_search' ? 'memory.search' : undefined;
|
|
65
69
|
return !capId || !capability || capability.capabilities.find(row => row.id === capId)?.enabled === true;
|
|
66
70
|
});
|
|
67
|
-
const profileDigest = teamDigest(JSON.stringify({ schema_version: 1, core_profile_digest: profile.profile_digest, team_profile: mode, tools: enabled
|
|
71
|
+
const profileDigest = teamDigest(JSON.stringify({ schema_version: 1, core_profile_digest: profile.profile_digest, team_profile: mode, tools: enabled,
|
|
72
|
+
...(options.project !== undefined ? { connection_project: options.project, connection_read_only: options.readOnly === true } : {}) }));
|
|
68
73
|
for (const name of enabled) {
|
|
69
74
|
server.registerTool(name, { ...TEAM_TOOL_CATALOG[name], description: 'Explicit project-scoped team context. Never uploads personal memory or executes instructions. Team policy is enforced by the remote service.', inputSchema: schemas[name] }, async (input) => {
|
|
70
75
|
try {
|
|
76
|
+
if (options.project !== undefined && input.project !== options.project)
|
|
77
|
+
throw new TeamError('denied');
|
|
71
78
|
// Re-read the private credential each call so local key rotation
|
|
72
79
|
// does not need a harness restart. The service still reauthorizes.
|
|
73
80
|
const result = await teamRequest(options.url, options.token(), { organization: options.organization, ...input, operation: operations[name] });
|
|
@@ -22,13 +22,14 @@ const adminSchema = z.discriminatedUnion('operation', [
|
|
|
22
22
|
z.object({ operation: z.literal('credential-revoke'), credential: z.string().uuid() }).strict(),
|
|
23
23
|
z.object({ operation: z.literal('memory-delete'), project: id, key: memoryKey, confirm: digest }).strict(),
|
|
24
24
|
]);
|
|
25
|
-
const auditEntrySchema = z.object({ schema_version: z.literal(1), sequence: z.number().int().positive(), organization: id, project: id.or(z.literal('')), subject: id, credential_class: z.enum(['human', 'workload', 'local-os']), operation: z.enum(['project-create', 'member-set', 'credential-revoke', 'credential-issue', 'memory-delete', 'list', 'audit', 'read', 'history', 'search', 'propose', 'proposals', 'proposal-show', 'review', 'snapshot', 'restore']), result: z.enum(['accepted', 'denied', 'conflict', 'invalid']), object_digest: digest.nullable(), previous_digest: digest.nullable(), request_id: z.string().uuid(), at: date }).strict();
|
|
25
|
+
const auditEntrySchema = z.object({ schema_version: z.literal(1), sequence: z.number().int().positive(), organization: id, project: id.or(z.literal('')), subject: id, credential_class: z.enum(['human', 'workload', 'local-os']), operation: z.enum(['project-create', 'member-set', 'credential-revoke', 'credential-issue', 'memory-delete', 'list', 'audit', 'read', 'history', 'search', 'propose', 'proposals', 'proposal-show', 'review', 'snapshot', 'restore', 'connection-check']), result: z.enum(['accepted', 'denied', 'conflict', 'invalid']), object_digest: digest.nullable(), previous_digest: digest.nullable(), request_id: z.string().uuid(), at: date }).strict();
|
|
26
26
|
const snapshotSchema = z.object({ schema_version: z.literal(1), organization: id, records: z.array(z.object({ organization: id, project: id.or(z.literal('')), kind: z.enum(['project', 'member', 'credential', 'proposal', 'review', 'revision', 'head']), id: z.string().min(1).max(128), value: z.unknown() }).strict()).max(32768), audit: z.array(z.object({ sequence: z.number().int().positive(), organization: id, project: id.or(z.literal('')), value: z.string().max(4096), digest }).strict()).max(100000), digest }).strict();
|
|
27
27
|
const MAX_RECORD_BYTES = 16 * 1024 * 1024;
|
|
28
28
|
const MAX_AUDIT_BYTES = 16 * 1024 * 1024;
|
|
29
29
|
export const TEAM_SNAPSHOT_MAX_BYTES = 64 * 1024 * 1024;
|
|
30
30
|
const requestBase = { organization: id, project: id };
|
|
31
31
|
const requestSchema = z.discriminatedUnion('operation', [
|
|
32
|
+
z.object({ ...requestBase, operation: z.literal('connection-check') }).strict(),
|
|
32
33
|
z.object({ ...requestBase, operation: z.literal('list') }).strict(),
|
|
33
34
|
z.object({ ...requestBase, operation: z.literal('audit') }).strict(),
|
|
34
35
|
z.object({ ...requestBase, operation: z.literal('search'), query: z.string().min(1).max(256) }).strict(),
|
|
@@ -459,7 +460,12 @@ export class TeamStore {
|
|
|
459
460
|
const project = valid(projectSchema, await this.get('project', request.project, request.project));
|
|
460
461
|
let result;
|
|
461
462
|
let objectDigest = null;
|
|
462
|
-
if (request.operation === '
|
|
463
|
+
if (request.operation === 'connection-check') {
|
|
464
|
+
result = { schema_version: 1, organization: this.organization, project: request.project,
|
|
465
|
+
subject: credential.subject, credential_id: credential.id, credential_class: credential.credential_class,
|
|
466
|
+
expires_at: credential.expires_at, roles: member.roles };
|
|
467
|
+
}
|
|
468
|
+
else if (request.operation === 'audit') {
|
|
463
469
|
const rows = await this.agent.getDatabase().prepare('SELECT value,digest FROM team_audit WHERE organization=? AND project=? ORDER BY sequence DESC LIMIT 100').all(this.organization, request.project);
|
|
464
470
|
result = rows.map(row => ({ ...JSON.parse(sqlText(row.value)), digest: row.digest }));
|
|
465
471
|
}
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"check:context-pack-retrieval": "node scripts/smoke-context-pack-retrieval.mjs",
|
|
59
59
|
"check:context-pack-v216-compat": "node scripts/smoke-context-pack-v216-compat.mjs",
|
|
60
60
|
"check:context-usage": "node scripts/smoke-context-usage.mjs",
|
|
61
|
-
"check:domain-retrieval": "node scripts/smoke-domain-retrieval.mjs",
|
|
61
|
+
"check:domain-retrieval": "node scripts/smoke-domain-retrieval.mjs && node scripts/smoke-provider-config-flow.mjs",
|
|
62
62
|
"check:anythingllm-workspace": "node scripts/smoke-anythingllm-workspace.mjs && node scripts/smoke-anythingllm-workspace-mcp.mjs",
|
|
63
63
|
"check:retrieval-benchmark": "node scripts/smoke-retrieval-benchmark.mjs && node scripts/run-retrieval-benchmark.mjs --check",
|
|
64
64
|
"benchmark:retrieval": "npm run build && node scripts/run-retrieval-benchmark.mjs",
|
|
65
65
|
"check:work-evidence": "node scripts/smoke-work-evidence.mjs",
|
|
66
66
|
"check:okf": "node scripts/smoke-okf.mjs",
|
|
67
67
|
"check:playbook": "node scripts/smoke-playbook.mjs",
|
|
68
|
-
"check:security-assurance": "node scripts/smoke-security-
|
|
68
|
+
"check:security-assurance": "node scripts/smoke-security-fixture-order.mjs",
|
|
69
69
|
"check:notes": "npm run build && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment",
|
|
70
70
|
"test": "npm run test:smoke",
|
|
71
71
|
"test:smoke": "npm run build && node scripts/run-smoke-suite.mjs",
|
package/package.json
CHANGED
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -62,7 +62,7 @@ Usage:
|
|
|
62
62
|
cairn evidence <list|show|delete|prune|doctor>
|
|
63
63
|
cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>
|
|
64
64
|
cairn security doctor [--project PATH] [--json]
|
|
65
|
-
cairn team <init|project|member|credential|serve|mcp|list|read|search|history|propose|proposals|proposal-show|review|audit|doctor|backup|restore|memory-delete>
|
|
65
|
+
cairn team <connect|disconnect|init|project|member|credential|serve|mcp|list|read|search|history|propose|proposals|proposal-show|review|audit|doctor|backup|restore|memory-delete>
|
|
66
66
|
cairn memory <path|export|import>
|
|
67
67
|
cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
|
|
68
68
|
cairn uninstall [--dry-run] [--yes] [--purge-memory] [--purge-packs] [PROJECT ...]
|
|
@@ -96,6 +96,11 @@ export async function main(argv) {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
if (command === 'team' && (['connect', 'disconnect'].includes(args[0]) || (args[0] === 'doctor' && args.includes('--client')))) {
|
|
100
|
+
const { runTeamConnection } = await import('./team-connect.mjs');
|
|
101
|
+
process.exitCode = await runTeamConnection(args);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
99
104
|
const nodeEntry = NODE_COMMANDS.get(command);
|
|
100
105
|
if (nodeEntry) {
|
|
101
106
|
// Preserve the historical dispatcher contract: child Node resolution comes
|
package/scripts/completion.sh
CHANGED
|
@@ -27,7 +27,7 @@ _cairn_complete() {
|
|
|
27
27
|
sync-omp) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
28
28
|
doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
|
|
29
29
|
security) COMPREPLY=( $(compgen -W "doctor --project --json" -- "$current") ) ;;
|
|
30
|
-
team) COMPREPLY=( $(compgen -W "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete --organization --data --project --roles --class --expires-at --output --input --confirm --port --key --query --proposal --decision --revision --project-root" -- "$current") ) ;;
|
|
30
|
+
team) COMPREPLY=( $(compgen -W "connect disconnect init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete --organization --data --project --roles --class --expires-at --output --input --confirm --port --key --query --proposal --decision --revision --project-root --connection --client --url --token-file --harness --access --apply --yes" -- "$current") ) ;;
|
|
31
31
|
trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
|
|
32
32
|
artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
|
|
33
33
|
evidence) COMPREPLY=( $(compgen -W "list show delete prune doctor --status --json --dry-run --repair" -- "$current") ) ;;
|
|
@@ -134,7 +134,7 @@ _cairn() {
|
|
|
134
134
|
sync-omp) _arguments '--apply[apply changes]' '--live-root[omp agent root]:directory:_files -/' ;;
|
|
135
135
|
doctor) _arguments '--repair[repair trajectory metadata and indexes]' ;;
|
|
136
136
|
security) _values 'security command' doctor '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
|
|
137
|
-
team) _values 'team command' init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete '--organization[team option]:value:' '--data[team option]:value:' '--project[team option]:value:' '--roles[team option]:value:' '--class[team option]:value:' '--expires-at[team option]:value:' '--output[team option]:value:' '--input[team option]:value:' '--confirm[team option]:value:' '--port[team option]:value:' '--key[team option]:value:' '--query[team option]:value:' '--proposal[team option]:value:' '--decision[team option]:value:' '--revision[team option]:value:' '--project-root[team option]:value:' ;;
|
|
137
|
+
team) _values 'team command' connect disconnect init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete '--organization[team option]:value:' '--data[team option]:value:' '--project[team option]:value:' '--roles[team option]:value:' '--class[team option]:value:' '--expires-at[team option]:value:' '--output[team option]:value:' '--input[team option]:value:' '--confirm[team option]:value:' '--port[team option]:value:' '--key[team option]:value:' '--query[team option]:value:' '--proposal[team option]:value:' '--decision[team option]:value:' '--revision[team option]:value:' '--apply[apply plan]' '--yes[confirm plan]' '--client[client diagnostics]' '--url[team URL]:value:' '--token-file[private credential file]:file:_files' '--harness[team adapters]:value:' '--access[tool profile]:value:(read-only full)' '--connection[private connection file]:file:_files' '--project-root[team option]:value:' ;;
|
|
138
138
|
trajectory) _values 'trajectory command' list show prune ;;
|
|
139
139
|
artifact)
|
|
140
140
|
case $words[3] in
|
|
@@ -222,7 +222,7 @@ complete -c cairn -n "__fish_seen_subcommand_from doctor" -l repair
|
|
|
222
222
|
complete -c cairn -n "__fish_seen_subcommand_from security" -a "doctor"
|
|
223
223
|
complete -c cairn -n "__fish_seen_subcommand_from security" -l project -r
|
|
224
224
|
complete -c cairn -n "__fish_seen_subcommand_from security" -l json
|
|
225
|
-
complete -c cairn -n "__fish_seen_subcommand_from team" -a "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete"
|
|
225
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -a "connect disconnect init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete"
|
|
226
226
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l organization -r
|
|
227
227
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l data -r
|
|
228
228
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l project -r
|
|
@@ -239,6 +239,14 @@ complete -c cairn -n "__fish_seen_subcommand_from team" -l proposal -r
|
|
|
239
239
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l decision -r
|
|
240
240
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l revision -r
|
|
241
241
|
complete -c cairn -n "__fish_seen_subcommand_from team" -l project-root -r
|
|
242
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l connection -r
|
|
243
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l client
|
|
244
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l url -r
|
|
245
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l token-file -r
|
|
246
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l harness -r -a "claude opencode qwen codex"
|
|
247
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l access -r -a "read-only full"
|
|
248
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l apply
|
|
249
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l yes
|
|
242
250
|
complete -c cairn -n "__fish_seen_subcommand_from trajectory" -a "list show prune"
|
|
243
251
|
complete -c cairn -n "__fish_seen_subcommand_from artifact" -a "list show delete prune"
|
|
244
252
|
complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from list" -l kind -r
|
|
@@ -11,7 +11,7 @@ function canonical(path) {
|
|
|
11
11
|
|
|
12
12
|
/** Uninstall never grants permission to purge the separate team store. */
|
|
13
13
|
export function assertUninstallRetainsTeam(targets, env = process.env, home = homedir()) {
|
|
14
|
-
const stores = new Set([join(home, '.cairnkeep', 'team'), ...(env.CAIRN_TEAM_BASE_DIR ? [env.CAIRN_TEAM_BASE_DIR] : [])]);
|
|
14
|
+
const stores = new Set([join(home, '.cairnkeep', 'team'), join(home, '.cairnkeep', 'team-clients'), ...(env.CAIRN_TEAM_BASE_DIR ? [env.CAIRN_TEAM_BASE_DIR] : [])]);
|
|
15
15
|
const normalized = path => {
|
|
16
16
|
const value = canonical(path);
|
|
17
17
|
return process.platform === 'win32' ? value.toLowerCase() : value;
|