@archme/cli 0.8.2 → 0.9.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 +87 -36
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17151,6 +17151,7 @@ var init_localPoller = __esm({
|
|
|
17151
17151
|
// src/daemon/run.ts
|
|
17152
17152
|
var run_exports = {};
|
|
17153
17153
|
__export(run_exports, {
|
|
17154
|
+
JOB_TOKEN_PREFIX: () => JOB_TOKEN_PREFIX,
|
|
17154
17155
|
MultipleAccountsError: () => MultipleAccountsError,
|
|
17155
17156
|
__testDispatch: () => __testDispatch,
|
|
17156
17157
|
affectedFieldForComplete: () => affectedFieldForComplete,
|
|
@@ -17162,6 +17163,7 @@ __export(run_exports, {
|
|
|
17162
17163
|
claudeFailureDetail: () => claudeFailureDetail,
|
|
17163
17164
|
computeDiff: () => computeDiff,
|
|
17164
17165
|
parseClaudeResult: () => parseClaudeResult,
|
|
17166
|
+
parseJobTokenResponse: () => parseJobTokenResponse,
|
|
17165
17167
|
probeMcpAuth: () => probeMcpAuth,
|
|
17166
17168
|
readBranchMarker: () => readBranchMarker,
|
|
17167
17169
|
routeShutdownInterrupt: () => routeShutdownInterrupt,
|
|
@@ -17254,6 +17256,56 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
|
|
|
17254
17256
|
expires_at: typeof body.expires_at === "string" ? body.expires_at : null
|
|
17255
17257
|
};
|
|
17256
17258
|
}
|
|
17259
|
+
function parseJobTokenResponse(status, ok, body) {
|
|
17260
|
+
if (!ok) {
|
|
17261
|
+
throw new Error(`MCP job token unavailable: ${responseError(body, `job-token failed (${status})`)}`);
|
|
17262
|
+
}
|
|
17263
|
+
if (typeof body.token !== "string" || !body.token.startsWith(JOB_TOKEN_PREFIX) || body.token.length <= JOB_TOKEN_PREFIX.length) {
|
|
17264
|
+
throw new Error("MCP job token response did not carry an amj_ token");
|
|
17265
|
+
}
|
|
17266
|
+
const str2 = (v) => typeof v === "string" ? v : null;
|
|
17267
|
+
return {
|
|
17268
|
+
token: body.token,
|
|
17269
|
+
expires_at: str2(body.expires_at),
|
|
17270
|
+
project_id: str2(body.project_id),
|
|
17271
|
+
archme_branch: str2(body.archme_branch),
|
|
17272
|
+
scope: str2(body.scope)
|
|
17273
|
+
};
|
|
17274
|
+
}
|
|
17275
|
+
async function mintJobToken(ctx, jobId, deviceId) {
|
|
17276
|
+
const response = await fetchWithAuthRetry(
|
|
17277
|
+
"job-token",
|
|
17278
|
+
() => {
|
|
17279
|
+
const accessToken = ctx.auth.getAccessToken();
|
|
17280
|
+
if (!accessToken) throw new Error("daemon is not signed in with a registered device");
|
|
17281
|
+
return fetch(`${ctx.supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/job-token`, {
|
|
17282
|
+
method: "POST",
|
|
17283
|
+
headers: {
|
|
17284
|
+
"Content-Type": "application/json",
|
|
17285
|
+
Authorization: `Bearer ${accessToken}`
|
|
17286
|
+
},
|
|
17287
|
+
body: JSON.stringify({
|
|
17288
|
+
device_id: deviceId,
|
|
17289
|
+
job_id: jobId
|
|
17290
|
+
})
|
|
17291
|
+
});
|
|
17292
|
+
},
|
|
17293
|
+
ctx.reLogin
|
|
17294
|
+
);
|
|
17295
|
+
const body = await parseJsonResponse(response);
|
|
17296
|
+
return parseJobTokenResponse(response.status, response.ok, body);
|
|
17297
|
+
}
|
|
17298
|
+
async function writeAgentMcpConfig(ctx, job, deviceId, projectSlug, source) {
|
|
17299
|
+
const { token } = await mintJobToken(ctx, job.id, deviceId);
|
|
17300
|
+
const { configPath, cleanup } = await writeMcpConfig({
|
|
17301
|
+
baseUrl: ctx.mcpBaseUrl,
|
|
17302
|
+
projectSlug,
|
|
17303
|
+
branch: job.archme_branch,
|
|
17304
|
+
bearer: token,
|
|
17305
|
+
source
|
|
17306
|
+
});
|
|
17307
|
+
return { configPath, cleanup, bearer: token };
|
|
17308
|
+
}
|
|
17257
17309
|
async function buildGitEnv(credentials) {
|
|
17258
17310
|
const dir = await mkdtemp3(join19(tmpdir3(), "archme-git-askpass-"));
|
|
17259
17311
|
const askpassPath = join19(dir, "askpass.sh");
|
|
@@ -17805,7 +17857,7 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
|
|
|
17805
17857
|
body
|
|
17806
17858
|
});
|
|
17807
17859
|
lastStatus = res.status;
|
|
17808
|
-
if (res.status !== 401) {
|
|
17860
|
+
if (res.status !== 401 && res.status !== 403) {
|
|
17809
17861
|
return {
|
|
17810
17862
|
ok: true,
|
|
17811
17863
|
attempts: i2 + 1,
|
|
@@ -17813,13 +17865,13 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
|
|
|
17813
17865
|
detail: `MCP accepted the daemon token (status ${res.status})`
|
|
17814
17866
|
};
|
|
17815
17867
|
}
|
|
17816
|
-
lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
|
|
17868
|
+
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
17869
|
} catch (err2) {
|
|
17818
17870
|
lastStatus = null;
|
|
17819
17871
|
lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
|
|
17820
17872
|
}
|
|
17821
17873
|
}
|
|
17822
|
-
if (lastStatus === 401) {
|
|
17874
|
+
if (lastStatus === 401 || lastStatus === 403) {
|
|
17823
17875
|
return { ok: false, attempts, lastStatus, detail: lastDetail };
|
|
17824
17876
|
}
|
|
17825
17877
|
return {
|
|
@@ -17894,19 +17946,12 @@ function parseClaudeResult(stdout) {
|
|
|
17894
17946
|
return { summary: raw, modelServed: null, usage: null, costUsd: null, isError: false, parsed: false };
|
|
17895
17947
|
}
|
|
17896
17948
|
}
|
|
17897
|
-
async function setupDigestMcp(ctx, job) {
|
|
17949
|
+
async function setupDigestMcp(ctx, job, deviceId) {
|
|
17898
17950
|
let cleanup;
|
|
17899
17951
|
try {
|
|
17952
|
+
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
17900
17953
|
const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
|
|
17901
|
-
const mcpBearer = await ctx
|
|
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
|
-
});
|
|
17954
|
+
const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(ctx, job, deviceId, projectSlug);
|
|
17910
17955
|
cleanup = cleanupFn;
|
|
17911
17956
|
const probe = await probeMcpAuth(
|
|
17912
17957
|
`${ctx.mcpBaseUrl}?project=${encodeURIComponent(projectSlug)}&branch=${encodeURIComponent(job.archme_branch)}`,
|
|
@@ -17929,7 +17974,7 @@ async function setupDigestMcp(ctx, job) {
|
|
|
17929
17974
|
async function runJob(ctx, job, claimDeviceId) {
|
|
17930
17975
|
if (job.job_kind === "digest") {
|
|
17931
17976
|
const digestDevice = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId() ?? void 0;
|
|
17932
|
-
const { mcp, cleanup } = await setupDigestMcp(ctx, job);
|
|
17977
|
+
const { mcp, cleanup } = await setupDigestMcp(ctx, job, digestDevice);
|
|
17933
17978
|
try {
|
|
17934
17979
|
await runDigestJob(ctx, job, digestDevice, inFlight, mcp);
|
|
17935
17980
|
} finally {
|
|
@@ -17961,6 +18006,27 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
17961
18006
|
}, HEARTBEAT_INTERVAL_MS);
|
|
17962
18007
|
try {
|
|
17963
18008
|
const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
|
|
18009
|
+
const { configPath, cleanup: cleanupFn, bearer: mcpBearer } = await writeAgentMcpConfig(
|
|
18010
|
+
ctx,
|
|
18011
|
+
job,
|
|
18012
|
+
deviceId,
|
|
18013
|
+
projectSlug,
|
|
18014
|
+
{
|
|
18015
|
+
repo: `${job.repo_owner}/${job.repo_name}`,
|
|
18016
|
+
before_sha: job.before_sha ?? null,
|
|
18017
|
+
after_sha: job.after_sha,
|
|
18018
|
+
job_id: job.id,
|
|
18019
|
+
// Effective run mode — always diff or pr_update now (reconcile is gone).
|
|
18020
|
+
mode: job.source_mode === "pr_update" ? "pr_update" : "diff",
|
|
18021
|
+
commit_message: job.commit_message ?? null,
|
|
18022
|
+
committed_at: job.head_commit_timestamp ?? null,
|
|
18023
|
+
author_name: job.author_name ?? null,
|
|
18024
|
+
author_login: job.author_login ?? null
|
|
18025
|
+
}
|
|
18026
|
+
);
|
|
18027
|
+
activeCleanups.add(cleanupFn);
|
|
18028
|
+
cleanupFns.push(cleanupFn);
|
|
18029
|
+
markPhase("job_token");
|
|
17964
18030
|
let clone;
|
|
17965
18031
|
try {
|
|
17966
18032
|
clone = await ensureCloneAndFetch(ctx, job, deviceId);
|
|
@@ -18041,27 +18107,6 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
18041
18107
|
throw new Error(`failed to materialize large diff to disk (${bytes} bytes): ${e.message}`);
|
|
18042
18108
|
}
|
|
18043
18109
|
}
|
|
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
18110
|
let affectedSeed = "";
|
|
18066
18111
|
let affectedForComplete = null;
|
|
18067
18112
|
if (diff) {
|
|
@@ -18674,7 +18719,7 @@ ${signal} received, shutting down`);
|
|
|
18674
18719
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18675
18720
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
18676
18721
|
}
|
|
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;
|
|
18722
|
+
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
18723
|
var init_run = __esm({
|
|
18679
18724
|
"src/daemon/run.ts"() {
|
|
18680
18725
|
"use strict";
|
|
@@ -18736,6 +18781,7 @@ var init_run = __esm({
|
|
|
18736
18781
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
18737
18782
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
18738
18783
|
DEFAULT_CLONE_BASE_DIR = `${homedir6()}/.archme-job-runner/clones`;
|
|
18784
|
+
JOB_TOKEN_PREFIX = "amj_";
|
|
18739
18785
|
INLINE_PATCH_BYTE_CAP = 2e5;
|
|
18740
18786
|
GIT_SHA_RE = /^[0-9a-f]{40}$/;
|
|
18741
18787
|
CURATOR_PREAMBLE = `## Your role
|
|
@@ -18864,6 +18910,11 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
18864
18910
|
return deviceByJob.get(jobId);
|
|
18865
18911
|
},
|
|
18866
18912
|
runJob,
|
|
18913
|
+
// R7: the job-bound `amj_` MCP token mint and the digest's MCP setup, so a
|
|
18914
|
+
// test can assert the agent's mcp.json carries the job token and never the
|
|
18915
|
+
// daemon's own session.
|
|
18916
|
+
mintJobToken,
|
|
18917
|
+
setupDigestMcp,
|
|
18867
18918
|
// Exposes the clone/fetch entrypoint so a test can assert that the repo-token
|
|
18868
18919
|
// mint (getRepoCredentials -> agents-device/repo-token, gated on
|
|
18869
18920
|
// 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.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|