@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.
Files changed (2) hide show
  1. package/dist/index.js +262 -71
  2. 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.11" : "0.0.0-dev";
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 = 2e4;
123527
- var DEFAULT_POLICY_POLL_WINDOW_MS = 30 * 6e4;
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
- return {
124133
- content: [{
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
- const res = await flavor.resolve(sdk, currentCredentials.apiKey, intent, category);
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
- const bridge = await startDelegateBridge({
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 closeBridge = () => void bridge.close().catch(() => {
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
- envKeys = plistEnvKeys(String(readFileSync16(join25(dir, f), "utf-8")));
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("--max-budget-usd <x>", "hard spend ceiling in USD", (v) => Number(v)).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
- "optional hard spend ceiling per run in USD (opt-in; omitted by default)",
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.11",
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.11",
94
- "@ametyst/cli-darwin-x64": "0.3.11",
95
- "@ametyst/cli-linux-x64-gnu": "0.3.11",
96
- "@ametyst/cli-win32-x64-msvc": "0.3.11"
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",