@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.
- package/dist/cli.js +106 -42
- 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
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
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
|
|
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.
|
|
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",
|