cleargate 0.7.0 → 0.8.1

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/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { Command } from "commander";
14
14
  // package.json
15
15
  var package_default = {
16
16
  name: "cleargate",
17
- version: "0.7.0",
17
+ version: "0.8.1",
18
18
  private: false,
19
19
  type: "module",
20
20
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
@@ -1423,9 +1423,16 @@ function mergeMcpJson(existing, entry) {
1423
1423
  next.mcpServers = servers;
1424
1424
  return JSON.stringify(next, null, 2) + "\n";
1425
1425
  }
1426
- function injectMcpJson(cwd, url) {
1426
+ function buildStdioEntry(pinVersion) {
1427
+ return {
1428
+ command: "npx",
1429
+ args: ["-y", `cleargate@${pinVersion}`, "mcp", "serve"]
1430
+ };
1431
+ }
1432
+ var STDIO_ENTRY_DEFAULT = buildStdioEntry("latest");
1433
+ function injectMcpJson(cwd, pinVersion = "latest") {
1427
1434
  const dst = path5.join(cwd, ".mcp.json");
1428
- const entry = { type: "http", url };
1435
+ const entry = buildStdioEntry(pinVersion);
1429
1436
  let existing = null;
1430
1437
  let existingRaw = null;
1431
1438
  if (fs6.existsSync(dst)) {
@@ -2518,7 +2525,7 @@ async function initHandler(opts = {}) {
2518
2525
  `);
2519
2526
  }
2520
2527
  try {
2521
- const action = injectMcpJson(cwd, "https://cleargate-mcp.soula.ge/mcp");
2528
+ const action = injectMcpJson(cwd, pinVersion ?? "latest");
2522
2529
  if (action === "created") {
2523
2530
  stdout(
2524
2531
  `[cleargate init] Created .mcp.json (cleargate MCP server registered) \u2014 restart Claude Code to load it.
@@ -7335,7 +7342,7 @@ function extractFrontmatterBlock(raw) {
7335
7342
  // src/lib/intake.ts
7336
7343
  async function runIntakeBranch(opts) {
7337
7344
  const {
7338
- mcp,
7345
+ mcp: mcp2,
7339
7346
  identity,
7340
7347
  sprintRoot,
7341
7348
  projectRoot,
@@ -7346,7 +7353,7 @@ async function runIntakeBranch(opts) {
7346
7353
  const pendingSyncDir = path36.join(projectRoot, ".cleargate", "delivery", "pending-sync");
7347
7354
  let remoteItems = [];
7348
7355
  try {
7349
- remoteItems = await mcp.call(
7356
+ remoteItems = await mcp2.call(
7350
7357
  "cleargate_detect_new_items",
7351
7358
  { label: labelFilter }
7352
7359
  );
@@ -7705,9 +7712,9 @@ async function syncCheckHandler(opts = {}) {
7705
7712
  }
7706
7713
  } catch {
7707
7714
  }
7708
- let mcp;
7715
+ let mcp2;
7709
7716
  if (opts.mcp) {
7710
- mcp = opts.mcp;
7717
+ mcp2 = opts.mcp;
7711
7718
  } else {
7712
7719
  let baseUrl = env["CLEARGATE_MCP_URL"];
7713
7720
  if (!baseUrl || !baseUrl.trim()) {
@@ -7736,10 +7743,10 @@ async function syncCheckHandler(opts = {}) {
7736
7743
  await emitError("adapter-not-configured", nowIso);
7737
7744
  return;
7738
7745
  }
7739
- mcp = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
7746
+ mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
7740
7747
  }
7741
7748
  try {
7742
- const adapterInfo = await mcp.adapterInfo();
7749
+ const adapterInfo = await mcp2.adapterInfo();
7743
7750
  if (!adapterInfo.configured || adapterInfo.name === "no-adapter-configured") {
7744
7751
  await emitError("adapter-not-configured", nowIso);
7745
7752
  return;
@@ -7748,7 +7755,7 @@ async function syncCheckHandler(opts = {}) {
7748
7755
  }
7749
7756
  let refs;
7750
7757
  try {
7751
- refs = await mcp.call("cleargate_list_remote_updates", { since });
7758
+ refs = await mcp2.call("cleargate_list_remote_updates", { since });
7752
7759
  } catch (err) {
7753
7760
  const msg = err instanceof Error ? err.message : String(err);
7754
7761
  await emitError(msg, nowIso);
@@ -7768,9 +7775,9 @@ async function syncHandler(opts = {}) {
7768
7775
  const identity = resolveIdentity(projectRoot);
7769
7776
  const sprintRoot = resolveActiveSprintDir(projectRoot);
7770
7777
  const sprintId = path40.basename(sprintRoot);
7771
- let mcp;
7778
+ let mcp2;
7772
7779
  if (opts.mcp) {
7773
- mcp = opts.mcp;
7780
+ mcp2 = opts.mcp;
7774
7781
  } else {
7775
7782
  let baseUrl = env["CLEARGATE_MCP_URL"];
7776
7783
  if (!baseUrl || !baseUrl.trim()) {
@@ -7813,11 +7820,11 @@ async function syncHandler(opts = {}) {
7813
7820
  exit(2);
7814
7821
  return;
7815
7822
  }
7816
- mcp = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
7823
+ mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
7817
7824
  }
7818
7825
  let adapterInfo;
7819
7826
  try {
7820
- adapterInfo = await mcp.adapterInfo();
7827
+ adapterInfo = await mcp2.adapterInfo();
7821
7828
  } catch {
7822
7829
  adapterInfo = { configured: true, name: "unknown" };
7823
7830
  }
@@ -7838,13 +7845,13 @@ async function syncHandler(opts = {}) {
7838
7845
  }
7839
7846
  } catch {
7840
7847
  }
7841
- const remoteRefs = await mcp.call(
7848
+ const remoteRefs = await mcp2.call(
7842
7849
  "cleargate_list_remote_updates",
7843
7850
  { since: lastRemoteSync }
7844
7851
  );
7845
7852
  const pulled = [];
7846
7853
  for (const ref of remoteRefs) {
7847
- const item = await mcp.call(
7854
+ const item = await mcp2.call(
7848
7855
  "cleargate_pull_item",
7849
7856
  { remote_id: ref.remote_id }
7850
7857
  );
@@ -7856,7 +7863,7 @@ async function syncHandler(opts = {}) {
7856
7863
  let intakeResult = { created: 0, items: [] };
7857
7864
  try {
7858
7865
  intakeResult = await runIntakeBranch({
7859
- mcp,
7866
+ mcp: mcp2,
7860
7867
  identity,
7861
7868
  sprintRoot,
7862
7869
  projectRoot,
@@ -7882,7 +7889,7 @@ async function syncHandler(opts = {}) {
7882
7889
  const activeSet = await resolveActiveItems(projectRoot, localRefs, nowFn);
7883
7890
  for (const remoteId of activeSet) {
7884
7891
  try {
7885
- const comments = await mcp.call(
7892
+ const comments = await mcp2.call(
7886
7893
  "cleargate_pull_comments",
7887
7894
  { remote_id: remoteId }
7888
7895
  );
@@ -8036,7 +8043,7 @@ async function syncHandler(opts = {}) {
8036
8043
  await appendSyncLog(sprintRoot, entry);
8037
8044
  }
8038
8045
  for (const { localPath, fm, body, itemId } of pushQueue) {
8039
- await mcp.call("push_item", {
8046
+ await mcp2.call("push_item", {
8040
8047
  cleargate_id: itemId,
8041
8048
  type: typeof fm["story_id"] === "string" ? "story" : typeof fm["epic_id"] === "string" ? "epic" : typeof fm["proposal_id"] === "string" ? "proposal" : "story",
8042
8049
  payload: fm
@@ -8166,9 +8173,9 @@ async function pullHandler(idOrRemoteId, opts = {}) {
8166
8173
  const nowFn = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
8167
8174
  const identity = resolveIdentity(projectRoot);
8168
8175
  const sprintRoot = resolveActiveSprintDir(projectRoot);
8169
- let mcp;
8176
+ let mcp2;
8170
8177
  if (opts.mcp) {
8171
- mcp = opts.mcp;
8178
+ mcp2 = opts.mcp;
8172
8179
  } else {
8173
8180
  let baseUrl = env["CLEARGATE_MCP_URL"];
8174
8181
  if (!baseUrl || !baseUrl.trim()) {
@@ -8203,7 +8210,7 @@ async function pullHandler(idOrRemoteId, opts = {}) {
8203
8210
  exit(2);
8204
8211
  return;
8205
8212
  }
8206
- mcp = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
8213
+ mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
8207
8214
  }
8208
8215
  const remoteId = await resolveRemoteId(idOrRemoteId, projectRoot);
8209
8216
  if (!remoteId) {
@@ -8212,7 +8219,7 @@ async function pullHandler(idOrRemoteId, opts = {}) {
8212
8219
  exit(1);
8213
8220
  return;
8214
8221
  }
8215
- const remoteItem = await mcp.call("cleargate_pull_item", { remote_id: remoteId });
8222
+ const remoteItem = await mcp2.call("cleargate_pull_item", { remote_id: remoteId });
8216
8223
  if (!remoteItem) {
8217
8224
  stderr(`Error: item ${remoteId} not found on MCP server.
8218
8225
  `);
@@ -8272,7 +8279,7 @@ async function pullHandler(idOrRemoteId, opts = {}) {
8272
8279
  stdout(`pull: ${remoteId} applied to ${path41.relative(projectRoot, localPath)}
8273
8280
  `);
8274
8281
  if (opts.comments) {
8275
- const comments = await mcp.call(
8282
+ const comments = await mcp2.call(
8276
8283
  "cleargate_pull_comments",
8277
8284
  { remote_id: remoteId }
8278
8285
  );
@@ -8467,10 +8474,10 @@ async function handlePush(filePath, ctx) {
8467
8474
  if (h1) payloadForPush["title"] = h1;
8468
8475
  }
8469
8476
  payloadForPush["body"] = body;
8470
- const mcp = await resolveMcp();
8477
+ const mcp2 = await resolveMcp();
8471
8478
  let result;
8472
8479
  try {
8473
- result = await mcp.call("push_item", {
8480
+ result = await mcp2.call("push_item", {
8474
8481
  cleargate_id: itemId,
8475
8482
  type,
8476
8483
  payload: payloadForPush,
@@ -8522,9 +8529,9 @@ async function handleRevert(idOrRemoteId, ctx) {
8522
8529
  exit(1);
8523
8530
  return;
8524
8531
  }
8525
- const mcp = await resolveMcp();
8532
+ const mcp2 = await resolveMcp();
8526
8533
  try {
8527
- await mcp.call("sync_status", {
8534
+ await mcp2.call("sync_status", {
8528
8535
  cleargate_id: itemId,
8529
8536
  new_status: "archived-without-shipping"
8530
8537
  });
@@ -8957,6 +8964,214 @@ function hotfixNewHandler(opts, cli) {
8957
8964
  return exitFn(0);
8958
8965
  }
8959
8966
 
8967
+ // src/commands/mcp-serve.ts
8968
+ import * as readline5 from "readline";
8969
+
8970
+ // src/auth/refresh.ts
8971
+ async function refreshAccessToken(baseUrl, refreshToken, deps = {}) {
8972
+ const fetchFn = deps.fetch ?? globalThis.fetch;
8973
+ const res = await fetchFn(`${baseUrl}/auth/refresh`, {
8974
+ method: "POST",
8975
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
8976
+ body: JSON.stringify({ refresh_token: refreshToken })
8977
+ });
8978
+ if (!res.ok) {
8979
+ const body = await res.json().catch(() => ({}));
8980
+ throw new RefreshError(res.status, body.error ?? "unknown_error");
8981
+ }
8982
+ const json = await res.json();
8983
+ if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
8984
+ throw new RefreshError(500, "malformed_response");
8985
+ }
8986
+ return json;
8987
+ }
8988
+ var RefreshError = class extends Error {
8989
+ constructor(status, code) {
8990
+ super(`refresh failed: ${status} ${code}`);
8991
+ this.status = status;
8992
+ this.code = code;
8993
+ this.name = "RefreshError";
8994
+ }
8995
+ status;
8996
+ code;
8997
+ };
8998
+ var AuthFetcher = class {
8999
+ constructor(opts) {
9000
+ this.opts = opts;
9001
+ }
9002
+ opts;
9003
+ accessToken = null;
9004
+ accessExpiresAt = 0;
9005
+ inflight = null;
9006
+ /** Returns a fresh access token, refreshing if needed. */
9007
+ async getAccessToken() {
9008
+ const now = (this.opts.now ?? (() => Date.now()))();
9009
+ const skewMs = (this.opts.skewSeconds ?? 60) * 1e3;
9010
+ if (this.accessToken && now < this.accessExpiresAt - skewMs) {
9011
+ return this.accessToken;
9012
+ }
9013
+ if (this.inflight) return this.inflight;
9014
+ this.inflight = this.refreshNow().finally(() => {
9015
+ this.inflight = null;
9016
+ });
9017
+ return this.inflight;
9018
+ }
9019
+ /** Force the next call to refresh. Used after a 401. */
9020
+ invalidate() {
9021
+ this.accessToken = null;
9022
+ this.accessExpiresAt = 0;
9023
+ }
9024
+ async refreshNow() {
9025
+ const stored = await this.opts.loadRefresh();
9026
+ if (!stored) {
9027
+ throw new RefreshError(401, "no_refresh_token");
9028
+ }
9029
+ const exchanged = await refreshAccessToken(this.opts.baseUrl, stored, {
9030
+ ...this.opts.fetch ? { fetch: this.opts.fetch } : {},
9031
+ ...this.opts.now ? { now: this.opts.now } : {}
9032
+ });
9033
+ await this.opts.saveRefresh(exchanged.refresh_token);
9034
+ const now = (this.opts.now ?? (() => Date.now()))();
9035
+ this.accessToken = exchanged.access_token;
9036
+ this.accessExpiresAt = now + exchanged.expires_in * 1e3;
9037
+ return exchanged.access_token;
9038
+ }
9039
+ };
9040
+
9041
+ // src/commands/mcp-serve.ts
9042
+ var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
9043
+ async function mcpServeHandler(opts) {
9044
+ const fetchFn = opts.fetch ?? globalThis.fetch;
9045
+ const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
9046
+ const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
9047
+ const exit = opts.exit ?? ((c) => process.exit(c));
9048
+ const cfg = loadConfig({
9049
+ flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
9050
+ });
9051
+ const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
9052
+ const store = await (opts.createStore ?? createTokenStore)({
9053
+ ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
9054
+ ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
9055
+ });
9056
+ const fetcher = new AuthFetcher({
9057
+ baseUrl,
9058
+ loadRefresh: () => store.load(opts.profile),
9059
+ saveRefresh: (t) => store.save(opts.profile, t),
9060
+ ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
9061
+ ...opts.now !== void 0 ? { now: opts.now } : {}
9062
+ });
9063
+ try {
9064
+ await fetcher.getAccessToken();
9065
+ } catch (err) {
9066
+ if (err instanceof RefreshError) {
9067
+ stderr(
9068
+ `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
9069
+ `
9070
+ );
9071
+ } else {
9072
+ stderr(
9073
+ `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
9074
+ `
9075
+ );
9076
+ }
9077
+ return exit(1);
9078
+ }
9079
+ const inputStream = opts.stdin ?? process.stdin;
9080
+ const rl = readline5.createInterface({
9081
+ input: inputStream,
9082
+ output: void 0,
9083
+ terminal: false
9084
+ });
9085
+ for await (const line of rl) {
9086
+ if (!line.trim()) continue;
9087
+ try {
9088
+ await proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr);
9089
+ } catch (err) {
9090
+ const errMsg = err instanceof Error ? err.message : String(err);
9091
+ stderr(`cleargate mcp serve: proxy error: ${errMsg}
9092
+ `);
9093
+ const id = extractId(line);
9094
+ if (id !== void 0) {
9095
+ stdout(
9096
+ JSON.stringify({
9097
+ jsonrpc: "2.0",
9098
+ id,
9099
+ error: { code: -32603, message: `proxy error: ${errMsg}` }
9100
+ }) + "\n"
9101
+ );
9102
+ }
9103
+ }
9104
+ }
9105
+ }
9106
+ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
9107
+ let parsed;
9108
+ try {
9109
+ parsed = JSON.parse(line);
9110
+ } catch {
9111
+ stderr(`cleargate mcp serve: ignoring non-JSON line: ${line.slice(0, 80)}
9112
+ `);
9113
+ return;
9114
+ }
9115
+ const isNotification = !("id" in parsed) || parsed.id === void 0 || parsed.id === null;
9116
+ let access = await fetcher.getAccessToken();
9117
+ let res = await postFrame(baseUrl, line, access, fetchFn);
9118
+ if (res.status === 401) {
9119
+ fetcher.invalidate();
9120
+ access = await fetcher.getAccessToken();
9121
+ res = await postFrame(baseUrl, line, access, fetchFn);
9122
+ }
9123
+ if (isNotification) {
9124
+ await res.arrayBuffer().catch(() => void 0);
9125
+ return;
9126
+ }
9127
+ const ct = res.headers.get("content-type") ?? "";
9128
+ if (ct.includes("text/event-stream")) {
9129
+ await streamSse(res, stdout);
9130
+ } else {
9131
+ const text = await res.text();
9132
+ if (text.length > 0) stdout(text + "\n");
9133
+ }
9134
+ }
9135
+ async function postFrame(baseUrl, body, accessToken, fetchFn) {
9136
+ return fetchFn(`${baseUrl}/mcp`, {
9137
+ method: "POST",
9138
+ headers: {
9139
+ "Content-Type": "application/json",
9140
+ Accept: "application/json, text/event-stream",
9141
+ Authorization: `Bearer ${accessToken}`
9142
+ },
9143
+ body
9144
+ });
9145
+ }
9146
+ async function streamSse(res, stdout) {
9147
+ if (!res.body) return;
9148
+ const reader = res.body.getReader();
9149
+ const decoder = new TextDecoder("utf-8");
9150
+ let buf = "";
9151
+ for (; ; ) {
9152
+ const { value, done } = await reader.read();
9153
+ if (done) break;
9154
+ buf += decoder.decode(value, { stream: true });
9155
+ let nl;
9156
+ while ((nl = buf.indexOf("\n")) !== -1) {
9157
+ const ln = buf.slice(0, nl);
9158
+ buf = buf.slice(nl + 1);
9159
+ if (ln.startsWith("data:")) {
9160
+ const payload = ln.slice(5).trim();
9161
+ if (payload) stdout(payload + "\n");
9162
+ }
9163
+ }
9164
+ }
9165
+ }
9166
+ function extractId(line) {
9167
+ try {
9168
+ const obj = JSON.parse(line);
9169
+ return "id" in obj ? obj.id : void 0;
9170
+ } catch {
9171
+ return void 0;
9172
+ }
9173
+ }
9174
+
8960
9175
  // src/cli.ts
8961
9176
  var program = new Command();
8962
9177
  program.name("cleargate").description("ClearGate CLI \u2014 connects AI agent teams to the ClearGate MCP server").version(package_default.version, "-V, --version").option("--profile <name>", "configuration profile to use", "default").option("--mcp-url <url>", "MCP server URL (overrides config file and env)").showHelpAfterError("(use `cleargate --help`)");
@@ -9199,5 +9414,13 @@ var hotfix = program.command("hotfix").description("hotfix lane commands (off-sp
9199
9414
  hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
9200
9415
  hotfixNewHandler({ slug });
9201
9416
  });
9417
+ var mcp = program.command("mcp").description("MCP-server bridge commands (stdio shim, registration helpers)");
9418
+ mcp.command("serve").description("run a stdio MCP server that proxies to the cleargate HTTP /mcp endpoint with auto-refresh Bearer auth").action(async (_opts, command) => {
9419
+ const globals = command.parent.parent.opts();
9420
+ await mcpServeHandler({
9421
+ profile: globals.profile,
9422
+ ...globals.mcpUrl !== void 0 ? { mcpUrlFlag: globals.mcpUrl } : {}
9423
+ });
9424
+ });
9202
9425
  void program.parseAsync(process.argv);
9203
9426
  //# sourceMappingURL=cli.js.map