@dev.sail.money/sailor 0.0.2-15 → 0.0.2-17
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/examples/permissions/BoundedSwap_UniswapV3_Base.sol +4 -3
- package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +4 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +401 -44
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/sdk/dist/templates/ammLiquidity.d.ts +24 -11
- package/packages/sdk/dist/templates/ammLiquidity.d.ts.map +1 -1
- package/packages/sdk/dist/templates/ammLiquidity.js +39 -31
- package/packages/sdk/dist/templates/ammLiquidity.js.map +1 -1
- package/packages/sdk/dist/templates/approveAndCallBatch.d.ts +24 -10
- package/packages/sdk/dist/templates/approveAndCallBatch.d.ts.map +1 -1
- package/packages/sdk/dist/templates/approveAndCallBatch.js +36 -23
- package/packages/sdk/dist/templates/approveAndCallBatch.js.map +1 -1
- package/packages/sdk/dist/templates/boundedBorrow.d.ts +19 -9
- package/packages/sdk/dist/templates/boundedBorrow.d.ts.map +1 -1
- package/packages/sdk/dist/templates/boundedBorrow.js +28 -19
- package/packages/sdk/dist/templates/boundedBorrow.js.map +1 -1
- package/packages/sdk/dist/templates/boundedSwap.d.ts +19 -9
- package/packages/sdk/dist/templates/boundedSwap.d.ts.map +1 -1
- package/packages/sdk/dist/templates/boundedSwap.js +30 -20
- package/packages/sdk/dist/templates/boundedSwap.js.map +1 -1
- package/packages/sdk/dist/templates/defiBundle.d.ts +35 -9
- package/packages/sdk/dist/templates/defiBundle.d.ts.map +1 -1
- package/packages/sdk/dist/templates/defiBundle.js +84 -22
- package/packages/sdk/dist/templates/defiBundle.js.map +1 -1
- package/packages/sdk/dist/templates/pendle.d.ts +23 -8
- package/packages/sdk/dist/templates/pendle.d.ts.map +1 -1
- package/packages/sdk/dist/templates/pendle.js +34 -14
- package/packages/sdk/dist/templates/pendle.js.map +1 -1
- package/packages/sdk/dist/templates/transferTarget.d.ts +11 -3
- package/packages/sdk/dist/templates/transferTarget.d.ts.map +1 -1
- package/packages/sdk/dist/templates/transferTarget.js +14 -7
- package/packages/sdk/dist/templates/transferTarget.js.map +1 -1
- package/packages/sdk/package.json +1 -0
- package/templates/default/.github/workflows/agent-tick.yml +8 -7
- package/templates/default/AGENTS.md +12 -1
- package/templates/default/src/config.ts +1 -11
- package/templates/default/tsconfig.json +7 -1
|
@@ -39455,6 +39455,40 @@ ${label} key saved. Address: ${checksum4(keyring.address)}`);
|
|
|
39455
39455
|
}
|
|
39456
39456
|
}
|
|
39457
39457
|
}
|
|
39458
|
+
async function keysExportCi() {
|
|
39459
|
+
const account2 = readJsonFile(sailPath("account.json"));
|
|
39460
|
+
const src = resolveKeyPath("manager", account2?.safe);
|
|
39461
|
+
if (!fileExists(src)) {
|
|
39462
|
+
throw new Error(
|
|
39463
|
+
'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
|
|
39464
|
+
);
|
|
39465
|
+
}
|
|
39466
|
+
const dest = import_node_path6.default.resolve(process.cwd(), "ci-keystore.json");
|
|
39467
|
+
import_node_fs7.default.copyFileSync(src, dest);
|
|
39468
|
+
console.log(`\u2713 Keystore copied to ci-keystore.json`);
|
|
39469
|
+
console.log(` Source: ${src}`);
|
|
39470
|
+
const gitignorePath = import_node_path6.default.resolve(process.cwd(), ".gitignore");
|
|
39471
|
+
if (import_node_fs7.default.existsSync(gitignorePath)) {
|
|
39472
|
+
const content = import_node_fs7.default.readFileSync(gitignorePath, "utf-8");
|
|
39473
|
+
if (!content.includes("ci-keystore.json")) {
|
|
39474
|
+
import_node_fs7.default.appendFileSync(
|
|
39475
|
+
gitignorePath,
|
|
39476
|
+
"\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
|
|
39477
|
+
);
|
|
39478
|
+
console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
|
|
39479
|
+
} else {
|
|
39480
|
+
console.log("\u2713 .gitignore already tracks ci-keystore.json");
|
|
39481
|
+
}
|
|
39482
|
+
}
|
|
39483
|
+
console.log("\nNext steps:");
|
|
39484
|
+
console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
|
|
39485
|
+
console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
|
|
39486
|
+
console.log(" RPC_URL \u2014 your RPC endpoint");
|
|
39487
|
+
console.log(" 2. Commit and push ci-keystore.json:");
|
|
39488
|
+
console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
|
|
39489
|
+
console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
|
|
39490
|
+
console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
|
|
39491
|
+
}
|
|
39458
39492
|
async function keysShow() {
|
|
39459
39493
|
const present = ROLES.filter((role) => keyExists(role));
|
|
39460
39494
|
if (present.length === 0) {
|
|
@@ -40403,7 +40437,7 @@ async function resolveSmaChoice(options, json) {
|
|
|
40403
40437
|
if (!isAddress(options.sma, { strict: false })) {
|
|
40404
40438
|
throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
40405
40439
|
}
|
|
40406
|
-
return { kind: "address", address: options.sma };
|
|
40440
|
+
return { kind: "address", address: getAddress(options.sma) };
|
|
40407
40441
|
}
|
|
40408
40442
|
if (options.newSma) return { kind: "new" };
|
|
40409
40443
|
if (json) {
|
|
@@ -40415,7 +40449,7 @@ async function resolveSmaChoice(options, json) {
|
|
|
40415
40449
|
);
|
|
40416
40450
|
if (choice.toLowerCase() === "y" || choice.toLowerCase() === "yes") return { kind: "new" };
|
|
40417
40451
|
if (!isAddress(choice, { strict: false })) throw new Error(`Invalid SMA address: ${choice}`);
|
|
40418
|
-
return { kind: "address", address: choice };
|
|
40452
|
+
return { kind: "address", address: getAddress(choice) };
|
|
40419
40453
|
}
|
|
40420
40454
|
async function resolveTemplate(project, options, json) {
|
|
40421
40455
|
const templates = (() => {
|
|
@@ -40432,7 +40466,7 @@ async function resolveTemplate(project, options, json) {
|
|
|
40432
40466
|
);
|
|
40433
40467
|
if (match) return { address: match.address, label: match.label };
|
|
40434
40468
|
if (isAddress(options.template, { strict: false })) {
|
|
40435
|
-
return { address: options.template, label: options.template };
|
|
40469
|
+
return { address: getAddress(options.template), label: options.template };
|
|
40436
40470
|
}
|
|
40437
40471
|
throw new Error(
|
|
40438
40472
|
`Unknown mandate template "${options.template}". Run "sailor mandate templates".`
|
|
@@ -40779,7 +40813,7 @@ async function runDeploy(project, channel, options) {
|
|
|
40779
40813
|
if (!json) fn();
|
|
40780
40814
|
};
|
|
40781
40815
|
if (options.attach && !options.sma) throw new Error("--attach requires --sma <address>");
|
|
40782
|
-
if (options.sma && !isAddress(options.sma)) {
|
|
40816
|
+
if (options.sma && !isAddress(options.sma, { strict: false })) {
|
|
40783
40817
|
throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
40784
40818
|
}
|
|
40785
40819
|
if (project.chainId !== 8453) {
|
|
@@ -40855,16 +40889,17 @@ async function runDeploy(project, channel, options) {
|
|
|
40855
40889
|
});
|
|
40856
40890
|
let attachTxHash;
|
|
40857
40891
|
if (options.attach && options.sma) {
|
|
40892
|
+
const sma = getAddress(options.sma);
|
|
40858
40893
|
attachTxHash = await attachToSma(
|
|
40859
40894
|
project,
|
|
40860
40895
|
channel,
|
|
40861
40896
|
publicClient,
|
|
40862
|
-
|
|
40897
|
+
sma,
|
|
40863
40898
|
deployed,
|
|
40864
40899
|
record.name,
|
|
40865
40900
|
json
|
|
40866
40901
|
);
|
|
40867
|
-
store.recordAttachment(deployed, { sma
|
|
40902
|
+
store.recordAttachment(deployed, { sma, txHash: attachTxHash });
|
|
40868
40903
|
} else {
|
|
40869
40904
|
say(
|
|
40870
40905
|
() => console.log(
|
|
@@ -40877,7 +40912,283 @@ Register it later with: sailor mandate attach --address ${deployed} --sma <SMA>`
|
|
|
40877
40912
|
}, {
|
|
40878
40913
|
status: "ok",
|
|
40879
40914
|
mandate: { name: record.name, address: deployed, txHash: response.txHash, chainId },
|
|
40880
|
-
attached: options.attach ? { sma: options.sma, txHash: attachTxHash } : null
|
|
40915
|
+
attached: options.attach ? { sma: getAddress(options.sma), txHash: attachTxHash } : null
|
|
40916
|
+
});
|
|
40917
|
+
}
|
|
40918
|
+
var CLONE_INIT_PREFIX = "0x3d602d80600a3d3981f3363d3d373d3d3d363d73";
|
|
40919
|
+
var CLONE_INIT_SUFFIX = "0x5af43d82803e903d91602b57fd5bf3";
|
|
40920
|
+
var PERMISSION_FACTORY_ABI = [
|
|
40921
|
+
{
|
|
40922
|
+
type: "function",
|
|
40923
|
+
name: "deployAndAttach",
|
|
40924
|
+
stateMutability: "payable",
|
|
40925
|
+
inputs: [
|
|
40926
|
+
{ name: "account", type: "address" },
|
|
40927
|
+
{ name: "impl", type: "address" },
|
|
40928
|
+
{ name: "salt", type: "bytes32" },
|
|
40929
|
+
{ name: "initData", type: "bytes" },
|
|
40930
|
+
{ name: "kernelDeadline", type: "uint256" },
|
|
40931
|
+
{ name: "kernelSig", type: "bytes" }
|
|
40932
|
+
],
|
|
40933
|
+
outputs: [{ name: "clone", type: "address" }]
|
|
40934
|
+
}
|
|
40935
|
+
];
|
|
40936
|
+
var CLONE_TEMPLATES = {
|
|
40937
|
+
boundedApprove: {
|
|
40938
|
+
label: "Bounded Approve",
|
|
40939
|
+
buildInitData: (p) => encodeFunctionData({
|
|
40940
|
+
abi: [
|
|
40941
|
+
{
|
|
40942
|
+
type: "function",
|
|
40943
|
+
name: "initialize",
|
|
40944
|
+
stateMutability: "nonpayable",
|
|
40945
|
+
inputs: [
|
|
40946
|
+
{ name: "allowedTokens", type: "address[]" },
|
|
40947
|
+
{ name: "allowedSpenders", type: "address[]" },
|
|
40948
|
+
{ name: "_maxAmountPerTx", type: "uint256" },
|
|
40949
|
+
{ name: "_permissionSigner", type: "address" }
|
|
40950
|
+
],
|
|
40951
|
+
outputs: []
|
|
40952
|
+
}
|
|
40953
|
+
],
|
|
40954
|
+
functionName: "initialize",
|
|
40955
|
+
args: [p.tokens, p.spenders, p.max, p.permissionSigner]
|
|
40956
|
+
}),
|
|
40957
|
+
describe: (p) => [
|
|
40958
|
+
{ label: "Allowed tokens", value: p.tokens.join(", ") },
|
|
40959
|
+
{ label: "Allowed spenders", value: p.spenders.join(", ") },
|
|
40960
|
+
{
|
|
40961
|
+
label: "Max approval / tx",
|
|
40962
|
+
value: p.max === maxUint256 ? "unlimited (uint256 max)" : p.max.toString()
|
|
40963
|
+
}
|
|
40964
|
+
]
|
|
40965
|
+
}
|
|
40966
|
+
};
|
|
40967
|
+
function predictCloneAddress(impl, factory, submitter, salt) {
|
|
40968
|
+
const namespacedSalt = keccak256(
|
|
40969
|
+
encodeAbiParameters([{ type: "address" }, { type: "bytes32" }], [submitter, salt])
|
|
40970
|
+
);
|
|
40971
|
+
const initCode = concatHex([CLONE_INIT_PREFIX, impl, CLONE_INIT_SUFFIX]);
|
|
40972
|
+
return getCreate2Address({
|
|
40973
|
+
from: factory,
|
|
40974
|
+
salt: namespacedSalt,
|
|
40975
|
+
bytecodeHash: keccak256(initCode)
|
|
40976
|
+
});
|
|
40977
|
+
}
|
|
40978
|
+
function parseAddressList(csv, flag) {
|
|
40979
|
+
if (!csv) throw new Error(`${flag} is required (comma-separated address list)`);
|
|
40980
|
+
const list = csv.split(",").map((s) => s.trim()).filter(Boolean);
|
|
40981
|
+
if (list.length === 0) throw new Error(`${flag} is empty`);
|
|
40982
|
+
for (const a of list) {
|
|
40983
|
+
if (!isAddress(a, { strict: false })) throw new Error(`${flag} contains an invalid address: ${a}`);
|
|
40984
|
+
}
|
|
40985
|
+
return list.map((a) => getAddress(a));
|
|
40986
|
+
}
|
|
40987
|
+
async function mandateDeployClone(options) {
|
|
40988
|
+
const project = requireProject();
|
|
40989
|
+
const channel = await createSigningChannel(process.cwd());
|
|
40990
|
+
try {
|
|
40991
|
+
await channel.start();
|
|
40992
|
+
await runDeployClone(project, channel, options);
|
|
40993
|
+
} catch (err) {
|
|
40994
|
+
fail(err, options.json);
|
|
40995
|
+
} finally {
|
|
40996
|
+
channel.stop();
|
|
40997
|
+
}
|
|
40998
|
+
}
|
|
40999
|
+
async function runDeployClone(project, channel, options) {
|
|
41000
|
+
const json = !!options.json;
|
|
41001
|
+
const say = (fn) => {
|
|
41002
|
+
if (!json) fn();
|
|
41003
|
+
};
|
|
41004
|
+
if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
41005
|
+
const sma = getAddress(options.sma);
|
|
41006
|
+
const spec = CLONE_TEMPLATES[options.template];
|
|
41007
|
+
if (!spec) {
|
|
41008
|
+
throw new Error(
|
|
41009
|
+
`Unsupported clone template "${options.template}". Supported: ${Object.keys(CLONE_TEMPLATES).join(", ")}`
|
|
41010
|
+
);
|
|
41011
|
+
}
|
|
41012
|
+
const impl = project.deployment.standaloneTemplates?.[options.template];
|
|
41013
|
+
if (!impl || !isAddress(impl, { strict: false })) {
|
|
41014
|
+
throw new Error(
|
|
41015
|
+
`No "${options.template}" standalone template is bundled for chain ${project.chainId}.`
|
|
41016
|
+
);
|
|
41017
|
+
}
|
|
41018
|
+
const chain2 = getChainById(project.chainId);
|
|
41019
|
+
const publicClient = publicClientFor(project);
|
|
41020
|
+
const agentSigner = await loadManagerSigner2();
|
|
41021
|
+
const registered = await publicClient.readContract({
|
|
41022
|
+
address: project.contracts.kernel,
|
|
41023
|
+
abi: SailKernelAbi,
|
|
41024
|
+
functionName: "registered",
|
|
41025
|
+
args: [sma]
|
|
41026
|
+
});
|
|
41027
|
+
if (!registered) {
|
|
41028
|
+
throw new Error(`SMA ${sma} is not registered with SailKernel; cannot register a permission.`);
|
|
41029
|
+
}
|
|
41030
|
+
const kernelConfig = await publicClient.readContract({
|
|
41031
|
+
address: project.contracts.kernel,
|
|
41032
|
+
abi: SailKernelAbi,
|
|
41033
|
+
functionName: "configs",
|
|
41034
|
+
args: [sma]
|
|
41035
|
+
});
|
|
41036
|
+
const permissionSigner = kernelConfig[0];
|
|
41037
|
+
const initParams = {
|
|
41038
|
+
permissionSigner,
|
|
41039
|
+
tokens: parseAddressList(options.tokens, "--tokens"),
|
|
41040
|
+
spenders: parseAddressList(options.spenders, "--spenders"),
|
|
41041
|
+
max: options.max ? BigInt(options.max) : maxUint256
|
|
41042
|
+
};
|
|
41043
|
+
const initData = spec.buildInitData(initParams);
|
|
41044
|
+
const submitter = agentSigner.address;
|
|
41045
|
+
const salt = keccak256(
|
|
41046
|
+
encodeAbiParameters(
|
|
41047
|
+
[{ type: "address" }, { type: "address" }, { type: "uint256" }],
|
|
41048
|
+
[sma, impl, BigInt(Math.floor(Date.now() / 1e3))]
|
|
41049
|
+
)
|
|
41050
|
+
);
|
|
41051
|
+
const clone = predictCloneAddress(impl, project.contracts.permissionFactory, submitter, salt);
|
|
41052
|
+
say(() => {
|
|
41053
|
+
console.log(`
|
|
41054
|
+
${spec.label} clone (${options.template})`);
|
|
41055
|
+
console.log(` logic impl: ${impl}`);
|
|
41056
|
+
console.log(` predicted clone: ${clone}`);
|
|
41057
|
+
console.log(` SMA: ${sma}`);
|
|
41058
|
+
for (const d of spec.describe(initParams)) console.log(` ${d.label}: ${d.value}`);
|
|
41059
|
+
console.log(`
|
|
41060
|
+
\u2192 Signing station:
|
|
41061
|
+
Open ${channel.url} and connect your Owner wallet
|
|
41062
|
+
`);
|
|
41063
|
+
});
|
|
41064
|
+
const nonce = await publicClient.readContract({
|
|
41065
|
+
address: project.contracts.kernel,
|
|
41066
|
+
abi: SailKernelAbi,
|
|
41067
|
+
functionName: "signerNonces",
|
|
41068
|
+
args: [sma]
|
|
41069
|
+
});
|
|
41070
|
+
let registerPermissionHasDeadline = false;
|
|
41071
|
+
try {
|
|
41072
|
+
const caps = await detectKernelCapabilities(publicClient, project.contracts.kernel, {
|
|
41073
|
+
chainId: project.chainId
|
|
41074
|
+
});
|
|
41075
|
+
registerPermissionHasDeadline = caps.registerPermissionHasDeadline;
|
|
41076
|
+
} catch {
|
|
41077
|
+
}
|
|
41078
|
+
const deadline = registerPermissionHasDeadline ? BigInt(Math.floor(Date.now() / 1e3) + 300) : void 0;
|
|
41079
|
+
const typedData = buildRegisterPermissionTypedData({
|
|
41080
|
+
chainId: project.chainId,
|
|
41081
|
+
kernel: project.contracts.kernel,
|
|
41082
|
+
account: sma,
|
|
41083
|
+
permission: clone,
|
|
41084
|
+
nonce,
|
|
41085
|
+
hasDeadline: registerPermissionHasDeadline,
|
|
41086
|
+
deadline
|
|
41087
|
+
});
|
|
41088
|
+
const label = options.label ?? `${spec.label} (${options.template})`;
|
|
41089
|
+
say(
|
|
41090
|
+
() => console.log(
|
|
41091
|
+
`Pushing signing request \u2014 the mandate signer (${permissionSigner}) must sign in the browser.`
|
|
41092
|
+
)
|
|
41093
|
+
);
|
|
41094
|
+
const response = await channel.requestSignature({
|
|
41095
|
+
type: "typed-data",
|
|
41096
|
+
kind: "register-permission",
|
|
41097
|
+
title: `Authorize "${label}"`,
|
|
41098
|
+
description: `Sign to authorize a new ${spec.label} permission on your SMA. The agent deploys and registers it in one transaction.`,
|
|
41099
|
+
chainId: project.chainId,
|
|
41100
|
+
details: [
|
|
41101
|
+
{ label: "SMA", value: sma },
|
|
41102
|
+
{ label: "Permission (predicted)", value: clone },
|
|
41103
|
+
{ label: "Template", value: options.template },
|
|
41104
|
+
{ label: "Mandate signer", value: permissionSigner },
|
|
41105
|
+
...spec.describe(initParams)
|
|
41106
|
+
],
|
|
41107
|
+
typedData
|
|
41108
|
+
});
|
|
41109
|
+
if (response.status === "rejected") {
|
|
41110
|
+
throw new Error(`User rejected authorization: ${response.reason ?? "no reason given"}`);
|
|
41111
|
+
}
|
|
41112
|
+
if (response.status !== "signature") {
|
|
41113
|
+
throw new Error(`Expected EIP-712 signature response, got: ${response.status}`);
|
|
41114
|
+
}
|
|
41115
|
+
const signature = response.signature;
|
|
41116
|
+
try {
|
|
41117
|
+
const recoveredSigner = await recoverTypedDataAddress({
|
|
41118
|
+
domain: sailKernelDomain({ chainId: project.chainId, kernel: project.contracts.kernel }),
|
|
41119
|
+
types: registerPermissionHasDeadline ? REGISTER_PERMISSION_TYPES : REGISTER_PERMISSION_TYPES_NO_DEADLINE,
|
|
41120
|
+
primaryType: "RegisterPermission",
|
|
41121
|
+
message: registerPermissionHasDeadline ? { account: sma, permission: clone, nonce, deadline } : { account: sma, permission: clone, nonce },
|
|
41122
|
+
signature
|
|
41123
|
+
});
|
|
41124
|
+
if (recoveredSigner.toLowerCase() !== permissionSigner.toLowerCase()) {
|
|
41125
|
+
throw new Error(
|
|
41126
|
+
`Security: RegisterPermission was signed by ${recoveredSigner} but the on-chain mandate signer is ${permissionSigner}.
|
|
41127
|
+
Connect the owner wallet (mandate signer) in the browser \u2014 the agent wallet must never sign permission registrations.`
|
|
41128
|
+
);
|
|
41129
|
+
}
|
|
41130
|
+
} catch (err) {
|
|
41131
|
+
if (err.message.startsWith("Security:")) throw err;
|
|
41132
|
+
}
|
|
41133
|
+
const fee = await estimatePermissionFee(publicClient, project.contracts.governance, clone);
|
|
41134
|
+
if (!registerPermissionHasDeadline || deadline === void 0) {
|
|
41135
|
+
throw new Error(
|
|
41136
|
+
"deploy-clone requires a selective kernel (RegisterPermission with deadline). This chain's kernel does not match."
|
|
41137
|
+
);
|
|
41138
|
+
}
|
|
41139
|
+
say(() => console.log(`Submitting deployAndAttach (agent pays gas; fee ${fee} wei)\u2026`));
|
|
41140
|
+
const walletClient = createWalletClient({
|
|
41141
|
+
account: agentSigner.viemAccount,
|
|
41142
|
+
chain: chain2,
|
|
41143
|
+
transport: http(getRpcUrl(project.chainId))
|
|
41144
|
+
});
|
|
41145
|
+
const data = encodeFunctionData({
|
|
41146
|
+
abi: PERMISSION_FACTORY_ABI,
|
|
41147
|
+
functionName: "deployAndAttach",
|
|
41148
|
+
args: [sma, impl, salt, initData, deadline, signature]
|
|
41149
|
+
});
|
|
41150
|
+
const txHash = await walletClient.sendTransaction({
|
|
41151
|
+
to: project.contracts.permissionFactory,
|
|
41152
|
+
data,
|
|
41153
|
+
value: fee,
|
|
41154
|
+
account: agentSigner.viemAccount,
|
|
41155
|
+
chain: chain2
|
|
41156
|
+
});
|
|
41157
|
+
say(() => console.log("Waiting for confirmation\u2026"));
|
|
41158
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
41159
|
+
if (receipt.status !== "success") {
|
|
41160
|
+
throw new Error(`deployAndAttach reverted (tx ${txHash})`);
|
|
41161
|
+
}
|
|
41162
|
+
const attached = await pollForPermission(publicClient, project.contracts.kernel, sma, clone);
|
|
41163
|
+
if (!attached) {
|
|
41164
|
+
throw new Error(
|
|
41165
|
+
`Tx ${txHash} mined, but clone ${clone} is not in getPermissions(${sma}). Verify on-chain.`
|
|
41166
|
+
);
|
|
41167
|
+
}
|
|
41168
|
+
say(() => console.log("\u2713", `Deployed + registered ${spec.label} at ${clone}`));
|
|
41169
|
+
const store = new MandateStore();
|
|
41170
|
+
store.add({
|
|
41171
|
+
name: label,
|
|
41172
|
+
address: clone,
|
|
41173
|
+
txHash,
|
|
41174
|
+
chainId: project.chainId,
|
|
41175
|
+
deployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41176
|
+
});
|
|
41177
|
+
store.recordAttachment(clone, { sma, txHash });
|
|
41178
|
+
appendActivity({
|
|
41179
|
+
ts: nowIso(),
|
|
41180
|
+
actor: "agent",
|
|
41181
|
+
type: "permission_registered",
|
|
41182
|
+
permission: clone,
|
|
41183
|
+
name: label,
|
|
41184
|
+
sma,
|
|
41185
|
+
txHash,
|
|
41186
|
+
chainId: project.chainId
|
|
41187
|
+
});
|
|
41188
|
+
emit(json, () => {
|
|
41189
|
+
}, {
|
|
41190
|
+
status: "ok",
|
|
41191
|
+
clone: { template: options.template, address: clone, impl, txHash, sma, chainId: project.chainId }
|
|
40881
41192
|
});
|
|
40882
41193
|
}
|
|
40883
41194
|
async function mandateAttach(options) {
|
|
@@ -40894,15 +41205,17 @@ async function mandateAttach(options) {
|
|
|
40894
41205
|
}
|
|
40895
41206
|
async function runAttach(project, channel, options) {
|
|
40896
41207
|
const json = !!options.json;
|
|
40897
|
-
if (!isAddress(options.sma)) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
41208
|
+
if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
41209
|
+
const sma = getAddress(options.sma);
|
|
40898
41210
|
const store = new MandateStore();
|
|
40899
41211
|
const tracked = store.find(options.address);
|
|
40900
|
-
const
|
|
40901
|
-
if (!isAddress(
|
|
41212
|
+
const rawAddress = tracked?.address ?? options.address;
|
|
41213
|
+
if (!isAddress(rawAddress, { strict: false })) {
|
|
40902
41214
|
throw new Error(
|
|
40903
41215
|
`--address must be a deployed mandate address or a tracked name: ${options.address}`
|
|
40904
41216
|
);
|
|
40905
41217
|
}
|
|
41218
|
+
const mandateAddress = getAddress(rawAddress);
|
|
40906
41219
|
const label = options.label ?? tracked?.name ?? "mandate";
|
|
40907
41220
|
const publicClient = publicClientFor(project);
|
|
40908
41221
|
if (!json) {
|
|
@@ -40917,16 +41230,16 @@ async function runAttach(project, channel, options) {
|
|
|
40917
41230
|
project,
|
|
40918
41231
|
channel,
|
|
40919
41232
|
publicClient,
|
|
40920
|
-
|
|
41233
|
+
sma,
|
|
40921
41234
|
mandateAddress,
|
|
40922
41235
|
label,
|
|
40923
41236
|
json
|
|
40924
41237
|
);
|
|
40925
|
-
if (tracked) store.recordAttachment(mandateAddress, { sma
|
|
41238
|
+
if (tracked) store.recordAttachment(mandateAddress, { sma, txHash });
|
|
40926
41239
|
emit(json, () => {
|
|
40927
41240
|
}, {
|
|
40928
41241
|
status: "ok",
|
|
40929
|
-
attached: { sma
|
|
41242
|
+
attached: { sma, mandate: mandateAddress, txHash }
|
|
40930
41243
|
});
|
|
40931
41244
|
}
|
|
40932
41245
|
async function mandateRevoke(options) {
|
|
@@ -40946,11 +41259,11 @@ async function runRevoke(project, channel, options) {
|
|
|
40946
41259
|
const say = (fn) => {
|
|
40947
41260
|
if (!json) fn();
|
|
40948
41261
|
};
|
|
40949
|
-
if (!isAddress(options.sma)) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
41262
|
+
if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
40950
41263
|
if (!options.all && !options.address) {
|
|
40951
41264
|
throw new Error("Provide --address <permission> (or a tracked name), or --all");
|
|
40952
41265
|
}
|
|
40953
|
-
const sma = options.sma;
|
|
41266
|
+
const sma = getAddress(options.sma);
|
|
40954
41267
|
const kernel = project.contracts.kernel;
|
|
40955
41268
|
const publicClient = publicClientFor(project);
|
|
40956
41269
|
const onchain = await publicClient.readContract({
|
|
@@ -40966,10 +41279,11 @@ async function runRevoke(project, channel, options) {
|
|
|
40966
41279
|
targets = onchain;
|
|
40967
41280
|
} else {
|
|
40968
41281
|
const tracked = store.find(options.address);
|
|
40969
|
-
const
|
|
40970
|
-
if (!isAddress(
|
|
41282
|
+
const rawWanted = tracked?.address ?? options.address;
|
|
41283
|
+
if (!isAddress(rawWanted, { strict: false })) {
|
|
40971
41284
|
throw new Error(`--address must be a permission address or a tracked name: ${options.address}`);
|
|
40972
41285
|
}
|
|
41286
|
+
const wanted = getAddress(rawWanted);
|
|
40973
41287
|
const match = onchain.find((p) => p.toLowerCase() === wanted.toLowerCase());
|
|
40974
41288
|
if (!match) {
|
|
40975
41289
|
throw new Error(`${wanted} is not in the SMA's current permission set; nothing to revoke.`);
|
|
@@ -41291,9 +41605,29 @@ function runForgeBuild() {
|
|
|
41291
41605
|
}
|
|
41292
41606
|
|
|
41293
41607
|
// src/commands/mandate.ts
|
|
41294
|
-
|
|
41608
|
+
init_esm2();
|
|
41609
|
+
async function fetchOnChainPermissions(account2) {
|
|
41610
|
+
try {
|
|
41611
|
+
const project = new ProjectContext();
|
|
41612
|
+
const rpcUrl = getRpcUrl(project.chainId) ?? getChainById(project.chainId).rpcUrls.default.http[0];
|
|
41613
|
+
const pc = createPublicClient({
|
|
41614
|
+
chain: getChainById(project.chainId),
|
|
41615
|
+
transport: http(rpcUrl)
|
|
41616
|
+
});
|
|
41617
|
+
const onChain = await pc.readContract({
|
|
41618
|
+
address: project.contracts.kernel,
|
|
41619
|
+
abi: SailKernelAbi,
|
|
41620
|
+
functionName: "getPermissions",
|
|
41621
|
+
args: [account2.safe]
|
|
41622
|
+
});
|
|
41623
|
+
return new Set(onChain.map((a) => a.toLowerCase()));
|
|
41624
|
+
} catch {
|
|
41625
|
+
return null;
|
|
41626
|
+
}
|
|
41627
|
+
}
|
|
41628
|
+
async function trackedPermissionsFor(account2) {
|
|
41295
41629
|
const store = new MandateStore();
|
|
41296
|
-
|
|
41630
|
+
const local = store.list().filter((m) => m.chainId === account2.chainId).map((m) => {
|
|
41297
41631
|
const attachment = m.attachments?.find(
|
|
41298
41632
|
(a) => a.sma.toLowerCase() === account2.safe.toLowerCase()
|
|
41299
41633
|
);
|
|
@@ -41304,6 +41638,15 @@ function trackedPermissionsFor(account2) {
|
|
|
41304
41638
|
attachedAt: attachment?.at
|
|
41305
41639
|
};
|
|
41306
41640
|
});
|
|
41641
|
+
const onChain = await fetchOnChainPermissions(account2);
|
|
41642
|
+
if (onChain !== null) {
|
|
41643
|
+
for (const p of local) {
|
|
41644
|
+
if (p.registeredOnSma && !onChain.has(p.address.toLowerCase())) {
|
|
41645
|
+
p.revokedOnChain = true;
|
|
41646
|
+
}
|
|
41647
|
+
}
|
|
41648
|
+
}
|
|
41649
|
+
return local;
|
|
41307
41650
|
}
|
|
41308
41651
|
function printNoPermissionsGuidance() {
|
|
41309
41652
|
console.log(
|
|
@@ -41315,7 +41658,7 @@ async function mandatePrepare() {
|
|
|
41315
41658
|
if (!account2) {
|
|
41316
41659
|
throw new Error('No account found at .sail/account.json.\nRun "sailor account create" first.');
|
|
41317
41660
|
}
|
|
41318
|
-
const permissions = trackedPermissionsFor(account2);
|
|
41661
|
+
const permissions = await trackedPermissionsFor(account2);
|
|
41319
41662
|
if (permissions.length === 0) {
|
|
41320
41663
|
printNoPermissionsGuidance();
|
|
41321
41664
|
return;
|
|
@@ -41324,7 +41667,7 @@ async function mandatePrepare() {
|
|
|
41324
41667
|
${permissions.length} permission(s) tracked for SMA ${account2.safe}:
|
|
41325
41668
|
`);
|
|
41326
41669
|
for (const p of permissions) {
|
|
41327
|
-
const status2 = p.registeredOnSma ? `registered on this SMA${p.attachedAt ? ` (${p.attachedAt})` : ""}` : "not yet registered on this SMA";
|
|
41670
|
+
const status2 = p.revokedOnChain ? "revoked on-chain (local record is stale)" : p.registeredOnSma ? `registered on this SMA${p.attachedAt ? ` (${p.attachedAt})` : ""}` : "not yet registered on this SMA";
|
|
41328
41671
|
console.log(`\u2022 ${p.label}`);
|
|
41329
41672
|
console.log(` ${p.address}`);
|
|
41330
41673
|
console.log(` ${status2}`);
|
|
@@ -41332,7 +41675,8 @@ ${permissions.length} permission(s) tracked for SMA ${account2.safe}:
|
|
|
41332
41675
|
const draft = {
|
|
41333
41676
|
account: account2.safe,
|
|
41334
41677
|
chainId: account2.chainId,
|
|
41335
|
-
|
|
41678
|
+
// Exclude permissions revoked on-chain — the draft reflects the live set.
|
|
41679
|
+
permissions: permissions.filter((p) => !p.revokedOnChain).map((p) => ({ address: p.address, label: p.label })),
|
|
41336
41680
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
41337
41681
|
};
|
|
41338
41682
|
writeJsonFile(sailPath("mandate-draft.json"), draft);
|
|
@@ -41343,7 +41687,7 @@ async function mandateSign(opts = {}) {
|
|
|
41343
41687
|
if (!account2) {
|
|
41344
41688
|
throw new Error('No account found at .sail/account.json.\nRun "sailor account create" first.');
|
|
41345
41689
|
}
|
|
41346
|
-
const permissions = trackedPermissionsFor(account2);
|
|
41690
|
+
const permissions = await trackedPermissionsFor(account2);
|
|
41347
41691
|
if (permissions.length === 0) {
|
|
41348
41692
|
printNoPermissionsGuidance();
|
|
41349
41693
|
return;
|
|
@@ -41353,22 +41697,25 @@ Permissions tracked for SMA ${account2.safe}:
|
|
|
41353
41697
|
`);
|
|
41354
41698
|
for (const p of permissions) {
|
|
41355
41699
|
console.log(`\u2022 ${p.label} (${p.address})`);
|
|
41356
|
-
console.log(
|
|
41700
|
+
console.log(
|
|
41701
|
+
` ${p.revokedOnChain ? "revoked on-chain (local record is stale)" : p.registeredOnSma ? "registered on-chain" : "NOT yet registered on this SMA"}`
|
|
41702
|
+
);
|
|
41357
41703
|
}
|
|
41358
41704
|
console.log(
|
|
41359
41705
|
"\nNote: `sailor mandate sign` reviews and confirms the permissions attached to your SMA.\nOn-chain registration happens via `sailor mandate attach` (or `sailor mandate deploy --attach`)."
|
|
41360
41706
|
);
|
|
41707
|
+
const activePermissions = permissions.filter((p) => !p.revokedOnChain);
|
|
41361
41708
|
const proceed = opts.yes || await confirm(
|
|
41362
|
-
`Confirm these ${
|
|
41709
|
+
`Confirm these ${activePermissions.length} permission(s) are authorized for your SMA?`
|
|
41363
41710
|
);
|
|
41364
41711
|
if (!proceed) {
|
|
41365
41712
|
console.log("No permissions confirmed.");
|
|
41366
41713
|
return;
|
|
41367
41714
|
}
|
|
41368
|
-
const unregistered =
|
|
41715
|
+
const unregistered = activePermissions.filter((p) => !p.registeredOnSma);
|
|
41369
41716
|
if (unregistered.length === 0) {
|
|
41370
41717
|
console.log(`
|
|
41371
|
-
\u2713 Confirmed ${
|
|
41718
|
+
\u2713 Confirmed ${activePermissions.length} permission(s) for ${account2.safe}.`);
|
|
41372
41719
|
} else {
|
|
41373
41720
|
console.log(
|
|
41374
41721
|
`
|
|
@@ -41384,7 +41731,8 @@ ${unregistered.length} permission(s) are not yet registered on this SMA. Initiat
|
|
|
41384
41731
|
signedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41385
41732
|
signature: "",
|
|
41386
41733
|
registeredOnChain: true,
|
|
41387
|
-
|
|
41734
|
+
// Only include permissions that are currently active on-chain.
|
|
41735
|
+
permissions: activePermissions.map((p) => ({ template: p.label, params: {} }))
|
|
41388
41736
|
};
|
|
41389
41737
|
writeJsonFile(sailPath("mandate.json"), storedMandate);
|
|
41390
41738
|
console.log(`
|
|
@@ -41723,14 +42071,15 @@ async function resolveNewManager(options, oldManager, json, say) {
|
|
|
41723
42071
|
if (!isAddress(options.to, { strict: false })) {
|
|
41724
42072
|
throw new Error(`Invalid --to address: ${options.to}`);
|
|
41725
42073
|
}
|
|
42074
|
+
const to = getAddress(options.to);
|
|
41726
42075
|
say(
|
|
41727
42076
|
() => console.log(
|
|
41728
42077
|
`
|
|
41729
|
-
Rotating to existing address ${
|
|
42078
|
+
Rotating to existing address ${to}. The local agent keystore is left unchanged \u2014
|
|
41730
42079
|
ensure the agent that signs dispatches holds this key.`
|
|
41731
42080
|
)
|
|
41732
42081
|
);
|
|
41733
|
-
return
|
|
42082
|
+
return to;
|
|
41734
42083
|
}
|
|
41735
42084
|
if (json) {
|
|
41736
42085
|
throw new Error("Pass --to <address> in --json mode (key generation is interactive).");
|
|
@@ -42114,7 +42463,8 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
|
|
|
42114
42463
|
} catch (err) {
|
|
42115
42464
|
console.error(`could not read registered permissions: ${err.message}`);
|
|
42116
42465
|
}
|
|
42117
|
-
let
|
|
42466
|
+
let tickExecuted = 0;
|
|
42467
|
+
let tickReverted = 0;
|
|
42118
42468
|
let tickSkipped = 0;
|
|
42119
42469
|
for (const rawDispatch of dispatches) {
|
|
42120
42470
|
const dispatch = rawDispatch;
|
|
@@ -42193,9 +42543,15 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
|
|
|
42193
42543
|
} catch {
|
|
42194
42544
|
}
|
|
42195
42545
|
}
|
|
42196
|
-
|
|
42197
|
-
|
|
42198
|
-
|
|
42546
|
+
if (!result.success) {
|
|
42547
|
+
appendActivity({ ts: nowIso(), actor: "agent", type: "dispatch_reverted", permission, target, txHash: result.txHash, gasUsed: String(result.gasUsed) });
|
|
42548
|
+
console.error(`reverted: ${result.txHash} (gas used: ${result.gasUsed})`);
|
|
42549
|
+
tickReverted++;
|
|
42550
|
+
} else {
|
|
42551
|
+
appendActivity({ ts: nowIso(), actor: "agent", type: "dispatch_executed", permission, target, txHash: result.txHash });
|
|
42552
|
+
console.log(`executed: ${result.txHash}`);
|
|
42553
|
+
tickExecuted++;
|
|
42554
|
+
}
|
|
42199
42555
|
} catch (err) {
|
|
42200
42556
|
const reason = err.message;
|
|
42201
42557
|
console.error(`dispatch error: ${reason}`);
|
|
@@ -42204,13 +42560,10 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
|
|
|
42204
42560
|
}
|
|
42205
42561
|
}
|
|
42206
42562
|
if (dispatches.length > 0) {
|
|
42207
|
-
|
|
42208
|
-
|
|
42209
|
-
|
|
42210
|
-
|
|
42211
|
-
} else {
|
|
42212
|
-
console.log(`tick complete: ${tickDispatched} dispatched`);
|
|
42213
|
-
}
|
|
42563
|
+
const parts = [`${tickExecuted} executed`];
|
|
42564
|
+
if (tickReverted > 0) parts.push(`${tickReverted} reverted`);
|
|
42565
|
+
if (tickSkipped > 0) parts.push(`${tickSkipped} skipped`);
|
|
42566
|
+
console.log(`tick complete: ${parts.join(", ")}`);
|
|
42214
42567
|
}
|
|
42215
42568
|
appendActivity({ ts: nowIso(), actor: "agent", type: "tick_end" });
|
|
42216
42569
|
}
|
|
@@ -42265,8 +42618,8 @@ async function scan(options) {
|
|
|
42265
42618
|
process.exit(1);
|
|
42266
42619
|
}
|
|
42267
42620
|
const project = new ProjectContext();
|
|
42268
|
-
const
|
|
42269
|
-
if (!
|
|
42621
|
+
const rawOwner = options.owner ?? project.getOwner() ?? void 0;
|
|
42622
|
+
if (!rawOwner || !isAddress(rawOwner, { strict: false })) {
|
|
42270
42623
|
emit(
|
|
42271
42624
|
options.json,
|
|
42272
42625
|
() => console.log(
|
|
@@ -42276,6 +42629,7 @@ async function scan(options) {
|
|
|
42276
42629
|
);
|
|
42277
42630
|
process.exit(1);
|
|
42278
42631
|
}
|
|
42632
|
+
const owner2 = getAddress(rawOwner);
|
|
42279
42633
|
const chainId = project.chainId;
|
|
42280
42634
|
const kernel = project.contracts.kernel;
|
|
42281
42635
|
const publicClient = createPublicClient({
|
|
@@ -42699,6 +43053,9 @@ ui.action(action(uiCommand));
|
|
|
42699
43053
|
var keys = program2.command("keys").description("Manage local signing keys");
|
|
42700
43054
|
keys.command("generate").description("Generate and encrypt an agent wallet or mandate signer key").action(action(keysGenerate));
|
|
42701
43055
|
keys.command("show").description("Show the address of each stored key").action(action(keysShow));
|
|
43056
|
+
keys.command("export-ci").description(
|
|
43057
|
+
"Copy the encrypted agent wallet keystore to ci-keystore.json for committing to CI"
|
|
43058
|
+
).action(action(keysExportCi));
|
|
42702
43059
|
var account = program2.command("account").description("Manage the Sail SMA");
|
|
42703
43060
|
account.command("create").description("Create a new Sail SMA on-chain").action(action(accountCreate));
|
|
42704
43061
|
account.command("rotate-signer").description("Rotate the SMA's delegated signer (agent wallet) and re-approve its mandates").option("--sma <address>", "SMA to rotate (defaults to the active account)").option("--to <address>", "Rotate to an existing agent-wallet address instead of generating one").option("--generate", "Generate a fresh local agent wallet (default when --to is omitted)").option("--skip-reattach", "Do not re-approve the previously-attached mandates").option("--reattach-only", "Skip rotation; only re-approve mandates (resume after funding)").option("--json", "Machine-readable output").action(actionWith(rotateSigner));
|
|
@@ -42707,6 +43064,7 @@ mandate.command("prepare").description("Prepare a mandate draft for review and s
|
|
|
42707
43064
|
mandate.command("sign").description("Review and confirm the permissions authorized for your SMA").option("--yes", "Skip the confirmation prompt (for non-interactive / CI use)").action(actionWith(mandateSign));
|
|
42708
43065
|
mandate.command("deploy").description("Deploy a Foundry-compiled permission contract via the browser signing UI").option("--artifact <path>", "Path to the Foundry artifact JSON (out/<Name>.sol/<Name>.json)").option("--contract <name>", "Contract name; resolves to <out>/<name>.sol/<name>.json").option("--out <dir>", "Foundry output directory", "out").option("--name <label>", "Label to track this permission under (defaults to contract name)").option("--args <json>", `Constructor args as a JSON array, e.g. '[["0x.."],"1000"]'`).option("--build", "Run `forge build` before deploying").option("--attach", "After deploy, register the permission on --sma").option("--sma <address>", "SMA to register on (required with --attach)").option("--json", "Emit machine-readable JSON").action(actionWith(mandateDeploy));
|
|
42709
43066
|
mandate.command("attach").description("Register an already-deployed permission on an SMA (EIP-712 RegisterPermission)").requiredOption("--address <mandateOrName>", "Permission address, or a name tracked locally").requiredOption("--sma <address>", "SMA to register the permission on").option("--label <label>", "Human-readable label shown in the signing UI").option("--json", "Emit machine-readable JSON").action(actionWith(mandateAttach));
|
|
43067
|
+
mandate.command("deploy-clone").description("Deploy + register a standalone clone permission (e.g. boundedApprove) via the signing UI").requiredOption("--template <key>", "Standalone clone template key (e.g. boundedApprove)").requiredOption("--sma <address>", "SMA to deploy the clone for and register it on").option("--tokens <csv>", "Comma-separated allowed token addresses").option("--spenders <csv>", "Comma-separated allowed spender addresses").option("--max <amount>", "Max amount per tx in base units (default: uint256 max)").option("--label <label>", "Human-readable label to track this permission under").option("--json", "Emit machine-readable JSON").action(actionWith(mandateDeployClone));
|
|
42710
43068
|
mandate.command("revoke").description("Revoke permission(s) from an SMA (EIP-712 RevokePermissions, owner-authorized)").option("--address <permissionOrName>", "Permission address, or a name tracked locally").requiredOption("--sma <address>", "Safe (SMA) to revoke the permission(s) from").option("--all", "Revoke every permission currently registered on the SMA").option("--json", "Output JSON").action(actionWith(mandateRevoke));
|
|
42711
43069
|
mandate.command("templates").description("Show how to author your own permission contract (and any community-deployed addresses)").option("--json", "Emit machine-readable JSON").action(actionWith(mandateTemplates));
|
|
42712
43070
|
mandate.command("list").description("List permission contracts deployed from this project").action(action(async () => mandateContractsList()));
|
|
@@ -42744,7 +43102,6 @@ function stub(name, description) {
|
|
|
42744
43102
|
console.log(`sailor ${name}: not implemented yet`);
|
|
42745
43103
|
});
|
|
42746
43104
|
}
|
|
42747
|
-
stub("setup", "Walk through the Sailor setup guide");
|
|
42748
43105
|
stub("dispatch preview", "Preview a dispatch without submitting");
|
|
42749
43106
|
program2.parse(process.argv);
|
|
42750
43107
|
/*! Bundled license information:
|