@archme/cli 0.8.2 → 0.9.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.
Files changed (2) hide show
  1. package/dist/cli.js +106 -42
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10399,6 +10399,9 @@ function buildClaudeArgs(opts) {
10399
10399
  "--allowedTools",
10400
10400
  tools
10401
10401
  ];
10402
+ if (opts.builtinTools) {
10403
+ args.push("--tools", opts.builtinTools.join(","));
10404
+ }
10402
10405
  if (opts.jsonOutput && !opts.interactive) {
10403
10406
  args.push("--output-format", "json");
10404
10407
  }
@@ -10682,6 +10685,18 @@ var init_digestPrompt = __esm({
10682
10685
  import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
10683
10686
  import { tmpdir as tmpdir2 } from "node:os";
10684
10687
  import { join as join8 } from "node:path";
10688
+ function digestSpawnOpts(o2) {
10689
+ return {
10690
+ prompt: o2.prompt,
10691
+ mcpConfigPath: o2.mcpConfigPath,
10692
+ cwd: o2.cwd,
10693
+ allowedTools: o2.mcp?.allowedTools ?? [],
10694
+ // tool-less unless the caller wired MCP reads
10695
+ builtinTools: [],
10696
+ timeoutMs: DIGEST_TIMEOUT_MS,
10697
+ model: o2.model
10698
+ };
10699
+ }
10685
10700
  function isCleanDigestExit(res) {
10686
10701
  return !res.timedOut && res.signal == null && res.exitCode === 0;
10687
10702
  }
@@ -10724,15 +10739,13 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
10724
10739
  await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
10725
10740
  }
10726
10741
  const model = await resolveRunnableModel(ctx.supabase);
10727
- const proc = spawnClaude({
10742
+ const proc = spawnClaude(digestSpawnOpts({
10728
10743
  prompt: buildDigestPrompt(payload),
10729
10744
  mcpConfigPath,
10730
10745
  cwd: dir,
10731
- allowedTools: mcp?.allowedTools ?? [],
10732
- // tool-less unless the caller wired MCP reads
10733
- timeoutMs: DIGEST_TIMEOUT_MS,
10734
- model
10735
- });
10746
+ model,
10747
+ mcp
10748
+ }));
10736
10749
  inFlight2?.set(job.id, proc);
10737
10750
  const res = await proc;
10738
10751
  const narrative = (typeof res.stdout === "string" ? res.stdout : "").trim();
@@ -17151,6 +17164,7 @@ var init_localPoller = __esm({
17151
17164
  // src/daemon/run.ts
17152
17165
  var run_exports = {};
17153
17166
  __export(run_exports, {
17167
+ JOB_TOKEN_PREFIX: () => JOB_TOKEN_PREFIX,
17154
17168
  MultipleAccountsError: () => MultipleAccountsError,
17155
17169
  __testDispatch: () => __testDispatch,
17156
17170
  affectedFieldForComplete: () => affectedFieldForComplete,
@@ -17162,6 +17176,7 @@ __export(run_exports, {
17162
17176
  claudeFailureDetail: () => claudeFailureDetail,
17163
17177
  computeDiff: () => computeDiff,
17164
17178
  parseClaudeResult: () => parseClaudeResult,
17179
+ parseJobTokenResponse: () => parseJobTokenResponse,
17165
17180
  probeMcpAuth: () => probeMcpAuth,
17166
17181
  readBranchMarker: () => readBranchMarker,
17167
17182
  routeShutdownInterrupt: () => routeShutdownInterrupt,
@@ -17254,6 +17269,56 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
17254
17269
  expires_at: typeof body.expires_at === "string" ? body.expires_at : null
17255
17270
  };
17256
17271
  }
17272
+ function parseJobTokenResponse(status, ok, body) {
17273
+ if (!ok) {
17274
+ throw new Error(`MCP job token unavailable: ${responseError(body, `job-token failed (${status})`)}`);
17275
+ }
17276
+ if (typeof body.token !== "string" || !body.token.startsWith(JOB_TOKEN_PREFIX) || body.token.length <= JOB_TOKEN_PREFIX.length) {
17277
+ throw new Error("MCP job token response did not carry an amj_ token");
17278
+ }
17279
+ const str2 = (v) => typeof v === "string" ? v : null;
17280
+ return {
17281
+ token: body.token,
17282
+ expires_at: str2(body.expires_at),
17283
+ project_id: str2(body.project_id),
17284
+ archme_branch: str2(body.archme_branch),
17285
+ scope: str2(body.scope)
17286
+ };
17287
+ }
17288
+ async function mintJobToken(ctx, jobId, deviceId) {
17289
+ const response = await fetchWithAuthRetry(
17290
+ "job-token",
17291
+ () => {
17292
+ const accessToken = ctx.auth.getAccessToken();
17293
+ if (!accessToken) throw new Error("daemon is not signed in with a registered device");
17294
+ return fetch(`${ctx.supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/job-token`, {
17295
+ method: "POST",
17296
+ headers: {
17297
+ "Content-Type": "application/json",
17298
+ Authorization: `Bearer ${accessToken}`
17299
+ },
17300
+ body: JSON.stringify({
17301
+ device_id: deviceId,
17302
+ job_id: jobId
17303
+ })
17304
+ });
17305
+ },
17306
+ ctx.reLogin
17307
+ );
17308
+ const body = await parseJsonResponse(response);
17309
+ return parseJobTokenResponse(response.status, response.ok, body);
17310
+ }
17311
+ async function writeAgentMcpConfig(ctx, job, deviceId, projectSlug, source) {
17312
+ const { token } = await mintJobToken(ctx, job.id, deviceId);
17313
+ const { configPath, cleanup } = await writeMcpConfig({
17314
+ baseUrl: ctx.mcpBaseUrl,
17315
+ projectSlug,
17316
+ branch: job.archme_branch,
17317
+ bearer: token,
17318
+ source
17319
+ });
17320
+ return { configPath, cleanup, bearer: token };
17321
+ }
17257
17322
  async function buildGitEnv(credentials) {
17258
17323
  const dir = await mkdtemp3(join19(tmpdir3(), "archme-git-askpass-"));
17259
17324
  const askpassPath = join19(dir, "askpass.sh");
@@ -17805,7 +17870,7 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
17805
17870
  body
17806
17871
  });
17807
17872
  lastStatus = res.status;
17808
- if (res.status !== 401) {
17873
+ if (res.status !== 401 && res.status !== 403) {
17809
17874
  return {
17810
17875
  ok: true,
17811
17876
  attempts: i2 + 1,
@@ -17813,13 +17878,13 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
17813
17878
  detail: `MCP accepted the daemon token (status ${res.status})`
17814
17879
  };
17815
17880
  }
17816
- lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
17881
+ lastDetail = res.status === 403 ? "MCP returned 403 \u2014 job token rejected (job not running under this claim, or URL outside its binding)" : "MCP returned 401 \u2014 token rejected by JWKS verification";
17817
17882
  } catch (err2) {
17818
17883
  lastStatus = null;
17819
17884
  lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
17820
17885
  }
17821
17886
  }
17822
- if (lastStatus === 401) {
17887
+ if (lastStatus === 401 || lastStatus === 403) {
17823
17888
  return { ok: false, attempts, lastStatus, detail: lastDetail };
17824
17889
  }
17825
17890
  return {
@@ -17894,19 +17959,12 @@ function parseClaudeResult(stdout) {
17894
17959
  return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, parsed: false };
17895
17960
  }
17896
17961
  }
17897
- async function setupDigestMcp(ctx, job) {
17962
+ async function setupDigestMcp(ctx, job, deviceId) {
17898
17963
  let cleanup;
17899
17964
  try {
17965
+ if (!deviceId) throw new Error("daemon is not signed in with a registered device");
17900
17966
  const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
17901
- const mcpBearer = await ctx.auth.getMcpAccessToken();
17902
- const { configPath, cleanup: cleanupFn } = await writeMcpConfig({
17903
- baseUrl: ctx.mcpBaseUrl,
17904
- projectSlug,
17905
- branch: job.archme_branch,
17906
- bearer: mcpBearer
17907
- // No `source`: digest reads must not produce source-tagged commits — they
17908
- // don't write at all, but leaving it off keeps the URL read-shaped.
17909
- });
17967
+ const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(ctx, job, deviceId, projectSlug);
17910
17968
  cleanup = cleanupFn;
17911
17969
  const probe = await probeMcpAuth(
17912
17970
  `${ctx.mcpBaseUrl}?project=${encodeURIComponent(projectSlug)}&branch=${encodeURIComponent(job.archme_branch)}`,
@@ -17929,7 +17987,7 @@ async function setupDigestMcp(ctx, job) {
17929
17987
  async function runJob(ctx, job, claimDeviceId) {
17930
17988
  if (job.job_kind === "digest") {
17931
17989
  const digestDevice = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId() ?? void 0;
17932
- const { mcp, cleanup } = await setupDigestMcp(ctx, job);
17990
+ const { mcp, cleanup } = await setupDigestMcp(ctx, job, digestDevice);
17933
17991
  try {
17934
17992
  await runDigestJob(ctx, job, digestDevice, inFlight, mcp);
17935
17993
  } finally {
@@ -17961,6 +18019,27 @@ async function runJob(ctx, job, claimDeviceId) {
17961
18019
  }, HEARTBEAT_INTERVAL_MS);
17962
18020
  try {
17963
18021
  const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
18022
+ const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(
18023
+ ctx,
18024
+ job,
18025
+ deviceId,
18026
+ projectSlug,
18027
+ {
18028
+ repo: `${job.repo_owner}/${job.repo_name}`,
18029
+ before_sha: job.before_sha ?? null,
18030
+ after_sha: job.after_sha,
18031
+ job_id: job.id,
18032
+ // Effective run mode — always diff or pr_update now (reconcile is gone).
18033
+ mode: job.source_mode === "pr_update" ? "pr_update" : "diff",
18034
+ commit_message: job.commit_message ?? null,
18035
+ committed_at: job.head_commit_timestamp ?? null,
18036
+ author_name: job.author_name ?? null,
18037
+ author_login: job.author_login ?? null
18038
+ }
18039
+ );
18040
+ activeCleanups.add(cleanupFn);
18041
+ cleanupFns.push(cleanupFn);
18042
+ markPhase("job_token");
17964
18043
  let clone;
17965
18044
  try {
17966
18045
  clone = await ensureCloneAndFetch(ctx, job, deviceId);
@@ -18041,27 +18120,6 @@ async function runJob(ctx, job, claimDeviceId) {
18041
18120
  throw new Error(`failed to materialize large diff to disk (${bytes} bytes): ${e.message}`);
18042
18121
  }
18043
18122
  }
18044
- const mcpBearer = await ctx.auth.getMcpAccessToken();
18045
- const { configPath, cleanup: cleanupFn } = await writeMcpConfig({
18046
- baseUrl: ctx.mcpBaseUrl,
18047
- projectSlug,
18048
- branch: job.archme_branch,
18049
- bearer: mcpBearer,
18050
- source: {
18051
- repo: `${job.repo_owner}/${job.repo_name}`,
18052
- before_sha: job.before_sha ?? null,
18053
- after_sha: job.after_sha,
18054
- job_id: job.id,
18055
- // Effective run mode — always diff or pr_update now (reconcile is gone).
18056
- mode: effectiveMode,
18057
- commit_message: job.commit_message ?? null,
18058
- committed_at: job.head_commit_timestamp ?? null,
18059
- author_name: job.author_name ?? null,
18060
- author_login: job.author_login ?? null
18061
- }
18062
- });
18063
- activeCleanups.add(cleanupFn);
18064
- cleanupFns.push(cleanupFn);
18065
18123
  let affectedSeed = "";
18066
18124
  let affectedForComplete = null;
18067
18125
  if (diff) {
@@ -18674,7 +18732,7 @@ ${signal} received, shutting down`);
18674
18732
  process.on("SIGINT", () => shutdown("SIGINT"));
18675
18733
  process.on("SIGTERM", () => shutdown("SIGTERM"));
18676
18734
  }
18677
- var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
18735
+ var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
18678
18736
  var init_run = __esm({
18679
18737
  "src/daemon/run.ts"() {
18680
18738
  "use strict";
@@ -18736,6 +18794,7 @@ var init_run = __esm({
18736
18794
  ctxByJob = /* @__PURE__ */ new Map();
18737
18795
  deviceByJob = /* @__PURE__ */ new Map();
18738
18796
  DEFAULT_CLONE_BASE_DIR = `${homedir6()}/.archme-job-runner/clones`;
18797
+ JOB_TOKEN_PREFIX = "amj_";
18739
18798
  INLINE_PATCH_BYTE_CAP = 2e5;
18740
18799
  GIT_SHA_RE = /^[0-9a-f]{40}$/;
18741
18800
  CURATOR_PREAMBLE = `## Your role
@@ -18864,6 +18923,11 @@ ${ANCHOR_VERIFICATION}`;
18864
18923
  return deviceByJob.get(jobId);
18865
18924
  },
18866
18925
  runJob,
18926
+ // R7: the job-bound `amj_` MCP token mint and the digest's MCP setup, so a
18927
+ // test can assert the agent's mcp.json carries the job token and never the
18928
+ // daemon's own session.
18929
+ mintJobToken,
18930
+ setupDigestMcp,
18867
18931
  // Exposes the clone/fetch entrypoint so a test can assert that the repo-token
18868
18932
  // mint (getRepoCredentials -> agents-device/repo-token, gated on
18869
18933
  // claimed_by_device_id) uses the CLAIM-time device rather than recomputing via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",