@ametyst/cli 0.3.11 → 0.3.12
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/index.js +262 -71
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -112487,7 +112487,7 @@ var init_version5 = __esm({
|
|
|
112487
112487
|
"src/version.ts"() {
|
|
112488
112488
|
"use strict";
|
|
112489
112489
|
init_esm_shims();
|
|
112490
|
-
CLI_VERSION = true ? "0.3.
|
|
112490
|
+
CLI_VERSION = true ? "0.3.12" : "0.0.0-dev";
|
|
112491
112491
|
}
|
|
112492
112492
|
});
|
|
112493
112493
|
|
|
@@ -118708,6 +118708,21 @@ function estimateBlastRadius(task) {
|
|
|
118708
118708
|
return { steps, paidSteps, estCostEur };
|
|
118709
118709
|
}
|
|
118710
118710
|
|
|
118711
|
+
// src/tasks/ownership.ts
|
|
118712
|
+
init_esm_shims();
|
|
118713
|
+
async function resolveTaskOwnership(sdk, apiKey, entity) {
|
|
118714
|
+
try {
|
|
118715
|
+
const createdBy = typeof entity?.createdBy === "string" ? entity.createdBy.trim() : "";
|
|
118716
|
+
if (!createdBy) return { resolved: false };
|
|
118717
|
+
const res = await sdk.compoundedSkills.getSyncSelection(apiKey);
|
|
118718
|
+
const name = res?.status === "ok" && typeof res.self?.name === "string" ? res.self.name.trim() : "";
|
|
118719
|
+
if (!name) return { resolved: false };
|
|
118720
|
+
return { resolved: true, isOwner: name === createdBy, createdBy };
|
|
118721
|
+
} catch {
|
|
118722
|
+
return { resolved: false };
|
|
118723
|
+
}
|
|
118724
|
+
}
|
|
118725
|
+
|
|
118711
118726
|
// src/tasks/dashboard-template.ts
|
|
118712
118727
|
init_esm_shims();
|
|
118713
118728
|
var DEFAULT_DASHBOARD_FILES = [
|
|
@@ -123135,6 +123150,18 @@ var GET_TASK_BASE_DESCRIPTION = "Search the workspace's TASKS by intent and RETU
|
|
|
123135
123150
|
function buildGetTaskDescription(items) {
|
|
123136
123151
|
return `${GET_TASK_BASE_DESCRIPTION}${buildTaskIndexSection(items, "task")}`;
|
|
123137
123152
|
}
|
|
123153
|
+
function pickExactSlugFromIntent(intent, index2, category) {
|
|
123154
|
+
if (!index2 || index2.length === 0) return null;
|
|
123155
|
+
const tokens = new Set(intent.toLowerCase().match(/[a-z0-9-]+/g) ?? []);
|
|
123156
|
+
if (tokens.size === 0) return null;
|
|
123157
|
+
const hits = /* @__PURE__ */ new Set();
|
|
123158
|
+
for (const item of index2) {
|
|
123159
|
+
if (typeof item.slug !== "string" || item.slug === "") continue;
|
|
123160
|
+
if (category !== void 0 && (item.category ?? void 0) !== category) continue;
|
|
123161
|
+
if (tokens.has(item.slug.toLowerCase())) hits.add(item.slug);
|
|
123162
|
+
}
|
|
123163
|
+
return hits.size === 1 ? [...hits][0] : null;
|
|
123164
|
+
}
|
|
123138
123165
|
var refreshGetAllowlistDescriptionInFlight = false;
|
|
123139
123166
|
var pendingRefreshTimer = null;
|
|
123140
123167
|
async function refreshGetAllowlistDescription() {
|
|
@@ -123523,8 +123550,8 @@ function buildPolicyActiveOnchainReader(sdk) {
|
|
|
123523
123550
|
}
|
|
123524
123551
|
};
|
|
123525
123552
|
}
|
|
123526
|
-
var DEFAULT_POLICY_POLL_INTERVAL_MS =
|
|
123527
|
-
var DEFAULT_POLICY_POLL_WINDOW_MS =
|
|
123553
|
+
var DEFAULT_POLICY_POLL_INTERVAL_MS = 5e3;
|
|
123554
|
+
var DEFAULT_POLICY_POLL_WINDOW_MS = 25e3;
|
|
123528
123555
|
function parsePollEnvMs(raw, fallback2) {
|
|
123529
123556
|
const n = Number(raw?.trim());
|
|
123530
123557
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback2;
|
|
@@ -123547,6 +123574,7 @@ var approvalWaitConfig = (() => {
|
|
|
123547
123574
|
delay: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
|
|
123548
123575
|
};
|
|
123549
123576
|
})();
|
|
123577
|
+
var pendingAccessRequest = null;
|
|
123550
123578
|
async function tryResolvePendingApproval(probe) {
|
|
123551
123579
|
if (currentCredentials.authorizationStatus === "approved") return true;
|
|
123552
123580
|
if (currentCredentials.authorizationStatus !== "pending" || !currentCredentials.apiKey || !currentCredentials.eoaAddress) {
|
|
@@ -124078,9 +124106,10 @@ server.tool(
|
|
|
124078
124106
|
server.tool(
|
|
124079
124107
|
{
|
|
124080
124108
|
name: "requestAccess",
|
|
124081
|
-
description: "Request wallet authorization with a chosen policy. Sends request to admin for approval.",
|
|
124109
|
+
description: "Request wallet authorization with a chosen policy. Sends the request to the admin for approval and waits BRIEFLY for it (~25 s \u2014 short by design, so this call returns before your MCP host times out). If it comes back `pending`, the request stays valid on the admin's side: call getWalletStatus once the user says it was approved, and do NOT call requestAccess again in a loop. While a request from this session is pending, calling requestAccess again RE-CHECKS that request and never submits a new one; `resubmit: true` is the only way to force a fresh request.",
|
|
124082
124110
|
inputs: [
|
|
124083
|
-
{ name: "policy_id", type: "number", required: true, description: "The policy ID to request access with" }
|
|
124111
|
+
{ name: "policy_id", type: "number", required: true, description: "The policy ID to request access with" },
|
|
124112
|
+
{ name: "resubmit", type: "boolean", required: false, description: "Force a FRESH request even though this session already has one pending (mints a new session key and queues a second approval for the admin). Default false: while a request from this session is pending, requestAccess only re-checks it." }
|
|
124084
124113
|
]
|
|
124085
124114
|
},
|
|
124086
124115
|
async (params) => {
|
|
@@ -124112,6 +124141,56 @@ server.tool(
|
|
|
124112
124141
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "No API key or wallet address. Run ametyst login, then start_session." }) }]
|
|
124113
124142
|
};
|
|
124114
124143
|
}
|
|
124144
|
+
const windowSeconds = Math.round(resolveApprovalWaitConfigFromEnv().windowMs / 1e3);
|
|
124145
|
+
const approvedResponse = (signerAddress, forPolicyId) => ({
|
|
124146
|
+
content: [{
|
|
124147
|
+
type: "text",
|
|
124148
|
+
text: JSON.stringify({
|
|
124149
|
+
success: true,
|
|
124150
|
+
data: {
|
|
124151
|
+
eoaAddress: signerAddress,
|
|
124152
|
+
policyId: forPolicyId,
|
|
124153
|
+
status: "approved",
|
|
124154
|
+
walletAddress: currentCredentials.walletAddress,
|
|
124155
|
+
message: "Authorization approved. Wallet is ready to spend."
|
|
124156
|
+
},
|
|
124157
|
+
guidance: {
|
|
124158
|
+
say_to_user: "Your admin approved the policy \u2014 the wallet is ready. Go ahead and tell me what you'd like to do.",
|
|
124159
|
+
next_action: "Proceed with the user's original request (e.g. getAllowlist or spend)."
|
|
124160
|
+
}
|
|
124161
|
+
})
|
|
124162
|
+
}]
|
|
124163
|
+
});
|
|
124164
|
+
const pendingResponse = (signerAddress, forPolicyId, requestId, rechecked) => ({
|
|
124165
|
+
content: [{
|
|
124166
|
+
type: "text",
|
|
124167
|
+
text: JSON.stringify({
|
|
124168
|
+
success: true,
|
|
124169
|
+
data: {
|
|
124170
|
+
eoaAddress: signerAddress,
|
|
124171
|
+
policyId: forPolicyId,
|
|
124172
|
+
status: "pending",
|
|
124173
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
124174
|
+
alreadyPending: rechecked,
|
|
124175
|
+
message: rechecked ? `This session already has an authorization request pending${requestId ? ` (id ${requestId})` : ""} \u2014 re-checked just now, still awaiting the admin; nothing new was submitted. It stays valid: call getWalletStatus once your admin approves. Pass resubmit: true only to force a fresh request (it mints a new session key and queues a second approval).` : `Authorization request sent and still pending after the ${windowSeconds}s auto-wait window \u2014 short by design, so this call returns before your MCP host times out. The request stays valid: call getWalletStatus once your admin approves. Do NOT call requestAccess again in a loop \u2014 a repeat call only re-checks this same request without submitting a new one; resubmit: true is the only way to force a fresh request.`
|
|
124176
|
+
},
|
|
124177
|
+
guidance: {
|
|
124178
|
+
say_to_user: rechecked ? "Your authorization request is still pending your admin's approval (I re-checked it just now, no new request was sent). Once they approve it, just ask me again or say 'check wallet status' and I'll pick it up." : "I've requested authorization and it's pending your admin's approval. I waited briefly \u2014 the wait is short by design \u2014 and it hasn't been approved yet, but the request stays valid on their side. Once they approve it, just ask me again or say 'check wallet status' and I'll pick it up. No need to do anything else right now.",
|
|
124179
|
+
next_action: "When the user says the admin approved, call getWalletStatus to sync. Do NOT call requestAccess again in a loop: a repeat call re-checks this pending request and never submits a new one (resubmit: true forces a fresh request \u2014 only when the user asks for one).",
|
|
124180
|
+
stop: true
|
|
124181
|
+
}
|
|
124182
|
+
})
|
|
124183
|
+
}]
|
|
124184
|
+
});
|
|
124185
|
+
if (currentCredentials.authorizationStatus === "pending" && pendingAccessRequest !== null && pendingAccessRequest.walletId === currentCredentials.pendingWalletId && params.resubmit !== true) {
|
|
124186
|
+
const pending = pendingAccessRequest;
|
|
124187
|
+
console.error(`\u{1F501} [requestAccess] A request from this session is already pending${pending.walletId ? ` (id ${pending.walletId})` : ""} \u2014 re-checking, not submitting`);
|
|
124188
|
+
if (await tryResolvePendingApproval()) {
|
|
124189
|
+
pendingAccessRequest = null;
|
|
124190
|
+
return approvedResponse(pending.signerAddress, pending.policyId);
|
|
124191
|
+
}
|
|
124192
|
+
return pendingResponse(pending.signerAddress, pending.policyId, pending.walletId, true);
|
|
124193
|
+
}
|
|
124115
124194
|
const { virtualWalletsManagers: virtualWalletsManagers2 } = await getSDK();
|
|
124116
124195
|
const result = await submitRotatedAccessRequest(
|
|
124117
124196
|
virtualWalletsManagers2,
|
|
@@ -124125,49 +124204,15 @@ server.tool(
|
|
|
124125
124204
|
);
|
|
124126
124205
|
currentCredentials.authorizationStatus = "pending";
|
|
124127
124206
|
currentCredentials.pendingWalletId = result.id ? String(result.id) : void 0;
|
|
124207
|
+
pendingAccessRequest = { walletId: currentCredentials.pendingWalletId, policyId, signerAddress: result.signerAddress };
|
|
124128
124208
|
console.error(`\u2705 [requestAccess] Authorization request sent for ${result.signerAddress}`);
|
|
124129
124209
|
await emitPendingApprovalNotice();
|
|
124130
124210
|
const approved = await autoWaitForApproval();
|
|
124131
124211
|
if (approved) {
|
|
124132
|
-
|
|
124133
|
-
|
|
124134
|
-
type: "text",
|
|
124135
|
-
text: JSON.stringify({
|
|
124136
|
-
success: true,
|
|
124137
|
-
data: {
|
|
124138
|
-
eoaAddress: result.signerAddress,
|
|
124139
|
-
policyId,
|
|
124140
|
-
status: "approved",
|
|
124141
|
-
walletAddress: currentCredentials.walletAddress,
|
|
124142
|
-
message: "Authorization approved. Wallet is ready to spend."
|
|
124143
|
-
},
|
|
124144
|
-
guidance: {
|
|
124145
|
-
say_to_user: "Your admin approved the policy \u2014 the wallet is ready. Go ahead and tell me what you'd like to do.",
|
|
124146
|
-
next_action: "Proceed with the user's original request (e.g. getAllowlist or spend)."
|
|
124147
|
-
}
|
|
124148
|
-
})
|
|
124149
|
-
}]
|
|
124150
|
-
};
|
|
124212
|
+
pendingAccessRequest = null;
|
|
124213
|
+
return approvedResponse(result.signerAddress, policyId);
|
|
124151
124214
|
}
|
|
124152
|
-
return
|
|
124153
|
-
content: [{
|
|
124154
|
-
type: "text",
|
|
124155
|
-
text: JSON.stringify({
|
|
124156
|
-
success: true,
|
|
124157
|
-
data: {
|
|
124158
|
-
eoaAddress: result.signerAddress,
|
|
124159
|
-
policyId,
|
|
124160
|
-
status: "pending",
|
|
124161
|
-
message: "Authorization request sent and still pending after the auto-wait window elapsed. The request is still valid \u2014 call getWalletStatus to re-check once your admin approves, or re-run requestAccess to wait again."
|
|
124162
|
-
},
|
|
124163
|
-
guidance: {
|
|
124164
|
-
say_to_user: "I've requested authorization and waited a while, but your admin hasn't approved it yet. The request is still pending on their side \u2014 once they approve it, just ask me again (or say 'check wallet status') and I'll pick it up. No need to do anything else right now.",
|
|
124165
|
-
next_action: "When the user is ready, call getWalletStatus to re-check approval, or requestAccess to wait again.",
|
|
124166
|
-
stop: true
|
|
124167
|
-
}
|
|
124168
|
-
})
|
|
124169
|
-
}]
|
|
124170
|
-
};
|
|
124215
|
+
return pendingResponse(result.signerAddress, policyId, currentCredentials.pendingWalletId, false);
|
|
124171
124216
|
} catch (error) {
|
|
124172
124217
|
console.error("\u274C Error in requestAccess:", error);
|
|
124173
124218
|
return {
|
|
@@ -124695,6 +124740,17 @@ function categoryGateResponse(params, slug) {
|
|
|
124695
124740
|
}) }]
|
|
124696
124741
|
};
|
|
124697
124742
|
}
|
|
124743
|
+
function renderSingleTaskMarkdown(task) {
|
|
124744
|
+
const updated = typeof task.updatedAt === "string" && task.updatedAt ? task.updatedAt.slice(0, 10) : "unknown";
|
|
124745
|
+
const catSeg = task.category ? `category: ${task.category} \xB7 ` : "";
|
|
124746
|
+
return `## ${task.slug}
|
|
124747
|
+
|
|
124748
|
+
> ${catSeg}updated ${updated}
|
|
124749
|
+
|
|
124750
|
+
${task.descriptionShort ?? ""}
|
|
124751
|
+
|
|
124752
|
+
${task.markdownBody ?? ""}`;
|
|
124753
|
+
}
|
|
124698
124754
|
async function resolveTasksCore(params, flavor) {
|
|
124699
124755
|
try {
|
|
124700
124756
|
const intent = typeof params.intent === "string" ? params.intent.trim() : "";
|
|
@@ -124703,7 +124759,16 @@ async function resolveTasksCore(params, flavor) {
|
|
|
124703
124759
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "No API key configured." }) }] };
|
|
124704
124760
|
}
|
|
124705
124761
|
const sdk = await getSDK();
|
|
124706
|
-
|
|
124762
|
+
let exactTask = null;
|
|
124763
|
+
if (flavor.fetchExact && intent !== "") {
|
|
124764
|
+
if (!taskIndexCache || taskIndexCache.length === 0) await refreshDynamicPrompts();
|
|
124765
|
+
const slug = pickExactSlugFromIntent(intent, taskIndexCache, category);
|
|
124766
|
+
if (slug) {
|
|
124767
|
+
const got = await flavor.fetchExact(sdk, currentCredentials.apiKey, slug).catch(() => null);
|
|
124768
|
+
if (got?.status === "ok" && got.task && typeof got.task.slug === "string") exactTask = got.task;
|
|
124769
|
+
}
|
|
124770
|
+
}
|
|
124771
|
+
const res = exactTask ? { status: "ok", markdown: renderSingleTaskMarkdown(exactTask), markdownBody: String(exactTask.markdownBody ?? "") } : await flavor.resolve(sdk, currentCredentials.apiKey, intent, category);
|
|
124707
124772
|
if (res.status === "ok") {
|
|
124708
124773
|
const payload = {
|
|
124709
124774
|
success: true,
|
|
@@ -124717,7 +124782,7 @@ async function resolveTasksCore(params, flavor) {
|
|
|
124717
124782
|
if (spilled.filePath) payload.markdownBodyFilePath = spilled.filePath;
|
|
124718
124783
|
else if (typeof spilled.inline === "string") payload.markdownBody = spilled.inline;
|
|
124719
124784
|
if (slug && flavor.fetchManifest) {
|
|
124720
|
-
const manifest = await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
|
|
124785
|
+
const manifest = exactTask ? normalizeMemoryManifest(exactTask.stateDocs ?? null) : await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
|
|
124721
124786
|
if (manifest) payload.stateDocs = manifest;
|
|
124722
124787
|
if (manifest !== void 0) payload.memory = memorySummaryLine(manifest);
|
|
124723
124788
|
}
|
|
@@ -124753,24 +124818,13 @@ server.tool(
|
|
|
124753
124818
|
sayToUser: "Present these matching tasks to the user, briefly explain them, and ask which one to run before proceeding. Then, before running the one they pick, ASK HOW to run it and wait for an answer \u2014 always ask, never pick one silently: (1) one-time, in this chat, followed here; (2) one-time, headless in its own process via `ametyst task run <slug>`; (3) scheduled, recurring via `ametyst task schedule <slug> --every <dur>` (ask which cadence). Only then call runTask with the chosen mode (`in-chat` for 1, `headless` for 2) \u2014 for 3, hand the user the schedule command instead. When the user gave arguments for the run (a company, a cap, a list), pass them to runTask as `input` \u2014 they reach the task exactly as if the user had typed them in chat.",
|
|
124754
124819
|
resolverFailedSayToUser: "Couldn't search tasks right now.",
|
|
124755
124820
|
surfaceRawBodies: true,
|
|
124756
|
-
fetchManifest: (sdk, apiKey, slug) => readTaskManifest(sdk, apiKey, slug)
|
|
124821
|
+
fetchManifest: (sdk, apiKey, slug) => readTaskManifest(sdk, apiKey, slug),
|
|
124822
|
+
fetchExact: (sdk, apiKey, slug) => sdk.tasks.get(apiKey, slug)
|
|
124757
124823
|
})
|
|
124758
124824
|
);
|
|
124759
124825
|
function shellQuote2(s) {
|
|
124760
124826
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
124761
124827
|
}
|
|
124762
|
-
async function resolveTaskOwnership(sdk, apiKey, entity) {
|
|
124763
|
-
try {
|
|
124764
|
-
const createdBy = typeof entity?.createdBy === "string" ? entity.createdBy.trim() : "";
|
|
124765
|
-
if (!createdBy) return { resolved: false };
|
|
124766
|
-
const res = await sdk.compoundedSkills.getSyncSelection(apiKey);
|
|
124767
|
-
const name = res?.status === "ok" && typeof res.self?.name === "string" ? res.self.name.trim() : "";
|
|
124768
|
-
if (!name) return { resolved: false };
|
|
124769
|
-
return { resolved: true, isOwner: name === createdBy, createdBy };
|
|
124770
|
-
} catch {
|
|
124771
|
-
return { resolved: false };
|
|
124772
|
-
}
|
|
124773
|
-
}
|
|
124774
124828
|
function taskMemoryBlock(slug, docKey) {
|
|
124775
124829
|
const listRecords = `taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" })`;
|
|
124776
124830
|
return {
|
|
@@ -124793,6 +124847,18 @@ function closeAllLiveDashboards() {
|
|
|
124793
124847
|
liveDashboards.clear();
|
|
124794
124848
|
}
|
|
124795
124849
|
process.once("exit", closeAllLiveDashboards);
|
|
124850
|
+
function shutdownForHostClose() {
|
|
124851
|
+
try {
|
|
124852
|
+
fireAutoShareOnShutdown();
|
|
124853
|
+
} catch {
|
|
124854
|
+
}
|
|
124855
|
+
if (liveUnlockListener) {
|
|
124856
|
+
void liveUnlockListener.close("closed").catch(() => {
|
|
124857
|
+
});
|
|
124858
|
+
liveUnlockListener = null;
|
|
124859
|
+
}
|
|
124860
|
+
closeAllLiveDashboards();
|
|
124861
|
+
}
|
|
124796
124862
|
async function startLiveDashboard(entity, dir, sdk, apiKey) {
|
|
124797
124863
|
const slug = typeof entity?.slug === "string" ? entity.slug : "";
|
|
124798
124864
|
const html = entity?.dashboardHtml;
|
|
@@ -124961,7 +125027,7 @@ server.tool(
|
|
|
124961
125027
|
fetch: (sdk, apiKey, ref) => sdk.tasks.get(apiKey, ref),
|
|
124962
125028
|
pick: (res) => res.task,
|
|
124963
125029
|
headlessCommand: (ref, input) => `ametyst task run ${ref}${input === void 0 ? "" : ` --input ${shellQuote2(input)}`}`,
|
|
124964
|
-
headlessSayToUser: (_ref, command) => `Run \`${command}\` in a terminal \u2014 it materializes the task, runs it unattended, and ships back improvements on a clean finish. (\`task run\` is the unattended runner for every task row, whatever the task was originally authored as.) Paid steps go through your on-chain policy.`,
|
|
125030
|
+
headlessSayToUser: (_ref, command) => `Run \`${command}\` in a terminal \u2014 it materializes the task, runs it unattended, and ships back improvements on a clean finish. (\`task run\` is the unattended runner for every task row, whatever the task was originally authored as.) Paid steps go through your on-chain policy. If you add \`--max-budget-usd <x>\`, that caps the MODEL's token spend for the headless run (it is forwarded to \`claude -p\`); merchant payments are bounded by your on-chain policy, not by that flag.`,
|
|
124965
125031
|
honorFrontmatterDefault: true,
|
|
124966
125032
|
materialize: (task, runId, runRoot) => materializeTask(task, runId, runRoot),
|
|
124967
125033
|
buildDirective: ({ dir, slug, files, docKey, runRoot }) => {
|
|
@@ -126873,6 +126939,18 @@ async function startMCPServer(walletKeystoreJson, eoaAddress, config, versionNot
|
|
|
126873
126939
|
}
|
|
126874
126940
|
};
|
|
126875
126941
|
}
|
|
126942
|
+
if (hooks?.onTransportClosed) {
|
|
126943
|
+
const inner = server.nativeServer.server;
|
|
126944
|
+
const previousOnClose = inner.onclose;
|
|
126945
|
+
inner.onclose = () => {
|
|
126946
|
+
previousOnClose?.();
|
|
126947
|
+
try {
|
|
126948
|
+
hooks.onTransportClosed?.();
|
|
126949
|
+
} catch (err) {
|
|
126950
|
+
console.error(`\u26A0\uFE0F onTransportClosed hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126951
|
+
}
|
|
126952
|
+
};
|
|
126953
|
+
}
|
|
126876
126954
|
const transport = new StdioServerTransport();
|
|
126877
126955
|
await server.nativeServer.connect(transport);
|
|
126878
126956
|
console.error("\u2705 MCP Server started on stdio");
|
|
@@ -126924,6 +127002,39 @@ function delegateAllowanceRef(merchantSlug, capability) {
|
|
|
126924
127002
|
return { permissionId, commerceInfoId };
|
|
126925
127003
|
}
|
|
126926
127004
|
|
|
127005
|
+
// src/commands/serve-lifecycle.ts
|
|
127006
|
+
init_esm_shims();
|
|
127007
|
+
var HOST_CLOSE_GRACE_MS = 1e3;
|
|
127008
|
+
var PARENT_WATCHDOG_INTERVAL_MS = 3e4;
|
|
127009
|
+
var HOST_CLOSED_MESSAGE = "ametyst serve: host closed the session \u2014 exiting";
|
|
127010
|
+
function armHostLifecycle(deps) {
|
|
127011
|
+
const graceMs = deps.graceMs ?? HOST_CLOSE_GRACE_MS;
|
|
127012
|
+
const watchdogMs = deps.watchdogIntervalMs ?? PARENT_WATCHDOG_INTERVAL_MS;
|
|
127013
|
+
let fired = false;
|
|
127014
|
+
const hostClosed = (reason) => {
|
|
127015
|
+
if (fired) return;
|
|
127016
|
+
fired = true;
|
|
127017
|
+
deps.log(`${HOST_CLOSED_MESSAGE} (${reason})`);
|
|
127018
|
+
try {
|
|
127019
|
+
void Promise.resolve(deps.shutdown()).catch(() => {
|
|
127020
|
+
});
|
|
127021
|
+
} catch {
|
|
127022
|
+
}
|
|
127023
|
+
setTimeout(() => deps.exit(0), graceMs);
|
|
127024
|
+
};
|
|
127025
|
+
deps.stdin.once("end", () => hostClosed("stdin ended"));
|
|
127026
|
+
deps.stdin.once("close", () => hostClosed("stdin closed"));
|
|
127027
|
+
const watchdog = setInterval(() => {
|
|
127028
|
+
if (deps.getPpid() === 1) hostClosed("parent process gone");
|
|
127029
|
+
}, watchdogMs);
|
|
127030
|
+
watchdog.unref?.();
|
|
127031
|
+
return {
|
|
127032
|
+
hostClosed,
|
|
127033
|
+
triggered: () => fired,
|
|
127034
|
+
dispose: () => clearInterval(watchdog)
|
|
127035
|
+
};
|
|
127036
|
+
}
|
|
127037
|
+
|
|
126927
127038
|
// src/commands/serve.ts
|
|
126928
127039
|
init_version5();
|
|
126929
127040
|
|
|
@@ -127316,7 +127427,19 @@ async function serveCommand() {
|
|
|
127316
127427
|
console.error(
|
|
127317
127428
|
persisted ? isPlaintextVault(persisted) ? "Starting MCP server... (persisted UNENCRYPTED wallet found \u2014 always unlocked, no start_session needed)" : "Starting MCP server... (persisted wallet found \u2014 unlock with start_session)" : "Starting MCP server... (no saved wallet \u2014 one will be created on start_session)"
|
|
127318
127429
|
);
|
|
127430
|
+
let bridge = null;
|
|
127431
|
+
const lifecycle = armHostLifecycle({
|
|
127432
|
+
stdin: process.stdin,
|
|
127433
|
+
getPpid: () => process.ppid,
|
|
127434
|
+
shutdown: async () => {
|
|
127435
|
+
shutdownForHostClose();
|
|
127436
|
+
if (bridge) await bridge.close();
|
|
127437
|
+
},
|
|
127438
|
+
exit: (code) => process.exit(code),
|
|
127439
|
+
log: (message) => console.error(message)
|
|
127440
|
+
});
|
|
127319
127441
|
await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
|
|
127442
|
+
onTransportClosed: () => lifecycle.hostClosed("transport closed"),
|
|
127320
127443
|
// Best-effort: materialize local `/`-command pointer skills for every published
|
|
127321
127444
|
// task so they're available without a manual `ametyst task sync-skills`.
|
|
127322
127445
|
// Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
|
|
@@ -127330,14 +127453,15 @@ async function serveCommand() {
|
|
|
127330
127453
|
console.error("\u{1F512} delegate bridge not started \u2014 this MCP server is running inside a delegated run");
|
|
127331
127454
|
return;
|
|
127332
127455
|
}
|
|
127333
|
-
|
|
127456
|
+
bridge = await startDelegateBridge({
|
|
127334
127457
|
spend: invokeSpendTool,
|
|
127335
127458
|
isUnlocked: isWalletUnlockedForDelegate,
|
|
127336
127459
|
resolveAllowanceRef: () => delegateAllowanceRef(DELEGATE_MERCHANT_SLUG, DELEGATE_CAPABILITY),
|
|
127337
127460
|
version: CLI_VERSION
|
|
127338
127461
|
});
|
|
127339
127462
|
if (bridge) {
|
|
127340
|
-
const
|
|
127463
|
+
const bound = bridge;
|
|
127464
|
+
const closeBridge = () => void bound.close().catch(() => {
|
|
127341
127465
|
});
|
|
127342
127466
|
process.on("exit", closeBridge);
|
|
127343
127467
|
process.on("SIGINT", closeBridge);
|
|
@@ -128406,6 +128530,14 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128406
128530
|
}
|
|
128407
128531
|
async function shipConstraints() {
|
|
128408
128532
|
try {
|
|
128533
|
+
const ownership = await resolveTaskOwnership(sdk, apiKey, task);
|
|
128534
|
+
if (ownership.resolved && !ownership.isOwner) {
|
|
128535
|
+
constraintsReport = { outcome: "skipped-not-owner", owner: ownership.createdBy };
|
|
128536
|
+
console.log(
|
|
128537
|
+
`Task ${task.slug}: ship-back skipped: this task is owned by ${ownership.createdBy}; your learnings stay in the run diary (and the member-scoped "learnings" document when declared)`
|
|
128538
|
+
);
|
|
128539
|
+
return;
|
|
128540
|
+
}
|
|
128409
128541
|
const constraintsPath = join24(dir, "CONSTRAINTS.md");
|
|
128410
128542
|
const materializedConstraints = existsSync16(constraintsPath) ? readFileSync15(constraintsPath, "utf-8") : void 0;
|
|
128411
128543
|
if (materializedConstraints === void 0) return;
|
|
@@ -128908,6 +129040,60 @@ function plistEnvKeys(xml) {
|
|
|
128908
129040
|
if (close < 0) return [];
|
|
128909
129041
|
return [...xml.slice(open, close).matchAll(/<key>([^<]*)<\/key>/g)].map((m) => unescapeXml(m[1]));
|
|
128910
129042
|
}
|
|
129043
|
+
function plistProgramArguments(xml) {
|
|
129044
|
+
const anchor = xml.indexOf("<key>ProgramArguments</key>");
|
|
129045
|
+
if (anchor < 0) return [];
|
|
129046
|
+
const open = xml.indexOf("<array>", anchor);
|
|
129047
|
+
if (open < 0) return [];
|
|
129048
|
+
const close = xml.indexOf("</array>", open);
|
|
129049
|
+
if (close < 0) return [];
|
|
129050
|
+
return [...xml.slice(open, close).matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unescapeXml(m[1]));
|
|
129051
|
+
}
|
|
129052
|
+
function cronCommandArgv(line) {
|
|
129053
|
+
const words = shellWords(line);
|
|
129054
|
+
const cd = words.indexOf("cd");
|
|
129055
|
+
const amp = words.indexOf("&&", cd >= 0 ? cd + 2 : 0);
|
|
129056
|
+
if (amp < 0) return [];
|
|
129057
|
+
let i = amp + 1;
|
|
129058
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
129059
|
+
const argv = [];
|
|
129060
|
+
for (; i < words.length; i++) {
|
|
129061
|
+
if (words[i] === "#") break;
|
|
129062
|
+
argv.push(words[i].replace(/\\%/g, "%"));
|
|
129063
|
+
}
|
|
129064
|
+
return argv;
|
|
129065
|
+
}
|
|
129066
|
+
function parseScheduledArgs(argv) {
|
|
129067
|
+
const out = {};
|
|
129068
|
+
const valueOf = (i, flag) => {
|
|
129069
|
+
const w = argv[i];
|
|
129070
|
+
if (w === flag) return argv[i + 1];
|
|
129071
|
+
if (w.startsWith(`${flag}=`)) return w.slice(flag.length + 1);
|
|
129072
|
+
return void 0;
|
|
129073
|
+
};
|
|
129074
|
+
for (let i = 0; i < argv.length; i++) {
|
|
129075
|
+
const input = valueOf(i, "--input");
|
|
129076
|
+
if (input !== void 0) out.input = input;
|
|
129077
|
+
const budget = valueOf(i, "--max-budget-usd");
|
|
129078
|
+
if (budget !== void 0) {
|
|
129079
|
+
const n = Number(budget);
|
|
129080
|
+
if (Number.isFinite(n)) out.maxBudgetUsd = n;
|
|
129081
|
+
}
|
|
129082
|
+
}
|
|
129083
|
+
return out;
|
|
129084
|
+
}
|
|
129085
|
+
var SCHEDULE_INPUT_DISPLAY_MAX = 60;
|
|
129086
|
+
function formatScheduleEntryLine(e) {
|
|
129087
|
+
const parts2 = [e.namespace, e.slug];
|
|
129088
|
+
if (typeof e.args.input === "string") {
|
|
129089
|
+
const flat = e.args.input.replace(/\s+/g, " ").trim();
|
|
129090
|
+
const shown = flat.length > SCHEDULE_INPUT_DISPLAY_MAX ? `${flat.slice(0, SCHEDULE_INPUT_DISPLAY_MAX)}\u2026` : flat;
|
|
129091
|
+
parts2.push(`input: "${shown}"`);
|
|
129092
|
+
}
|
|
129093
|
+
if (typeof e.args.maxBudgetUsd === "number") parts2.push(`budget: $${e.args.maxBudgetUsd}`);
|
|
129094
|
+
parts2.push(`env: ${e.envKeys.length ? e.envKeys.join(", ") : "\u2014"}`);
|
|
129095
|
+
return parts2.join(" ");
|
|
129096
|
+
}
|
|
128911
129097
|
function shellWords(s) {
|
|
128912
129098
|
const out = [];
|
|
128913
129099
|
let i = 0;
|
|
@@ -128956,17 +129142,22 @@ function listEntries(kind, opts = {}) {
|
|
|
128956
129142
|
return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
|
|
128957
129143
|
const slug = f.slice(prefix.length, -".plist".length);
|
|
128958
129144
|
let envKeys = [];
|
|
129145
|
+
let args = {};
|
|
128959
129146
|
try {
|
|
128960
|
-
|
|
129147
|
+
const xml = String(readFileSync16(join25(dir, f), "utf-8"));
|
|
129148
|
+
envKeys = plistEnvKeys(xml);
|
|
129149
|
+
args = parseScheduledArgs(plistProgramArguments(xml));
|
|
128961
129150
|
} catch {
|
|
128962
129151
|
envKeys = [];
|
|
129152
|
+
args = {};
|
|
128963
129153
|
}
|
|
128964
|
-
return { slug, envKeys: [...envKeys].sort() };
|
|
129154
|
+
return { slug, envKeys: [...envKeys].sort(), args };
|
|
128965
129155
|
});
|
|
128966
129156
|
}
|
|
128967
129157
|
return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => ({
|
|
128968
129158
|
slug: l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length),
|
|
128969
|
-
envKeys: cronEnvKeys(l).sort()
|
|
129159
|
+
envKeys: cronEnvKeys(l).sort(),
|
|
129160
|
+
args: parseScheduledArgs(cronCommandArgv(l))
|
|
128970
129161
|
}));
|
|
128971
129162
|
}
|
|
128972
129163
|
function scheduleTask(slug, opts = {}) {
|
|
@@ -129113,7 +129304,11 @@ var taskCommand = new Command("task").description(
|
|
|
129113
129304
|
);
|
|
129114
129305
|
taskCommand.command("run <id>").description(
|
|
129115
129306
|
"Materialize a task and run it headless, then ship back improvements on clean exit. While it runs, the task's dashboard (if it has one) is served on http://localhost:4477 (base port: AMETYST_TASK_DASHBOARD_PORT, walking up when taken) and opened in the browser once when stdout is a TTY (set AMETYST_DASHBOARD_NO_OPEN=1 to skip)"
|
|
129116
|
-
).option(
|
|
129307
|
+
).option(
|
|
129308
|
+
"--max-budget-usd <x>",
|
|
129309
|
+
"cap on the MODEL's token spend for this headless run (forwarded to claude -p); merchant payments are bounded by your on-chain policy, not by this flag",
|
|
129310
|
+
(v) => Number(v)
|
|
129311
|
+
).option(
|
|
129117
129312
|
"--max-concurrent-fires <n>",
|
|
129118
129313
|
"ceiling on simultaneously-live fires of this task; 0 = unlimited (default: AMETYST_TASK_MAX_CONCURRENT_FIRES, else 6)",
|
|
129119
129314
|
(v) => Number(v)
|
|
@@ -129144,7 +129339,7 @@ taskCommand.command("push <path>").description("Publish a task whose body is rea
|
|
|
129144
129339
|
);
|
|
129145
129340
|
taskCommand.command("schedule <slug>").description("Schedule a headless task run (launchd on macOS, crontab on Linux)").option("--at <hhmm>", "daily run time, 24h HH:MM (e.g. 02:30)").option("--every <dur>", "recurring interval, e.g. 30m, 1h, 1d").option(
|
|
129146
129341
|
"--max-budget-usd <x>",
|
|
129147
|
-
"
|
|
129342
|
+
"cap on the MODEL's token spend for this headless run (forwarded to claude -p); merchant payments are bounded by your on-chain policy, not by this flag (opt-in; omitted by default)",
|
|
129148
129343
|
(v) => Number(v)
|
|
129149
129344
|
).option(
|
|
129150
129345
|
"--model <id>",
|
|
@@ -129185,11 +129380,7 @@ taskCommand.command("schedules").description(
|
|
|
129185
129380
|
"List every scheduled job and the environment each armed job carries \u2014 across every label namespace, the legacy ones an older cli used included"
|
|
129186
129381
|
).action(() => {
|
|
129187
129382
|
const entries = listAllScheduleEntries();
|
|
129188
|
-
console.log(
|
|
129189
|
-
entries.length ? entries.map(
|
|
129190
|
-
(e) => `${e.namespace} ${e.slug} env: ${e.envKeys.length ? e.envKeys.join(", ") : "\u2014"}`
|
|
129191
|
-
).join("\n") : "no scheduled tasks"
|
|
129192
|
-
);
|
|
129383
|
+
console.log(entries.length ? entries.map(formatScheduleEntryLine).join("\n") : "no scheduled tasks");
|
|
129193
129384
|
});
|
|
129194
129385
|
taskCommand.command("sync-skills").description(
|
|
129195
129386
|
"Write a local pointer skill per published task into the host agent's skills directory \u2014 .claude/skills/<slug>/SKILL.md for Claude, .codex/skills/<slug>/SKILL.md for Codex (restores /-slash-command invocation in file-based hosts)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ametyst/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Ametyst CLI — embedded MCP server, wallet ops, agent payments",
|
|
6
6
|
"type": "module",
|
|
@@ -90,10 +90,10 @@
|
|
|
90
90
|
"esbuild@>=0.27.3 <0.28.1": "0.28.1"
|
|
91
91
|
},
|
|
92
92
|
"optionalDependencies": {
|
|
93
|
-
"@ametyst/cli-darwin-arm64": "0.3.
|
|
94
|
-
"@ametyst/cli-darwin-x64": "0.3.
|
|
95
|
-
"@ametyst/cli-linux-x64-gnu": "0.3.
|
|
96
|
-
"@ametyst/cli-win32-x64-msvc": "0.3.
|
|
93
|
+
"@ametyst/cli-darwin-arm64": "0.3.12",
|
|
94
|
+
"@ametyst/cli-darwin-x64": "0.3.12",
|
|
95
|
+
"@ametyst/cli-linux-x64-gnu": "0.3.12",
|
|
96
|
+
"@ametyst/cli-win32-x64-msvc": "0.3.12"
|
|
97
97
|
},
|
|
98
98
|
"scripts": {
|
|
99
99
|
"build:native": "cd native && cargo build --release && napi build --release",
|