@cotal-ai/connector-claude-code 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @cotal-ai/connector-claude-code
2
+
3
+ The Claude Code adapter: a bundled, installed plugin plus `claude/channel` push that turns a
4
+ real `claude` session into a Cotal mesh peer. A thin client over
5
+ [`@cotal-ai/connector-core`](../connector-core).
6
+
7
+ **Tier:** `extensions/`. Peer-depends [`@cotal-ai/core`](../../packages/core); self-registers on
8
+ import.
9
+
10
+ See [docs/claude-code-integration.md](../../docs/claude-code-integration.md) for the full
11
+ integration, and the [root AGENTS.md](../../AGENTS.md) for the tier rules.
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2B,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAmB3G;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SA8D7B,CAAC"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAoB3G;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SA2F7B,CAAC"}
package/dist/extension.js CHANGED
@@ -1,6 +1,9 @@
1
- import { resolve } from "node:path";
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
2
4
  import { fileURLToPath } from "node:url";
3
5
  import { loadAgentFile, registry } from "@cotal-ai/core";
6
+ import { launchEnv, mcpServerEnvKeys } from "@cotal-ai/connector-core";
4
7
  /** Name the cotal MCP server is registered under via --mcp-config (see buildLaunch). */
5
8
  const MCP_SERVER_NAME = "cotal";
6
9
  /** Channel ref for `--dangerously-load-development-channels`, which turns on the cotal MCP server's
@@ -27,16 +30,25 @@ export const claudeConnector = {
27
30
  name: "claude",
28
31
  pluginRoot: PLUGIN_ROOT,
29
32
  buildLaunch(opts) {
33
+ // Operator MCP servers shared with this agent (default none — see the --mcp-config block).
34
+ const shared = opts.mcpServers ?? {};
35
+ // claude auths via macOS Keychain / an OAuth token, not an env key → forward NO provider key.
36
+ // The OS allow-list (PATH/HOME/TERM/…) is the only thing inherited from the manager env, plus
37
+ // — only when a shared server declares them via `${VAR}` — the named secrets it needs (mcpKeys,
38
+ // by name). The operator's unrelated secrets don't reach the child (P3).
30
39
  const env = {
40
+ ...launchEnv({ mcpKeys: mcpServerEnvKeys(shared) }),
31
41
  COTAL_SPACE: opts.space,
32
42
  COTAL_NAME: opts.name,
33
43
  // Force the connector to emit channel wake-nudges: Claude doesn't advertise the
34
44
  // `claude/channel` capability back over MCP, so auto-detection would see it "off".
35
45
  COTAL_CHANNEL: "1",
36
- // Managed sessions mirror their own transcript to `tr-<name>` so peers can read
37
- // what the agent actually did. Personal sessions (no buildLaunch) never mirror.
38
- COTAL_TRANSCRIPT: "1",
39
46
  };
47
+ // A session can mirror its own transcript to `tr-<name>` so peers can read what the
48
+ // agent actually did — OFF by default (transcripts are verbose and may carry sensitive
49
+ // content); `--transcript` (opts.transcript === true) opts in. Personal sessions never mirror.
50
+ if (opts.transcript === true)
51
+ env.COTAL_TRANSCRIPT = "1";
40
52
  if (opts.role)
41
53
  env.COTAL_ROLE = opts.role;
42
54
  if (opts.id)
@@ -54,13 +66,39 @@ export const claudeConnector = {
54
66
  // can look something up under `npx` (no repo on disk) without prompting the operator
55
67
  // mid-demo. Additive under the default permission mode — leaves other tools as-is.
56
68
  args.push("--allowedTools", "WebFetch(domain:github.com),WebFetch(domain:raw.githubusercontent.com)");
57
- // Isolate the spawned session's MCP to ONLY the cotal server. --strict-mcp-config drops every
58
- // other MCP source — including the operator's personal ~/.claude.json servers (e.g. a headless
59
- // Chromium, a DB server) that a meshed teammate never needs and that, multiplied across several
60
- // spawns on a busy machine, starve memory and kill the session before it registers presence —
61
- // and --mcp-config re-supplies cotal so its tools + presence still load. The plugin itself stays
62
- // enabled (its hooks + the dev-channels wake path are unaffected; only MCP config is scoped).
63
- args.push("--strict-mcp-config", "--mcp-config", JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: "node", args: [MCP_CJS] } } }));
69
+ // Isolate the spawned session's MCP. --strict-mcp-config drops every ambient MCP source —
70
+ // including the operator's personal ~/.claude.json servers (e.g. a headless Chromium, a DB
71
+ // server) that a meshed teammate never needs and that, multiplied across several spawns on a
72
+ // busy machine, starve memory and kill the session before it registers presence — so the ONLY
73
+ // servers that load are the ones we name in --mcp-config: cotal (always, for its tools +
74
+ // presence) plus any the operator explicitly opted to share (`shared`, from the cotal config).
75
+ // The plugin itself stays enabled (its hooks + the dev-channels wake path are unaffected).
76
+ // cotal is spread LAST so a shared server can never shadow the mesh server by reusing its name.
77
+ const mcpServers = { ...shared, [MCP_SERVER_NAME]: { command: "node", args: [MCP_CJS] } };
78
+ // Default (no shared servers): pass the config inline, unchanged. With shared servers, write it
79
+ // to a file instead and pass the path. Either way the secret stays a `${VAR}` reference (Claude
80
+ // expands it from the child env at launch — see the mcpKeys forwarding above), never the resolved
81
+ // value, so nothing secret reaches disk or argv. We prefer the file when sharing because env
82
+ // expansion is only *documented* for --mcp-config files (inline expansion does work today, but
83
+ // isn't contracted), and a file keeps a potentially multi-server config off the process argv.
84
+ // Verified end-to-end on claude 2.1.183: ${VAR} expands in the --mcp-config file and the value
85
+ // is handed to the shared server. This is host-version behavior — if a future claude stops
86
+ // expanding here, a shared server would receive a literal `${VAR}`; re-check on host upgrades.
87
+ let mcpConfig;
88
+ if (Object.keys(shared).length === 0) {
89
+ mcpConfig = JSON.stringify({ mcpServers });
90
+ }
91
+ else {
92
+ // A private 0700 temp dir (unique per spawn) holds the 0600 config. mkdtemp can't be raced
93
+ // by a pre-created or symlinked path the way a predictable name in the world-writable tmpdir
94
+ // could, and a fresh file guarantees the 0600 mode applies on creation (mode is ignored on an
95
+ // overwrite). Left for the OS to reap: the file must outlive this call (Claude reads it at
96
+ // startup and on /mcp reconnect), and buildLaunch doesn't own the child's lifecycle.
97
+ const dir = mkdtempSync(join(tmpdir(), "cotal-mcp-"));
98
+ mcpConfig = join(dir, "mcp.json");
99
+ writeFileSync(mcpConfig, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
100
+ }
101
+ args.push("--strict-mcp-config", "--mcp-config", mcpConfig);
64
102
  // An agent file carries identity (read in-session via COTAL_AGENT_FILE) plus
65
103
  // persona + model, which can only be applied to a `claude` session at launch.
66
104
  if (opts.configPath) {
@@ -1 +1 @@
1
- {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"}
package/dist/hook.cjs CHANGED
@@ -3504,16 +3504,16 @@ var require_errors = __commonJS({
3504
3504
  }
3505
3505
  };
3506
3506
  exports2.ProtocolError = ProtocolError;
3507
- var RequestError = class extends Error {
3507
+ var RequestError2 = class extends Error {
3508
3508
  constructor(message = "", options) {
3509
3509
  super(message, options);
3510
3510
  this.name = "RequestError";
3511
3511
  }
3512
3512
  isNoResponders() {
3513
- return this.cause instanceof NoRespondersError;
3513
+ return this.cause instanceof NoRespondersError2;
3514
3514
  }
3515
3515
  };
3516
- exports2.RequestError = RequestError;
3516
+ exports2.RequestError = RequestError2;
3517
3517
  var TimeoutError = class extends Error {
3518
3518
  constructor(options) {
3519
3519
  super("timeout", options);
@@ -3521,7 +3521,7 @@ var require_errors = __commonJS({
3521
3521
  }
3522
3522
  };
3523
3523
  exports2.TimeoutError = TimeoutError;
3524
- var NoRespondersError = class extends Error {
3524
+ var NoRespondersError2 = class extends Error {
3525
3525
  subject;
3526
3526
  constructor(subject, options) {
3527
3527
  super(`no responders: '${subject}'`, options);
@@ -3529,7 +3529,7 @@ var require_errors = __commonJS({
3529
3529
  this.name = "NoResponders";
3530
3530
  }
3531
3531
  };
3532
- exports2.NoRespondersError = NoRespondersError;
3532
+ exports2.NoRespondersError = NoRespondersError2;
3533
3533
  var PermissionViolationError2 = class _PermissionViolationError extends Error {
3534
3534
  operation;
3535
3535
  subject;
@@ -3572,10 +3572,10 @@ var require_errors = __commonJS({
3572
3572
  InvalidArgumentError,
3573
3573
  InvalidOperationError,
3574
3574
  InvalidSubjectError,
3575
- NoRespondersError,
3575
+ NoRespondersError: NoRespondersError2,
3576
3576
  PermissionViolationError: PermissionViolationError2,
3577
3577
  ProtocolError,
3578
- RequestError,
3578
+ RequestError: RequestError2,
3579
3579
  TimeoutError,
3580
3580
  UserAuthenticationExpiredError: UserAuthenticationExpiredError2
3581
3581
  };
@@ -13797,7 +13797,7 @@ var require_kv = __commonJS({
13797
13797
  throw new Error(`invalid bucket name: ${name}`);
13798
13798
  }
13799
13799
  }
13800
- var Kvm5 = class {
13800
+ var Kvm6 = class {
13801
13801
  js;
13802
13802
  /**
13803
13803
  * Creates an instance of the Kv that allows you to create and access KV stores.
@@ -13863,7 +13863,7 @@ var require_kv = __commonJS({
13863
13863
  return new internal_2.ListerImpl(subj, filter, this.js);
13864
13864
  }
13865
13865
  };
13866
- exports2.Kvm = Kvm5;
13866
+ exports2.Kvm = Kvm6;
13867
13867
  var Bucket = class _Bucket {
13868
13868
  js;
13869
13869
  jsm;
@@ -14708,6 +14708,56 @@ var require_mod6 = __commonJS({
14708
14708
  var import_node_os = require("node:os");
14709
14709
  var import_node_fs2 = require("node:fs");
14710
14710
 
14711
+ // ../../packages/core/dist/subjects.js
14712
+ function isConcreteChannel(channel) {
14713
+ return !channel.split(".").some((s) => s.trim() === "*" || s.trim() === ">");
14714
+ }
14715
+ function subjectMatches(pattern, subject) {
14716
+ const p = pattern.split(".");
14717
+ const s = subject.split(".");
14718
+ for (let i = 0; i < p.length; i++) {
14719
+ if (p[i] === ">")
14720
+ return i < s.length;
14721
+ if (i >= s.length)
14722
+ return false;
14723
+ if (p[i] === "*")
14724
+ continue;
14725
+ if (p[i] !== s[i])
14726
+ return false;
14727
+ }
14728
+ return p.length === s.length;
14729
+ }
14730
+ function assertValidChannel(channel) {
14731
+ const segs = channel.split(".");
14732
+ if (!channel.length || segs.some((s) => s.length === 0))
14733
+ throw new Error(`invalid channel "${channel}": empty segment (no leading/trailing/double dots)`);
14734
+ segs.forEach((s, i) => {
14735
+ if (s === ">") {
14736
+ if (i !== segs.length - 1)
14737
+ throw new Error(`invalid channel "${channel}": '>' is only valid as the last segment`);
14738
+ return;
14739
+ }
14740
+ if (s === "*")
14741
+ return;
14742
+ if (!/^[A-Za-z0-9_-]+$/.test(s))
14743
+ throw new Error(`invalid channel "${channel}": segment "${s}" must be a NATS-safe token ([A-Za-z0-9_-]), '*', or '>' \u2014 policy channel names can't contain characters the wire layer would rewrite`);
14744
+ });
14745
+ return channel;
14746
+ }
14747
+ function channelInAllow(allow, channel) {
14748
+ return allow.some((a) => subjectMatches(a, channel));
14749
+ }
14750
+
14751
+ // ../../packages/core/dist/resolve.js
14752
+ function assertValidName(name) {
14753
+ if (name.length === 0 || name !== name.trim())
14754
+ throw new Error(`invalid name ${JSON.stringify(name)}: must be non-empty with no surrounding whitespace`);
14755
+ if (/[\r\n]/.test(name))
14756
+ throw new Error(`invalid name ${JSON.stringify(name)}: must be a single line`);
14757
+ if (name.includes("/"))
14758
+ throw new Error(`invalid name ${JSON.stringify(name)}: "/" is reserved (the owner/name separator)`);
14759
+ }
14760
+
14711
14761
  // ../../packages/core/dist/link.js
14712
14762
  function parseJoinLink(link) {
14713
14763
  const tls = link.startsWith("cotals://");
@@ -16364,11 +16414,15 @@ var SYS_LIMITS = { ...BASE_LIMITS, mem_storage: 0, disk_storage: 0 };
16364
16414
  var import_jetstream = __toESM(require_mod4(), 1);
16365
16415
  var import_transport_node = __toESM(require_transport_node(), 1);
16366
16416
  var import_kv = __toESM(require_mod6(), 1);
16417
+ var PLANE3_DEDUP_WINDOW_MS = 2 * 60 * 60 * 1e3;
16367
16418
 
16368
16419
  // ../../packages/core/dist/channels.js
16369
16420
  var import_kv2 = __toESM(require_mod6(), 1);
16370
16421
  var import_transport_node2 = __toESM(require_transport_node(), 1);
16371
16422
 
16423
+ // ../../packages/core/dist/members.js
16424
+ var import_kv3 = __toESM(require_mod6(), 1);
16425
+
16372
16426
  // ../../packages/core/dist/agent-file.js
16373
16427
  var import_node_fs = require("node:fs");
16374
16428
  function unquote(v) {
@@ -16419,10 +16473,45 @@ function loadAgentFile(path) {
16419
16473
  const name = str("name");
16420
16474
  if (!name)
16421
16475
  throw new Error(`agent file ${path}: "name" is required`);
16476
+ assertValidName(name);
16422
16477
  const kind = str("kind");
16423
16478
  if (kind && kind !== "agent" && kind !== "endpoint")
16424
16479
  throw new Error(`agent file ${path}: "kind" must be "agent" or "endpoint"`);
16425
- const known = /* @__PURE__ */ new Set(["name", "role", "kind", "description", "tags", "channels", "publish", "model"]);
16480
+ for (const old of ["channels", "publish"])
16481
+ if (old in fm)
16482
+ throw new Error(`agent file ${path}: "${old}" was renamed \u2014 use "subscribe"/"allowSubscribe" (read) and "allowPublish" (post)`);
16483
+ const subscribe = list("subscribe");
16484
+ const allowSubscribe = list("allowSubscribe");
16485
+ const allowPublish = list("allowPublish");
16486
+ const quiet = list("quiet");
16487
+ const muted = list("muted");
16488
+ for (const ch of [...subscribe ?? [], ...allowSubscribe ?? [], ...allowPublish ?? []])
16489
+ try {
16490
+ assertValidChannel(ch);
16491
+ } catch (e) {
16492
+ throw new Error(`agent file ${path}: ${e.message}`);
16493
+ }
16494
+ const effSubscribe = subscribe?.length ? subscribe : ["general"];
16495
+ const effAllow = allowSubscribe?.length ? allowSubscribe : effSubscribe;
16496
+ for (const ch of effSubscribe)
16497
+ if (!channelInAllow(effAllow, ch))
16498
+ throw new Error(`agent file ${path}: subscribe channel "${ch}" is not within allowSubscribe [${effAllow.join(", ")}]`);
16499
+ const both = (quiet ?? []).filter((c) => (muted ?? []).includes(c));
16500
+ if (both.length)
16501
+ throw new Error(`agent file ${path}: channel(s) [${both.join(", ")}] are in both quiet and muted \u2014 pick one`);
16502
+ for (const [field, chans] of [["quiet", quiet], ["muted", muted]])
16503
+ for (const ch of chans ?? []) {
16504
+ try {
16505
+ assertValidChannel(ch);
16506
+ } catch (e) {
16507
+ throw new Error(`agent file ${path}: ${e.message}`);
16508
+ }
16509
+ if (!isConcreteChannel(ch))
16510
+ throw new Error(`agent file ${path}: ${field} channel "${ch}" must be a concrete channel (no wildcard)`);
16511
+ if (!channelInAllow(effAllow, ch))
16512
+ throw new Error(`agent file ${path}: ${field} channel "${ch}" is not within your read ACL / allowSubscribe [${effAllow.join(", ")}]`);
16513
+ }
16514
+ const known = /* @__PURE__ */ new Set(["name", "role", "kind", "description", "tags", "subscribe", "allowSubscribe", "allowPublish", "quiet", "muted", "model", "capabilities", "owner"]);
16426
16515
  const meta = {};
16427
16516
  for (const [k, v] of Object.entries(fm))
16428
16517
  if (!known.has(k) && typeof v === "string")
@@ -16433,9 +16522,14 @@ function loadAgentFile(path) {
16433
16522
  kind,
16434
16523
  description: str("description"),
16435
16524
  tags: list("tags"),
16436
- channels: list("channels"),
16437
- publish: list("publish"),
16525
+ subscribe,
16526
+ allowSubscribe,
16527
+ allowPublish,
16528
+ quiet,
16529
+ muted,
16438
16530
  model: str("model"),
16531
+ capabilities: list("capabilities"),
16532
+ owner: str("owner"),
16439
16533
  meta: Object.keys(meta).length ? meta : void 0,
16440
16534
  persona: persona || void 0
16441
16535
  };
@@ -16444,13 +16538,13 @@ function loadAgentFile(path) {
16444
16538
  // ../../packages/core/dist/endpoint.js
16445
16539
  var import_transport_node3 = __toESM(require_transport_node(), 1);
16446
16540
  var import_jetstream2 = __toESM(require_mod4(), 1);
16447
- var import_kv3 = __toESM(require_mod6(), 1);
16541
+ var import_kv4 = __toESM(require_mod6(), 1);
16448
16542
  var DEFAULT_SERVER = "nats://127.0.0.1:4222";
16449
16543
 
16450
16544
  // ../../packages/core/dist/spaces.js
16451
16545
  var import_transport_node4 = __toESM(require_transport_node(), 1);
16452
16546
  var import_jetstream3 = __toESM(require_mod4(), 1);
16453
- var import_kv4 = __toESM(require_mod6(), 1);
16547
+ var import_kv5 = __toESM(require_mod6(), 1);
16454
16548
 
16455
16549
  // ../../packages/core/dist/registry.js
16456
16550
  var Registry = class {
@@ -16493,9 +16587,31 @@ function configFromEnv(env = process.env) {
16493
16587
  const name = env.COTAL_NAME?.trim() || def?.name || (link ? (0, import_node_os.userInfo)().username : void 0);
16494
16588
  if (!name)
16495
16589
  throw new Error("COTAL_NAME, COTAL_AGENT_FILE or COTAL_LINK is required \u2014 a Cotal session needs an explicit identity from its launcher");
16496
- const channels = splitList(env.COTAL_CHANNELS);
16497
- const resolvedChannels = channels.length ? channels : def?.channels ?? link?.channels ?? ["general"];
16498
- const publish = splitList(env.COTAL_PUBLISH);
16590
+ const subscribe = splitList(env.COTAL_SUBSCRIBE);
16591
+ const resolvedSubscribe = subscribe.length ? subscribe : def?.subscribe ?? link?.channels ?? ["general"];
16592
+ const allowSub = splitList(env.COTAL_ALLOW_SUBSCRIBE);
16593
+ const resolvedAllowSub = allowSub.length ? allowSub : def?.allowSubscribe ?? resolvedSubscribe;
16594
+ for (const ch of resolvedSubscribe)
16595
+ if (!channelInAllow(resolvedAllowSub, ch))
16596
+ throw new Error(`COTAL config: subscribe channel "${ch}" is not within allowSubscribe [${resolvedAllowSub.join(", ")}]`);
16597
+ const allowPub = splitList(env.COTAL_ALLOW_PUBLISH);
16598
+ const resolvedAllowPub = allowPub.length ? allowPub : def?.allowPublish ?? [];
16599
+ for (const ch of [...resolvedSubscribe, ...resolvedAllowSub, ...resolvedAllowPub])
16600
+ assertValidChannel(ch);
16601
+ const qEnv = splitList(env.COTAL_QUIET), mEnv = splitList(env.COTAL_MUTED);
16602
+ const resolvedQuiet = qEnv.length ? qEnv : def?.quiet ?? [];
16603
+ const resolvedMuted = mEnv.length ? mEnv : def?.muted ?? [];
16604
+ const bothModes = resolvedQuiet.filter((c) => resolvedMuted.includes(c));
16605
+ if (bothModes.length)
16606
+ throw new Error(`COTAL config: channel(s) [${bothModes.join(", ")}] are in both quiet and muted`);
16607
+ for (const [field, chans] of [["quiet", resolvedQuiet], ["muted", resolvedMuted]])
16608
+ for (const ch of chans) {
16609
+ assertValidChannel(ch);
16610
+ if (!isConcreteChannel(ch))
16611
+ throw new Error(`COTAL config: ${field} channel "${ch}" must be concrete (no wildcard)`);
16612
+ if (!channelInAllow(resolvedAllowSub, ch))
16613
+ throw new Error(`COTAL config: ${field} channel "${ch}" is not within allowSubscribe [${resolvedAllowSub.join(", ")}]`);
16614
+ }
16499
16615
  const credsPath = env.COTAL_CREDS?.trim();
16500
16616
  return {
16501
16617
  space: env.COTAL_SPACE?.trim() || link?.space || "demo",
@@ -16505,9 +16621,17 @@ function configFromEnv(env = process.env) {
16505
16621
  role: env.COTAL_ROLE?.trim() || def?.role || void 0,
16506
16622
  description: def?.description,
16507
16623
  tags: def?.tags,
16624
+ meta: def?.meta,
16625
+ capabilities: def?.capabilities,
16626
+ model: env.COTAL_MODEL?.trim() || def?.model || void 0,
16508
16627
  servers: env.COTAL_SERVERS?.trim() || link?.servers || DEFAULT_SERVER,
16509
- channels: resolvedChannels,
16510
- publish: publish.length ? publish : def?.publish ?? resolvedChannels,
16628
+ subscribe: resolvedSubscribe,
16629
+ allowSubscribe: resolvedAllowSub,
16630
+ // Post ACL is default-DENY: only what's explicitly declared (env > agent-file). The broker
16631
+ // enforces it under auth; in open mode posting is unrestricted regardless (see laneLine).
16632
+ allowPublish: resolvedAllowPub,
16633
+ quiet: resolvedQuiet,
16634
+ muted: resolvedMuted,
16511
16635
  kind: env.COTAL_KIND?.trim() || def?.kind || "agent",
16512
16636
  token: env.COTAL_TOKEN?.trim() || link?.token,
16513
16637
  user: link?.user,