@cairnkeep/cli 2.25.0 → 2.27.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/docs/README.md +6 -0
  3. package/docs/harness-compatibility.md +5 -2
  4. package/docs/learning/CURRICULUM-MAP.md +3 -3
  5. package/docs/learning/FEATURE-GUIDE.md +13 -1
  6. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  7. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  8. package/docs/learning/lessons/L02-installation.md +2 -2
  9. package/docs/learning/lessons/L03-first-project.md +1 -1
  10. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  11. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  12. package/docs/learning/lessons/L21-context-packs.md +1 -1
  13. package/docs/learning/lessons/L22-native-windows.md +2 -2
  14. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  15. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  16. package/docs/learning/lessons/L25-playbooks.md +1 -1
  17. package/docs/learning/lessons/L26-context-intelligence.md +1 -1
  18. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  19. package/docs/learning/lessons/L28-security-assurance.md +5 -1
  20. package/docs/learning/lessons/L29-team-continuity.md +42 -1
  21. package/docs/learning/tracks/operator.md +5 -0
  22. package/docs/learning/video-scripts/V28-security-assurance.md +4 -0
  23. package/docs/learning/video-scripts/V29-team-continuity.md +29 -1
  24. package/docs/operating.md +10 -0
  25. package/docs/privacy-and-data-flow.md +30 -1
  26. package/docs/provider-data-flow-review.md +48 -0
  27. package/docs/releasing.md +6 -0
  28. package/docs/security-assurance.md +11 -0
  29. package/docs/storage.md +23 -0
  30. package/docs/team-onboarding.md +230 -0
  31. package/docs/team-operations.md +127 -0
  32. package/docs/team-pilot.md +2 -0
  33. package/docs/team.md +21 -4
  34. package/examples/eval/bundled-fake.json +1 -1
  35. package/mcp-memory-server/dist/capability-config.js +12 -1
  36. package/mcp-memory-server/dist/team-audit.js +41 -0
  37. package/mcp-memory-server/dist/team-cli.js +81 -30
  38. package/mcp-memory-server/dist/team-client.js +71 -0
  39. package/mcp-memory-server/dist/team-errors.js +7 -0
  40. package/mcp-memory-server/dist/team-files.js +28 -0
  41. package/mcp-memory-server/dist/team-mcp.js +12 -4
  42. package/mcp-memory-server/dist/team-pages.js +40 -0
  43. package/mcp-memory-server/dist/team-store.js +105 -40
  44. package/mcp-memory-server/package.json +2 -2
  45. package/package.json +1 -1
  46. package/scripts/cairn-cli.mjs +6 -1
  47. package/scripts/completion.sh +16 -3
  48. package/scripts/lib/team-retention.mjs +1 -1
  49. package/scripts/team-connect.mjs +366 -0
  50. package/scripts/test-ci-sharding.sh +6 -1
  51. package/scripts/test-codeql-cohort.mjs +35 -0
  52. package/scripts/test-completion.sh +7 -1
  53. package/scripts/test-package-install.sh +1 -0
  54. package/scripts/test-phase18-capability-lifecycle.sh +2 -2
  55. package/scripts/test-release-cache.mjs +16 -0
  56. package/scripts/test-security-baseline.sh +4 -0
  57. package/scripts/test-smoke-timings.mjs +19 -0
  58. package/scripts/test-team-docs.sh +26 -2
  59. package/scripts/test-team-installed.mjs +62 -0
  60. package/scripts/test-team-lab-env.mjs +9 -0
  61. package/scripts/test-team-operations.mjs +31 -0
  62. package/scripts/test-team-operations.sh +5 -0
  63. package/scripts/test-team-pilot.mjs +43 -1
  64. package/scripts/verify-published-release.mjs +11 -2
  65. package/scripts/verify-security-baseline.mjs +20 -0
  66. package/scripts/windows-platform.mjs +1 -1
@@ -3,6 +3,10 @@ import { closeSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, op
3
3
  import { basename, dirname, isAbsolute, join, parse, resolve } from 'node:path';
4
4
  import { z } from 'zod';
5
5
  import { hardenPrivatePath, privatePathIsSafe } from './platform-security.js';
6
+ import { TeamError } from './team-errors.js';
7
+ export { TeamError } from './team-errors.js';
8
+ import { teamAuditCheckpointSchema, verifyTeamAuditRows, verifyTeamAuditArchive } from './team-audit.js';
9
+ import { teamPageSchema, pageTeamItems } from './team-pages.js';
6
10
  const id = z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
7
11
  const roles = z.array(z.enum(['reader', 'contributor', 'reviewer', 'maintainer', 'auditor'])).max(5);
8
12
  const digest = z.string().regex(/^[a-f0-9]{64}$/);
@@ -22,20 +26,21 @@ const adminSchema = z.discriminatedUnion('operation', [
22
26
  z.object({ operation: z.literal('credential-revoke'), credential: z.string().uuid() }).strict(),
23
27
  z.object({ operation: z.literal('memory-delete'), project: id, key: memoryKey, confirm: digest }).strict(),
24
28
  ]);
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();
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();
29
+ const legacySnapshotSchema = 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();
30
+ const snapshotSchema = z.discriminatedUnion('schema_version', [legacySnapshotSchema, legacySnapshotSchema.extend({ schema_version: z.literal(2), audit_checkpoint: teamAuditCheckpointSchema })]);
27
31
  const MAX_RECORD_BYTES = 16 * 1024 * 1024;
28
32
  const MAX_AUDIT_BYTES = 16 * 1024 * 1024;
29
33
  export const TEAM_SNAPSHOT_MAX_BYTES = 64 * 1024 * 1024;
30
34
  const requestBase = { organization: id, project: id };
31
35
  const requestSchema = z.discriminatedUnion('operation', [
32
- z.object({ ...requestBase, operation: z.literal('list') }).strict(),
33
- z.object({ ...requestBase, operation: z.literal('audit') }).strict(),
34
- z.object({ ...requestBase, operation: z.literal('search'), query: z.string().min(1).max(256) }).strict(),
36
+ z.object({ ...requestBase, operation: z.literal('connection-check') }).strict(),
37
+ z.object({ ...requestBase, operation: z.literal('list'), page: teamPageSchema.optional() }).strict(),
38
+ z.object({ ...requestBase, operation: z.literal('audit'), page: teamPageSchema.optional() }).strict(),
39
+ z.object({ ...requestBase, operation: z.literal('search'), page: teamPageSchema.optional(), query: z.string().min(1).max(256) }).strict(),
35
40
  z.object({ ...requestBase, operation: z.literal('read'), key: memoryKey, revision: digest.optional() }).strict(),
36
- z.object({ ...requestBase, operation: z.literal('history'), key: memoryKey }).strict(),
41
+ z.object({ ...requestBase, operation: z.literal('history'), page: teamPageSchema.optional(), key: memoryKey }).strict(),
37
42
  z.object({ ...requestBase, operation: z.literal('propose'), ...proposalInput.shape }).strict(),
38
- z.object({ ...requestBase, operation: z.literal('proposals') }).strict(),
43
+ z.object({ ...requestBase, operation: z.literal('proposals'), page: teamPageSchema.optional() }).strict(),
39
44
  z.object({ ...requestBase, operation: z.literal('proposal-show'), proposal_id: z.string().uuid() }).strict(),
40
45
  z.object({ ...requestBase, operation: z.literal('review'), proposal_id: z.string().uuid(), confirm: digest, decision: z.enum(['approve', 'reject']) }).strict(),
41
46
  ]);
@@ -54,13 +59,6 @@ async function immediate(db, operation) {
54
59
  throw error;
55
60
  }
56
61
  }
57
- export class TeamError extends Error {
58
- code;
59
- constructor(code) {
60
- super(`Team ${code}.`);
61
- this.code = code;
62
- }
63
- }
64
62
  export function teamDigest(text) { return createHash('sha256').update(text).digest('hex'); }
65
63
  function checkedProposal(input) {
66
64
  const value = valid(proposalSchema, input);
@@ -171,18 +169,7 @@ export function verifyTeamSnapshot(input) {
171
169
  }
172
170
  }
173
171
  }
174
- let previous = null;
175
- let sequence = 0;
176
- let auditBytes = 0;
177
- for (const row of snapshot.audit) {
178
- const entry = valid(auditEntrySchema, JSON.parse(row.value));
179
- auditBytes += Buffer.byteLength(row.value);
180
- if (row.organization !== snapshot.organization || row.sequence !== ++sequence || entry.sequence !== sequence || entry.previous_digest !== previous || entry.organization !== row.organization || entry.project !== row.project || row.digest !== teamDigest(row.value))
181
- throw new Error();
182
- previous = row.digest;
183
- }
184
- if (auditBytes > MAX_AUDIT_BYTES + 256 * 1024)
185
- throw new Error();
172
+ verifyTeamAuditRows(snapshot.organization, snapshot.audit, snapshot.schema_version === 2 ? snapshot.audit_checkpoint : null);
186
173
  return snapshot;
187
174
  }
188
175
  catch {
@@ -300,7 +287,7 @@ export class TeamStore {
300
287
  return options.organization;
301
288
  });
302
289
  const version = await db.prepare('SELECT value FROM team_meta WHERE id = ?').get('schema_version');
303
- if (version?.value !== '1')
290
+ if (!['1', '2'].includes(String(version?.value)))
304
291
  throw new TeamError('integrity');
305
292
  const store = new TeamStore(agent, org, root);
306
293
  store.harden();
@@ -367,8 +354,9 @@ export class TeamStore {
367
354
  async appendAudit(project, subject, credentialClass, operation, result, objectDigest = null) {
368
355
  const db = this.agent.getDatabase();
369
356
  const last = await db.prepare('SELECT sequence,digest FROM team_audit ORDER BY sequence DESC LIMIT 1').get();
370
- const sequence = valid(z.number().int().nonnegative(), last?.sequence ?? 0) + 1;
371
- const entry = { schema_version: 1, sequence, organization: this.organization, project, subject, credential_class: credentialClass, operation, result, object_digest: objectDigest, previous_digest: last?.digest ?? null, request_id: randomUUID(), at: new Date().toISOString() };
357
+ const checkpoint = this.auditCheckpoint();
358
+ const sequence = valid(z.number().int().nonnegative(), last?.sequence ?? checkpoint?.sequence ?? 0) + 1;
359
+ const entry = { schema_version: 1, sequence, organization: this.organization, project, subject, credential_class: credentialClass, operation, result, object_digest: objectDigest, previous_digest: last?.digest ?? checkpoint?.digest ?? null, request_id: randomUUID(), at: new Date().toISOString() };
372
360
  const value = JSON.stringify(entry);
373
361
  const size = db.prepare('SELECT count(*) AS entries,coalesce(sum(length(CAST(value AS BLOB))),0) AS bytes FROM team_audit').get();
374
362
  // Reserve bounded space for local revocation/recovery after a client's
@@ -459,9 +447,14 @@ export class TeamStore {
459
447
  const project = valid(projectSchema, await this.get('project', request.project, request.project));
460
448
  let result;
461
449
  let objectDigest = null;
462
- if (request.operation === 'audit') {
463
- 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
- result = rows.map(row => ({ ...JSON.parse(sqlText(row.value)), digest: row.digest }));
450
+ if (request.operation === 'connection-check') {
451
+ result = { schema_version: 1, organization: this.organization, project: request.project,
452
+ subject: credential.subject, credential_id: credential.id, credential_class: credential.credential_class,
453
+ expires_at: credential.expires_at, roles: member.roles };
454
+ }
455
+ else if (request.operation === 'audit') {
456
+ const rows = await this.agent.getDatabase().prepare('SELECT value,digest FROM team_audit WHERE organization=? AND project=? ORDER BY sequence DESC' + (request.page ? '' : ' LIMIT 100')).all(this.organization, request.project);
457
+ result = (request.page ? rows : rows.slice(0, 100)).map(row => ({ ...JSON.parse(sqlText(row.value)), digest: row.digest }));
465
458
  }
466
459
  else if (request.operation === 'propose') {
467
460
  const { organization: _org, project: _project, operation: _op, ...rawInput } = request;
@@ -525,7 +518,7 @@ export class TeamStore {
525
518
  result = proposal;
526
519
  }
527
520
  else {
528
- result = await Promise.all(visible.slice(0, 100).map(async (proposal) => ({ id: proposal.id, digest: proposal.digest, key: proposal.input.key, subject: proposal.subject, expires_at: proposal.input.expires_at, status: (await this.get('review', request.project, proposal.id)) ? 'reviewed' : 'pending' })));
521
+ result = await Promise.all(visible.slice(0, request.page ? undefined : 100).map(async (proposal) => ({ id: proposal.id, digest: proposal.digest, key: proposal.input.key, subject: proposal.subject, expires_at: proposal.input.expires_at, status: (await this.get('review', request.project, proposal.id)) ? 'reviewed' : 'pending' })));
529
522
  }
530
523
  }
531
524
  else if (request.operation === 'read') {
@@ -541,17 +534,29 @@ export class TeamStore {
541
534
  }
542
535
  }
543
536
  else if (request.operation === 'history') {
544
- result = (await this.rows('revision', request.project)).map(row => checkedRevision(row.value)).filter(revision => revision.key === request.key).sort((a, b) => b.created_at.localeCompare(a.created_at) || a.revision_digest.localeCompare(b.revision_digest)).slice(0, 100).map(({ value: _value, ...metadata }) => metadata);
537
+ result = (await this.rows('revision', request.project)).map(row => checkedRevision(row.value)).filter(revision => revision.key === request.key).sort((a, b) => b.created_at.localeCompare(a.created_at) || a.revision_digest.localeCompare(b.revision_digest)).slice(0, request.page ? undefined : 100).map(({ value: _value, ...metadata }) => metadata);
545
538
  }
546
539
  else {
547
540
  const heads = await this.rows('head', request.project);
548
541
  const revisions = await Promise.all(heads.map(async (head) => checkedRevision(await this.get('revision', request.project, valid(digest, head.value)))));
549
542
  if (request.operation === 'search') {
550
543
  const needle = request.query.toLowerCase();
551
- result = revisions.filter(revision => `${revision.key}\n${revision.value}`.toLowerCase().includes(needle)).slice(0, 10).map(revision => this.presentRevision(revision));
544
+ result = revisions.filter(revision => `${revision.key}\n${revision.value}`.toLowerCase().includes(needle)).slice(0, request.page ? undefined : 10).map(revision => this.presentRevision(revision));
552
545
  }
553
546
  else
554
- result = revisions.slice(0, 100).map(({ value: _value, ...metadata }) => metadata);
547
+ result = revisions.slice(0, request.page ? undefined : 100).map(({ value: _value, ...metadata }) => metadata);
548
+ }
549
+ if ('page' in request && request.page) {
550
+ if (!Array.isArray(result) || (request.operation === 'search' && request.page.size > 10))
551
+ throw new TeamError('invalid');
552
+ const db = this.agent.getDatabase();
553
+ let secret = db.prepare('SELECT value FROM team_meta WHERE id=?').get('pagination-secret')?.value;
554
+ if (!secret) {
555
+ secret = randomBytes(32).toString('hex');
556
+ db.prepare('INSERT INTO team_meta(id,value) VALUES(?,?)').run('pagination-secret', secret);
557
+ }
558
+ const scope = JSON.stringify([this.organization, request.project, credential.id, credential.subject, credential.credential_class, member.roles, project.policy_revision, request.operation, 'key' in request ? request.key : null, 'query' in request ? request.query : null, request.page.size, this.auditCheckpoint()?.archive_digest ?? null]);
559
+ result = pageTeamItems(result, valid(digest, secret), scope, request.page, request.operation === 'audit');
555
560
  }
556
561
  await this.appendAudit(request.project, credential.subject, credential.credential_class, request.operation, 'accepted', objectDigest);
557
562
  return result;
@@ -577,12 +582,68 @@ export class TeamStore {
577
582
  const db = this.agent.getDatabase();
578
583
  const records = db.prepare('SELECT organization,project,kind,id,value FROM team_records ORDER BY organization,project,kind,id').all().map(row => ({ organization: sqlText(row.organization), project: sqlText(row.project), kind: sqlText(row.kind), id: sqlText(row.id), value: JSON.parse(sqlText(row.value)) }));
579
584
  const audit = db.prepare('SELECT sequence,organization,project,value,digest FROM team_audit ORDER BY sequence').all();
580
- const body = { schema_version: 1, organization: this.organization, records, audit };
585
+ const checkpoint = this.auditCheckpoint();
586
+ const body = checkpoint ? { schema_version: 2, organization: this.organization, records, audit, audit_checkpoint: checkpoint } : { schema_version: 1, organization: this.organization, records, audit };
581
587
  return verifyTeamSnapshot({ ...body, digest: teamDigest(JSON.stringify(body)) });
582
588
  }
583
- async snapshot() {
589
+ auditCheckpoint() {
590
+ const db = this.agent.getDatabase();
591
+ const version = db.prepare('SELECT value FROM team_meta WHERE id=?').get('schema_version')?.value;
592
+ const row = db.prepare('SELECT value FROM team_meta WHERE id=?').get('audit-checkpoint');
593
+ if (version === '1' && !row)
594
+ return null;
595
+ if (version !== '2' || !row)
596
+ throw new TeamError('integrity');
597
+ try {
598
+ return teamAuditCheckpointSchema.parse(JSON.parse(sqlText(row.value)));
599
+ }
600
+ catch {
601
+ throw new TeamError('integrity');
602
+ }
603
+ }
604
+ capacity() {
605
+ const db = this.agent.getDatabase();
606
+ const usage = (table, limit, bytesLimit) => {
607
+ const row = db.prepare(`SELECT count(*) AS entries,coalesce(sum(length(CAST(value AS BLOB))),0) AS bytes FROM ${table}`).get();
608
+ const entries = Number(row?.entries), bytes = Number(row?.bytes);
609
+ return { entries, bytes, limit, bytes_limit: bytesLimit, remaining_entries: Math.max(0, limit - entries), remaining_bytes: Math.max(0, bytesLimit - bytes), status: entries >= limit || bytes >= bytesLimit ? 'exhausted' : entries >= limit * 0.8 || bytes >= bytesLimit * 0.8 ? 'near-limit' : 'available' };
610
+ };
611
+ return { records: usage('team_records', 32768, MAX_RECORD_BYTES), audit: usage('team_audit', 99000, MAX_AUDIT_BYTES), local_audit_reserve: { entries: 1000, bytes: 256 * 1024 }, checkpoint: this.auditCheckpoint() };
612
+ }
613
+ async exportAudit(input) {
614
+ const backup = verifyTeamSnapshot(input);
615
+ return this.transaction(async () => {
616
+ const current = this.collectSnapshot();
617
+ if (current.digest !== backup.digest)
618
+ throw new TeamError('conflict');
619
+ const body = { schema_version: 1, organization: this.organization, backup_digest: backup.digest, previous_checkpoint: this.auditCheckpoint(), entries: current.audit };
620
+ return verifyTeamAuditArchive({ ...body, digest: teamDigest(JSON.stringify(body)) });
621
+ });
622
+ }
623
+ async archiveAudit(input, backupInput, confirm, apply = false) {
624
+ const archive = verifyTeamAuditArchive(input), backup = verifyTeamSnapshot(backupInput);
625
+ if (archive.digest !== confirm || archive.organization !== this.organization || archive.backup_digest !== backup.digest)
626
+ throw new TeamError('conflict');
584
627
  return this.transaction(async () => {
585
- await this.appendAudit('', 'local-operator', 'local-os', 'snapshot', 'accepted');
628
+ const current = this.collectSnapshot();
629
+ if (current.digest !== backup.digest || JSON.stringify(current.audit) !== JSON.stringify(archive.entries) || JSON.stringify(this.auditCheckpoint()) !== JSON.stringify(archive.previous_checkpoint))
630
+ throw new TeamError('conflict');
631
+ const last = archive.entries[archive.entries.length - 1];
632
+ if (apply) {
633
+ const db = this.agent.getDatabase();
634
+ const checkpoint = { sequence: last.sequence, digest: last.digest, archive_digest: archive.digest, backup_digest: backup.digest, previous_archive_digest: archive.previous_checkpoint?.archive_digest ?? null };
635
+ db.prepare('INSERT INTO team_meta(id,value) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value').run('audit-checkpoint', JSON.stringify(checkpoint));
636
+ db.prepare('UPDATE team_meta SET value=? WHERE id=?').run('2', 'schema_version');
637
+ db.exec('DELETE FROM team_audit');
638
+ await this.appendAudit('', 'local-operator', 'local-os', 'audit-archive', 'accepted', archive.digest);
639
+ }
640
+ return { applied: apply, events: archive.entries.length, archive_digest: archive.digest, backup_digest: backup.digest, last_sequence: last.sequence };
641
+ });
642
+ }
643
+ async snapshot(options = {}) {
644
+ return this.transaction(async () => {
645
+ if (options.audit !== false)
646
+ await this.appendAudit('', 'local-operator', 'local-os', 'snapshot', 'accepted');
586
647
  return this.collectSnapshot();
587
648
  });
588
649
  }
@@ -593,7 +654,7 @@ export class TeamStore {
593
654
  const snapshot = this.collectSnapshot();
594
655
  // Report only a count. Never select a restore, expose paths or delete remnants.
595
656
  const remnants = readdirSync(dirname(this.root)).filter(name => name.startsWith('.team-restore-') || name === `.${basename(this.root)}.restore.lock`).length;
596
- return { ok: true, records: snapshot.records.length, audit_events: snapshot.audit.length, digest: snapshot.digest, temporary_remnants: remnants };
657
+ return { ok: true, records: snapshot.records.length, audit_events: snapshot.audit.length, digest: snapshot.digest, temporary_remnants: remnants, capacity: this.capacity() };
597
658
  });
598
659
  }
599
660
  static async restore(root, input, confirm) {
@@ -629,6 +690,10 @@ export class TeamStore {
629
690
  const value = row.kind === 'credential' ? { ...valid(credentialSchema, row.value), revoked: true } : row.value;
630
691
  db.prepare('INSERT INTO team_records(organization,project,kind,id,value) VALUES(?,?,?,?,?)').run(row.organization, row.project, row.kind, row.id, JSON.stringify(value));
631
692
  }
693
+ if (snapshot.schema_version === 2) {
694
+ db.prepare('UPDATE team_meta SET value=? WHERE id=?').run('2', 'schema_version');
695
+ db.prepare('INSERT INTO team_meta(id,value) VALUES(?,?)').run('audit-checkpoint', JSON.stringify(snapshot.audit_checkpoint));
696
+ }
632
697
  for (const row of snapshot.audit)
633
698
  db.prepare('INSERT INTO team_audit(sequence,organization,project,value,digest) VALUES(?,?,?,?,?)').run(row.sequence, row.organization, row.project, row.value, row.digest);
634
699
  await store.appendAudit('', 'local-operator', 'local-os', 'restore', 'accepted', snapshot.digest);
@@ -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-assurance.mjs",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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|audit-export|audit-verify|audit-archive|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
@@ -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 audit-export audit-verify audit-archive doctor backup restore memory-delete --backup --previous --page-size --cursor --read-only --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 audit-export audit-verify audit-archive doctor backup restore memory-delete '--previous[previous archive segment]:file:_files' '--backup[verified backup file]:file:_files' '--page-size[page bound]:value:' '--cursor[continuation]:value:' '--read-only[observe-only backup]' '--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,12 @@ 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 audit-export audit-verify audit-archive doctor backup restore memory-delete"
226
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l previous -r
227
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l backup -r
228
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l page-size -r
229
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l cursor -r
230
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l read-only
226
231
  complete -c cairn -n "__fish_seen_subcommand_from team" -l organization -r
227
232
  complete -c cairn -n "__fish_seen_subcommand_from team" -l data -r
228
233
  complete -c cairn -n "__fish_seen_subcommand_from team" -l project -r
@@ -239,6 +244,14 @@ complete -c cairn -n "__fish_seen_subcommand_from team" -l proposal -r
239
244
  complete -c cairn -n "__fish_seen_subcommand_from team" -l decision -r
240
245
  complete -c cairn -n "__fish_seen_subcommand_from team" -l revision -r
241
246
  complete -c cairn -n "__fish_seen_subcommand_from team" -l project-root -r
247
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l connection -r
248
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l client
249
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l url -r
250
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l token-file -r
251
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l harness -r -a "claude opencode qwen codex"
252
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l access -r -a "read-only full"
253
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l apply
254
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l yes
242
255
  complete -c cairn -n "__fish_seen_subcommand_from trajectory" -a "list show prune"
243
256
  complete -c cairn -n "__fish_seen_subcommand_from artifact" -a "list show delete prune"
244
257
  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;