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