thinyai 0.1.1 → 0.1.3
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/bin.js +572 -196
- package/package.json +6 -3
package/dist/bin.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { createInterface } from "readline/promises";
|
|
5
|
-
import { clearLine, cursorTo } from "readline";
|
|
5
|
+
import { clearLine, cursorTo, emitKeypressEvents } from "readline";
|
|
6
6
|
import { stdin, stdout } from "process";
|
|
7
|
-
import { mkdirSync } from "fs";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { z as
|
|
7
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
8
|
+
import { homedir as homedir2 } from "os";
|
|
9
|
+
import { join as join2 } from "path";
|
|
10
|
+
import { z as z6 } from "zod";
|
|
11
11
|
|
|
12
12
|
// ../../packages/core/src/domain/messages.ts
|
|
13
13
|
var systemMessage = (content) => ({ role: "system", content });
|
|
@@ -452,9 +452,9 @@ async function createAgent(config) {
|
|
|
452
452
|
const seed = config.systemPrompt && !history.some((m) => m.role === "system") ? [systemMessage(config.systemPrompt), ...history] : history;
|
|
453
453
|
const generate = composeModel(extensions.middleware.model, async (req) => {
|
|
454
454
|
if (opts.onToken && config.model.stream) {
|
|
455
|
-
return assembleStream(config.model.stream(req.messages, req.tools), opts.onToken);
|
|
455
|
+
return assembleStream(config.model.stream(req.messages, req.tools, opts.signal), opts.onToken);
|
|
456
456
|
}
|
|
457
|
-
return config.model.generate(req.messages, req.tools);
|
|
457
|
+
return config.model.generate(req.messages, req.tools, opts.signal);
|
|
458
458
|
});
|
|
459
459
|
const runComposedTool = composeTool(
|
|
460
460
|
extensions.middleware.tool,
|
|
@@ -829,44 +829,38 @@ function buildToolOptions(tools) {
|
|
|
829
829
|
if (tools.length === 0) return { tools: void 0, toolChoice: void 0 };
|
|
830
830
|
return { tools: toAiTools(tools), toolChoice: "auto" };
|
|
831
831
|
}
|
|
832
|
+
var KNOWN_PROVIDERS = /* @__PURE__ */ new Set(["openai", "openai-compat", "anthropic"]);
|
|
832
833
|
function resolveModel(model, opts) {
|
|
833
834
|
if (typeof model !== "string") return model;
|
|
834
835
|
const colonIdx = model.indexOf(":");
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
836
|
+
const prefix = colonIdx === -1 ? "" : model.slice(0, colonIdx);
|
|
837
|
+
if (KNOWN_PROVIDERS.has(prefix)) {
|
|
838
|
+
const modelId = model.slice(colonIdx + 1);
|
|
839
|
+
if (prefix === "anthropic") {
|
|
840
|
+
return createAnthropic({ baseURL: opts.anthropic?.baseURL, apiKey: opts.anthropic?.apiKey })(
|
|
841
|
+
modelId
|
|
839
842
|
);
|
|
840
843
|
}
|
|
841
|
-
return createOpenAI({ baseURL: opts.openai?.baseURL, apiKey: opts.openai?.apiKey })(model);
|
|
842
|
-
}
|
|
843
|
-
const provider = model.slice(0, colonIdx);
|
|
844
|
-
const modelId = model.slice(colonIdx + 1);
|
|
845
|
-
if (provider === "openai" || provider === "openai-compat") {
|
|
846
844
|
return createOpenAI({ baseURL: opts.openai?.baseURL, apiKey: opts.openai?.apiKey })(modelId);
|
|
847
845
|
}
|
|
848
|
-
if (
|
|
849
|
-
return createAnthropic({ baseURL: opts.anthropic
|
|
850
|
-
|
|
846
|
+
if (opts.anthropic?.baseURL) {
|
|
847
|
+
return createAnthropic({ baseURL: opts.anthropic.baseURL, apiKey: opts.anthropic.apiKey })(
|
|
848
|
+
model
|
|
851
849
|
);
|
|
852
850
|
}
|
|
853
|
-
|
|
854
|
-
`unknown provider "${provider}" in model string "${model}"
|
|
855
|
-
Supported prefixes: "openai:<id>", "openai-compat:<id>", "anthropic:<id>"
|
|
856
|
-
Or omit the prefix and set THINY_OPENAI_BASE_URL / THINY_ANTHROPIC_BASE_URL instead.
|
|
857
|
-
Or pass a LanguageModel instance directly.`
|
|
858
|
-
);
|
|
851
|
+
return createOpenAI({ baseURL: opts.openai?.baseURL, apiKey: opts.openai?.apiKey })(model);
|
|
859
852
|
}
|
|
860
853
|
function aiSdkModel(opts) {
|
|
861
854
|
const model = resolveModel(opts.model, opts);
|
|
862
855
|
const maxRetries = opts.maxRetries ?? 2;
|
|
863
856
|
return {
|
|
864
|
-
async generate(messages, tools) {
|
|
857
|
+
async generate(messages, tools, signal) {
|
|
865
858
|
const result = await generateText({
|
|
866
859
|
model,
|
|
867
860
|
messages: toCoreMessages(messages),
|
|
868
861
|
...buildToolOptions(tools),
|
|
869
|
-
maxRetries
|
|
862
|
+
maxRetries,
|
|
863
|
+
abortSignal: signal
|
|
870
864
|
});
|
|
871
865
|
return {
|
|
872
866
|
text: result.text || void 0,
|
|
@@ -880,12 +874,13 @@ function aiSdkModel(opts) {
|
|
|
880
874
|
usage: normalizeUsage(result.usage)
|
|
881
875
|
};
|
|
882
876
|
},
|
|
883
|
-
async *stream(messages, tools) {
|
|
877
|
+
async *stream(messages, tools, signal) {
|
|
884
878
|
const result = streamText({
|
|
885
879
|
model,
|
|
886
880
|
messages: toCoreMessages(messages),
|
|
887
881
|
...buildToolOptions(tools),
|
|
888
|
-
maxRetries
|
|
882
|
+
maxRetries,
|
|
883
|
+
abortSignal: signal
|
|
889
884
|
});
|
|
890
885
|
for await (const part of result.fullStream) {
|
|
891
886
|
if (part.type === "text-delta") {
|
|
@@ -956,7 +951,7 @@ function pinoLogger(opts = {}) {
|
|
|
956
951
|
);
|
|
957
952
|
}
|
|
958
953
|
if (opts.file) {
|
|
959
|
-
const destination = pino2.destination({ dest: opts.file, sync:
|
|
954
|
+
const destination = pino2.destination({ dest: opts.file, sync: true });
|
|
960
955
|
return adaptPinoLogger(pino2(pinoConfig(level), destination));
|
|
961
956
|
}
|
|
962
957
|
const usePretty = opts.pretty ?? (opts.file === void 0 && process.env.NODE_ENV !== "production");
|
|
@@ -1315,6 +1310,300 @@ ${context}` : task;
|
|
|
1315
1310
|
return { name: "agents", tools: [delegateTask, updatePlan] };
|
|
1316
1311
|
}
|
|
1317
1312
|
|
|
1313
|
+
// ../../packages/adapters/signer-sui/src/index.ts
|
|
1314
|
+
import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc";
|
|
1315
|
+
import { Transaction } from "@mysten/sui/transactions";
|
|
1316
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
1317
|
+
var ZERO_ADDRESS = `0x${"0".repeat(64)}`;
|
|
1318
|
+
function suiSigner(opts = {}) {
|
|
1319
|
+
const network = opts.network ?? "testnet";
|
|
1320
|
+
const client = opts.client ?? new SuiJsonRpcClient({ url: opts.rpcUrl ?? getJsonRpcFullnodeUrl(network), network });
|
|
1321
|
+
const keypair = opts.signer ?? (opts.secretKey ? Ed25519Keypair.fromSecretKey(opts.secretKey) : void 0);
|
|
1322
|
+
const address = keypair?.getPublicKey().toSuiAddress();
|
|
1323
|
+
return {
|
|
1324
|
+
address,
|
|
1325
|
+
network,
|
|
1326
|
+
client,
|
|
1327
|
+
hasKey: () => keypair !== void 0,
|
|
1328
|
+
devInspect(tx, devOpts) {
|
|
1329
|
+
return client.devInspectTransactionBlock({
|
|
1330
|
+
sender: devOpts?.sender ?? address ?? ZERO_ADDRESS,
|
|
1331
|
+
transactionBlock: tx
|
|
1332
|
+
});
|
|
1333
|
+
},
|
|
1334
|
+
async signAndExecute(tx) {
|
|
1335
|
+
if (!keypair) {
|
|
1336
|
+
throw new Error("suiSigner.signAndExecute: no key configured (pass `secretKey` or `signer`).");
|
|
1337
|
+
}
|
|
1338
|
+
if (network === "mainnet" && opts.allowMainnet !== true) {
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
"suiSigner: refusing to sign on mainnet. Pass `allowMainnet: true` to opt in."
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const result = await client.signAndExecuteTransaction({
|
|
1344
|
+
signer: keypair,
|
|
1345
|
+
transaction: tx,
|
|
1346
|
+
options: { showEffects: true }
|
|
1347
|
+
});
|
|
1348
|
+
await client.waitForTransaction({ digest: result.digest });
|
|
1349
|
+
return { digest: result.digest, effects: result.effects ?? null };
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ../../packages/plugins/sui/src/index.ts
|
|
1355
|
+
import { z as z4 } from "zod";
|
|
1356
|
+
import { Transaction as Transaction2 } from "@mysten/sui/transactions";
|
|
1357
|
+
function explorerTxUrl(network, digest) {
|
|
1358
|
+
return `https://suiscan.xyz/${network}/tx/${digest}`;
|
|
1359
|
+
}
|
|
1360
|
+
function suiPlugin(opts) {
|
|
1361
|
+
const sig = opts.signer;
|
|
1362
|
+
const resolve2 = typeof sig === "function" ? sig : () => sig;
|
|
1363
|
+
const signerOrThrow = () => {
|
|
1364
|
+
const s = resolve2();
|
|
1365
|
+
if (!s) {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
"No Sui wallet configured yet. Call sui_setup to create or import one (or tell the user to run `thiny sui init`)."
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
return s;
|
|
1371
|
+
};
|
|
1372
|
+
const requireSimSuccess = opts.policy?.requireSimSuccess ?? true;
|
|
1373
|
+
const executeTx = async (tx, toolName, approvalArgs, reason) => {
|
|
1374
|
+
const signer = signerOrThrow();
|
|
1375
|
+
const sim = await signer.devInspect(tx);
|
|
1376
|
+
const status = sim.effects.status.status;
|
|
1377
|
+
if (requireSimSuccess && status !== "success") {
|
|
1378
|
+
throw new Error(`${toolName}: simulation failed (${sim.effects.status.error ?? status})`);
|
|
1379
|
+
}
|
|
1380
|
+
if (opts.policy?.maxGasBudgetMist !== void 0) {
|
|
1381
|
+
const gas = sim.effects.gasUsed;
|
|
1382
|
+
const estGas = BigInt(gas.computationCost) + BigInt(gas.storageCost);
|
|
1383
|
+
if (estGas > opts.policy.maxGasBudgetMist) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`${toolName}: estimated gas ${estGas.toString()} MIST exceeds policy cap ${opts.policy.maxGasBudgetMist.toString()}.`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (opts.approver) {
|
|
1390
|
+
const ok = await opts.approver({ tool: toolName, args: approvalArgs, reason });
|
|
1391
|
+
if (!ok) throw new Error(`${toolName}: rejected by approver.`);
|
|
1392
|
+
}
|
|
1393
|
+
const { digest, effects } = await signer.signAndExecute(tx);
|
|
1394
|
+
return { digest, effects, explorerUrl: explorerTxUrl(signer.network, digest) };
|
|
1395
|
+
};
|
|
1396
|
+
const balance = defineTool({
|
|
1397
|
+
name: "sui_balance",
|
|
1398
|
+
description: "Read a Sui coin balance. Defaults to the agent's own address and SUI when omitted. Returns total balance in MIST plus the coin count.",
|
|
1399
|
+
parameters: z4.object({
|
|
1400
|
+
address: z4.string().optional().describe("Owner address (default: the agent's address)."),
|
|
1401
|
+
coinType: z4.string().optional().describe("Coin type, e.g. 0x2::sui::SUI (default: SUI).")
|
|
1402
|
+
}),
|
|
1403
|
+
execute: async ({ address, coinType }) => {
|
|
1404
|
+
const signer = signerOrThrow();
|
|
1405
|
+
const owner = address ?? signer.address;
|
|
1406
|
+
if (owner === void 0) {
|
|
1407
|
+
throw new Error("sui_balance: no address given and the signer has no key/address.");
|
|
1408
|
+
}
|
|
1409
|
+
const bal = await signer.client.getBalance({ owner, ...coinType ? { coinType } : {} });
|
|
1410
|
+
return {
|
|
1411
|
+
owner,
|
|
1412
|
+
coinType: bal.coinType,
|
|
1413
|
+
totalBalanceMist: bal.totalBalance,
|
|
1414
|
+
coins: bal.coinObjectCount
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
const object = defineTool({
|
|
1419
|
+
name: "sui_object",
|
|
1420
|
+
description: "Read a Sui object's type and fields by id.",
|
|
1421
|
+
parameters: z4.object({ objectId: z4.string().min(1).describe("The object id (0x\u2026).") }),
|
|
1422
|
+
execute: async ({ objectId }) => {
|
|
1423
|
+
const signer = signerOrThrow();
|
|
1424
|
+
const res = await signer.client.getObject({
|
|
1425
|
+
id: objectId,
|
|
1426
|
+
options: { showContent: true, showType: true, showOwner: true }
|
|
1427
|
+
});
|
|
1428
|
+
if (!res.data) throw new Error(`sui_object: ${objectId} not found`);
|
|
1429
|
+
const content = res.data.content;
|
|
1430
|
+
return {
|
|
1431
|
+
objectId,
|
|
1432
|
+
type: res.data.type,
|
|
1433
|
+
fields: content?.dataType === "moveObject" ? content.fields : void 0
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
const executePtb = defineTool({
|
|
1438
|
+
name: "sui_execute_ptb",
|
|
1439
|
+
description: "Sign and submit an unsigned Sui programmable transaction (PTB) that a builder/MCP produced. Re-simulates the PTB, applies the soft policy and approval gate, then signs + submits. Pass the builder's `unsignedPtb` (base64 of the serialized transaction) \u2014 built with NO sender and NO gas (the signer fills both). On-chain caps may still abort it.",
|
|
1440
|
+
sensitive: true,
|
|
1441
|
+
parameters: z4.object({
|
|
1442
|
+
unsignedPtb: z4.string().min(1).describe("The builder's unsigned PTB \u2014 base64 of a serialized Sui transaction.")
|
|
1443
|
+
}),
|
|
1444
|
+
execute: async ({ unsignedPtb }) => {
|
|
1445
|
+
const tx = Transaction2.from(Buffer.from(unsignedPtb, "base64").toString("utf8"));
|
|
1446
|
+
return await executeTx(tx, "sui_execute_ptb", { unsignedPtb }, "sign and submit a Sui PTB");
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
const transfer = defineTool({
|
|
1450
|
+
name: "sui_transfer",
|
|
1451
|
+
description: "Build, sign, and submit a coin transfer: send an amount of SUI (or any coin type) to an address. Amounts are in MIST (1 SUI = 1,000,000,000 MIST). Use this for simple sends.",
|
|
1452
|
+
sensitive: true,
|
|
1453
|
+
parameters: z4.object({
|
|
1454
|
+
recipient: z4.string().min(1).describe("Destination address (0x\u2026)."),
|
|
1455
|
+
amountMist: z4.string().min(1).describe('Amount in MIST. e.g. "1000000000" = 1 SUI.'),
|
|
1456
|
+
coinType: z4.string().optional().describe("Coin type (default 0x2::sui::SUI).")
|
|
1457
|
+
}),
|
|
1458
|
+
execute: async ({ recipient, amountMist, coinType }) => {
|
|
1459
|
+
const signer = signerOrThrow();
|
|
1460
|
+
const amount = BigInt(amountMist);
|
|
1461
|
+
const type = coinType ?? "0x2::sui::SUI";
|
|
1462
|
+
const tx = new Transaction2();
|
|
1463
|
+
if (type.endsWith("::sui::SUI")) {
|
|
1464
|
+
const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(amount)]);
|
|
1465
|
+
tx.transferObjects([coin], tx.pure.address(recipient));
|
|
1466
|
+
} else {
|
|
1467
|
+
const owner = signer.address;
|
|
1468
|
+
if (owner === void 0) throw new Error("sui_transfer: signer has no address.");
|
|
1469
|
+
const { data } = await signer.client.getCoins({ owner, coinType: type });
|
|
1470
|
+
const [first, ...rest] = data;
|
|
1471
|
+
if (!first) throw new Error(`sui_transfer: no ${type} coins owned by ${owner}.`);
|
|
1472
|
+
const primary = tx.object(first.coinObjectId);
|
|
1473
|
+
if (rest.length > 0) {
|
|
1474
|
+
tx.mergeCoins(primary, rest.map((c) => tx.object(c.coinObjectId)));
|
|
1475
|
+
}
|
|
1476
|
+
const [coin] = tx.splitCoins(primary, [tx.pure.u64(amount)]);
|
|
1477
|
+
tx.transferObjects([coin], tx.pure.address(recipient));
|
|
1478
|
+
}
|
|
1479
|
+
return executeTx(
|
|
1480
|
+
tx,
|
|
1481
|
+
"sui_transfer",
|
|
1482
|
+
{ recipient, amountMist, coinType: type },
|
|
1483
|
+
`transfer ${amountMist} MIST of ${type} to ${recipient}`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
const moveCall = defineTool({
|
|
1488
|
+
name: "sui_move_call",
|
|
1489
|
+
description: "Build, sign, and submit ANY Sui Move call \u2014 invoke a function on any package/module. This is the general way to run any on-chain action (swaps, mints, staking, arbitrary contracts). Provide the exact target, type arguments, and ordered arguments.",
|
|
1490
|
+
sensitive: true,
|
|
1491
|
+
parameters: z4.object({
|
|
1492
|
+
target: z4.string().regex(/^0x[0-9a-fA-F]+::[^:]+::[^:]+$/, "must be package::module::function").describe("Fully-qualified function, e.g. 0x2::coin::value."),
|
|
1493
|
+
typeArguments: z4.array(z4.string()).optional().describe('Generic type args, e.g. ["0x2::sui::SUI"].'),
|
|
1494
|
+
args: z4.array(
|
|
1495
|
+
z4.object({
|
|
1496
|
+
kind: z4.enum(["pure", "object", "gas"]).describe("pure value, object id, or the gas coin"),
|
|
1497
|
+
value: z4.string().optional().describe("object: the 0x\u2026 id; pure: the value as a string"),
|
|
1498
|
+
type: z4.string().optional().describe("pure type: u8|u16|u32|u64|u128|u256|bool|address|string (default string)")
|
|
1499
|
+
})
|
|
1500
|
+
).optional().describe("Ordered arguments to the function.")
|
|
1501
|
+
}),
|
|
1502
|
+
execute: async ({ target, typeArguments, args }) => {
|
|
1503
|
+
const tx = new Transaction2();
|
|
1504
|
+
const built = (args ?? []).map((a) => {
|
|
1505
|
+
if (a.kind === "gas") return tx.gas;
|
|
1506
|
+
if (a.kind === "object") {
|
|
1507
|
+
if (a.value === void 0) throw new Error("sui_move_call: object arg needs `value` (id).");
|
|
1508
|
+
return tx.object(a.value);
|
|
1509
|
+
}
|
|
1510
|
+
const v = a.value ?? "";
|
|
1511
|
+
switch (a.type) {
|
|
1512
|
+
case "u8":
|
|
1513
|
+
return tx.pure.u8(Number(v));
|
|
1514
|
+
case "u16":
|
|
1515
|
+
return tx.pure.u16(Number(v));
|
|
1516
|
+
case "u32":
|
|
1517
|
+
return tx.pure.u32(Number(v));
|
|
1518
|
+
case "u64":
|
|
1519
|
+
return tx.pure.u64(BigInt(v));
|
|
1520
|
+
case "u128":
|
|
1521
|
+
return tx.pure.u128(BigInt(v));
|
|
1522
|
+
case "u256":
|
|
1523
|
+
return tx.pure.u256(BigInt(v));
|
|
1524
|
+
case "bool":
|
|
1525
|
+
return tx.pure.bool(v === "true");
|
|
1526
|
+
case "address":
|
|
1527
|
+
return tx.pure.address(v);
|
|
1528
|
+
default:
|
|
1529
|
+
return tx.pure.string(v);
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
tx.moveCall({ target, typeArguments: typeArguments ?? [], arguments: built });
|
|
1533
|
+
return await executeTx(tx, "sui_move_call", { target, typeArguments, args }, `Move call ${target}`);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
return { name: "sui", tools: [balance, object, executePtb, transfer, moveCall] };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// ../../packages/adapters/mcp/src/index.ts
|
|
1540
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1541
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1542
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1543
|
+
|
|
1544
|
+
// ../../packages/adapters/mcp/src/schema.ts
|
|
1545
|
+
import { z as z5 } from "zod";
|
|
1546
|
+
function jsonSchemaToZod(schema) {
|
|
1547
|
+
if (!schema?.type) return z5.unknown();
|
|
1548
|
+
switch (schema.type) {
|
|
1549
|
+
case "string":
|
|
1550
|
+
return schema.enum ? z5.enum(schema.enum) : z5.string();
|
|
1551
|
+
case "number":
|
|
1552
|
+
case "integer":
|
|
1553
|
+
return z5.number();
|
|
1554
|
+
case "boolean":
|
|
1555
|
+
return z5.boolean();
|
|
1556
|
+
case "array":
|
|
1557
|
+
return z5.array(jsonSchemaToZod(schema.items));
|
|
1558
|
+
case "object": {
|
|
1559
|
+
const required = new Set(schema.required ?? []);
|
|
1560
|
+
const shape = {};
|
|
1561
|
+
for (const [key, prop] of Object.entries(schema.properties ?? {})) {
|
|
1562
|
+
const fieldSchema = jsonSchemaToZod(prop);
|
|
1563
|
+
shape[key] = required.has(key) ? fieldSchema : fieldSchema.optional();
|
|
1564
|
+
}
|
|
1565
|
+
return z5.object(shape).passthrough();
|
|
1566
|
+
}
|
|
1567
|
+
default:
|
|
1568
|
+
return z5.unknown();
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ../../packages/adapters/mcp/src/index.ts
|
|
1573
|
+
async function buildMcpPlugin(transport, prefix) {
|
|
1574
|
+
const client = new Client({ name: "thiny", version: "0.1.0" }, { capabilities: {} });
|
|
1575
|
+
await client.connect(transport);
|
|
1576
|
+
const listed = await client.listTools();
|
|
1577
|
+
const tools = listed.tools.map(
|
|
1578
|
+
(toolDef) => defineTool({
|
|
1579
|
+
name: `${prefix}_${toolDef.name}`,
|
|
1580
|
+
description: toolDef.description ?? toolDef.name,
|
|
1581
|
+
parameters: jsonSchemaToZod(toolDef.inputSchema),
|
|
1582
|
+
execute: async (args) => {
|
|
1583
|
+
const result = await client.callTool({
|
|
1584
|
+
name: toolDef.name,
|
|
1585
|
+
arguments: args ?? {}
|
|
1586
|
+
});
|
|
1587
|
+
return result.content;
|
|
1588
|
+
}
|
|
1589
|
+
})
|
|
1590
|
+
);
|
|
1591
|
+
return {
|
|
1592
|
+
name: `mcp:${prefix}`,
|
|
1593
|
+
tools,
|
|
1594
|
+
async close() {
|
|
1595
|
+
await client.close();
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async function mcpHttpPlugin(opts) {
|
|
1600
|
+
const transport = new StreamableHTTPClientTransport(
|
|
1601
|
+
new URL(opts.url),
|
|
1602
|
+
opts.headers ? { requestInit: { headers: opts.headers } } : void 0
|
|
1603
|
+
);
|
|
1604
|
+
return buildMcpPlugin(transport, opts.name ?? "mcp");
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1318
1607
|
// ../../packages/skills/src/catalog.ts
|
|
1319
1608
|
var BUILTIN_SKILLS = [
|
|
1320
1609
|
{
|
|
@@ -1451,6 +1740,157 @@ var SkillRegistry = class {
|
|
|
1451
1740
|
// ../../packages/skills/src/index.ts
|
|
1452
1741
|
var defaultRegistry = new SkillRegistry();
|
|
1453
1742
|
|
|
1743
|
+
// src/onboarding.ts
|
|
1744
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
1745
|
+
import { homedir } from "os";
|
|
1746
|
+
import { join, dirname as dirname2 } from "path";
|
|
1747
|
+
import { fileURLToPath } from "url";
|
|
1748
|
+
import * as p from "@clack/prompts";
|
|
1749
|
+
var THINY_DIR = join(homedir(), ".thiny");
|
|
1750
|
+
var CONFIG = join(THINY_DIR, "config.json");
|
|
1751
|
+
function version() {
|
|
1752
|
+
try {
|
|
1753
|
+
const pkg = join(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
1754
|
+
return JSON.parse(readFileSync2(pkg, "utf8")).version ?? "0.0.0";
|
|
1755
|
+
} catch {
|
|
1756
|
+
return "0.0.0";
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function loadConfig() {
|
|
1760
|
+
return existsSync2(CONFIG) ? JSON.parse(readFileSync2(CONFIG, "utf8")) : null;
|
|
1761
|
+
}
|
|
1762
|
+
function saveConfig(cfg) {
|
|
1763
|
+
mkdirSync(THINY_DIR, { recursive: true });
|
|
1764
|
+
chmodSync(THINY_DIR, 448);
|
|
1765
|
+
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
|
|
1766
|
+
chmodSync(CONFIG, 384);
|
|
1767
|
+
}
|
|
1768
|
+
function applyConfig(cfg) {
|
|
1769
|
+
if (!cfg) return;
|
|
1770
|
+
const set = (k, v) => {
|
|
1771
|
+
if (v && !process.env[k]) process.env[k] = v;
|
|
1772
|
+
};
|
|
1773
|
+
set("THINY_MODEL", cfg.model);
|
|
1774
|
+
if (cfg.apiKey) {
|
|
1775
|
+
set(cfg.model?.startsWith("anthropic") ? "THINY_ANTHROPIC_API_KEY" : "THINY_OPENAI_API_KEY", cfg.apiKey);
|
|
1776
|
+
}
|
|
1777
|
+
set("THINY_OPENAI_BASE_URL", cfg.baseUrl);
|
|
1778
|
+
set("THINY_PERSONA_NAME", cfg.agentName);
|
|
1779
|
+
set("THINY_USER_ID", cfg.userId);
|
|
1780
|
+
if (cfg.sui?.network) {
|
|
1781
|
+
set("SUI_NETWORK", cfg.sui.network);
|
|
1782
|
+
if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
|
|
1783
|
+
}
|
|
1784
|
+
const sk = cfg.sui?.wallet.secretKey;
|
|
1785
|
+
if (sk) {
|
|
1786
|
+
set("SUI_SECRET_KEY", sk);
|
|
1787
|
+
set("THINY_SUI_SECRET_KEY", sk);
|
|
1788
|
+
}
|
|
1789
|
+
set("MCP_URL", cfg.sui?.rillMcpUrl);
|
|
1790
|
+
}
|
|
1791
|
+
function bail(v) {
|
|
1792
|
+
if (p.isCancel(v)) {
|
|
1793
|
+
p.cancel("Cancelled.");
|
|
1794
|
+
process.exit(0);
|
|
1795
|
+
}
|
|
1796
|
+
return v;
|
|
1797
|
+
}
|
|
1798
|
+
var MODELS = [
|
|
1799
|
+
{ value: "oai-mini", label: "OpenAI \xB7 gpt-4o-mini", hint: "fast, cheap", model: "openai:gpt-4o-mini", needsKey: true },
|
|
1800
|
+
{ value: "oai-4o", label: "OpenAI \xB7 gpt-4o", model: "openai:gpt-4o", needsKey: true },
|
|
1801
|
+
{ value: "claude-haiku", label: "Anthropic \xB7 claude-haiku-4-5", model: "anthropic:claude-haiku-4-5-20251001", needsKey: true },
|
|
1802
|
+
{ value: "claude-sonnet", label: "Anthropic \xB7 claude-sonnet-4-6", model: "anthropic:claude-sonnet-4-6", needsKey: true },
|
|
1803
|
+
{ value: "ollama", label: "Ollama", hint: "local, no key", model: "llama3", baseUrl: "http://localhost:11434/v1", apiKey: "ollama" },
|
|
1804
|
+
{ value: "custom", label: "Custom", hint: "any OpenAI-compatible endpoint", custom: true }
|
|
1805
|
+
];
|
|
1806
|
+
async function baseSetup() {
|
|
1807
|
+
p.intro(`Thiny ${version()} \u2014 setup`);
|
|
1808
|
+
const agentName = bail(
|
|
1809
|
+
await p.text({ message: "Agent name", placeholder: "ThinyAI", defaultValue: "ThinyAI" })
|
|
1810
|
+
);
|
|
1811
|
+
const choice = bail(
|
|
1812
|
+
await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
|
|
1813
|
+
);
|
|
1814
|
+
const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
|
|
1815
|
+
const cfg = { agentName, userId: "default" };
|
|
1816
|
+
if (pick.custom) {
|
|
1817
|
+
cfg.model = bail(
|
|
1818
|
+
await p.text({ message: "Model id", placeholder: "e.g. MiniMax-M3", validate: (v) => v ? void 0 : "Required" })
|
|
1819
|
+
);
|
|
1820
|
+
cfg.baseUrl = bail(
|
|
1821
|
+
await p.text({
|
|
1822
|
+
message: "Base URL (OpenAI-compatible)",
|
|
1823
|
+
placeholder: "https://api.example.com/v1",
|
|
1824
|
+
validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
|
|
1825
|
+
})
|
|
1826
|
+
);
|
|
1827
|
+
cfg.apiKey = bail(await p.password({ message: "API key" }));
|
|
1828
|
+
} else {
|
|
1829
|
+
cfg.model = pick.model;
|
|
1830
|
+
if (pick.baseUrl) cfg.baseUrl = pick.baseUrl;
|
|
1831
|
+
cfg.apiKey = pick.apiKey ?? (pick.needsKey ? bail(await p.password({ message: "API key" })) : void 0);
|
|
1832
|
+
}
|
|
1833
|
+
saveConfig(cfg);
|
|
1834
|
+
p.outro(`Saved ${CONFIG} \u2014 run \`thiny\` to start, or \`thiny sui init\` for Sui.`);
|
|
1835
|
+
return cfg;
|
|
1836
|
+
}
|
|
1837
|
+
async function suiInit() {
|
|
1838
|
+
let cfg = loadConfig();
|
|
1839
|
+
cfg ??= await baseSetup();
|
|
1840
|
+
p.intro("Thiny \u2014 Sui setup");
|
|
1841
|
+
const network = bail(
|
|
1842
|
+
await p.select({
|
|
1843
|
+
message: "Sui network (you can change this later)",
|
|
1844
|
+
options: [
|
|
1845
|
+
{ value: "testnet", label: "Testnet", hint: "recommended for testing" },
|
|
1846
|
+
{ value: "mainnet", label: "Mainnet", hint: "real funds" }
|
|
1847
|
+
]
|
|
1848
|
+
})
|
|
1849
|
+
);
|
|
1850
|
+
const choice = bail(
|
|
1851
|
+
await p.select({
|
|
1852
|
+
message: "Wallet",
|
|
1853
|
+
options: [
|
|
1854
|
+
{ value: "paste", label: "Paste an existing private key", hint: "suiprivkey\u2026" },
|
|
1855
|
+
{ value: "generate", label: "Generate a new key pair locally" },
|
|
1856
|
+
{ value: "rill", label: "Agent wallet with on-chain capabilities", hint: "Rill" }
|
|
1857
|
+
]
|
|
1858
|
+
})
|
|
1859
|
+
);
|
|
1860
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
1861
|
+
let wallet;
|
|
1862
|
+
let address;
|
|
1863
|
+
if (choice === "generate" || choice === "rill") {
|
|
1864
|
+
const kp = Ed25519Keypair2.generate();
|
|
1865
|
+
wallet = { type: "generated", secretKey: kp.getSecretKey() };
|
|
1866
|
+
address = kp.getPublicKey().toSuiAddress();
|
|
1867
|
+
} else {
|
|
1868
|
+
const sk = bail(
|
|
1869
|
+
await p.password({
|
|
1870
|
+
message: "Private key (suiprivkey\u2026)",
|
|
1871
|
+
validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
|
|
1872
|
+
})
|
|
1873
|
+
);
|
|
1874
|
+
wallet = { type: "imported", secretKey: sk };
|
|
1875
|
+
address = Ed25519Keypair2.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
1876
|
+
}
|
|
1877
|
+
cfg.sui = { network, wallet, address };
|
|
1878
|
+
if (choice === "rill") {
|
|
1879
|
+
const url = bail(
|
|
1880
|
+
await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
|
|
1881
|
+
);
|
|
1882
|
+
if (url) cfg.sui.rillMcpUrl = url;
|
|
1883
|
+
}
|
|
1884
|
+
saveConfig(cfg);
|
|
1885
|
+
const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
|
|
1886
|
+
p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
|
|
1887
|
+
p.outro(`Sui configured (${network}).`);
|
|
1888
|
+
}
|
|
1889
|
+
async function ensureSetup() {
|
|
1890
|
+
if (loadConfig() || process.env.THINY_MODEL) return;
|
|
1891
|
+
await baseSetup();
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1454
1894
|
// src/skills.ts
|
|
1455
1895
|
async function loadSkills(ids, env = process.env) {
|
|
1456
1896
|
const { satisfied, warnings } = defaultRegistry.checkEnv(ids, env);
|
|
@@ -1784,7 +2224,7 @@ function captureStats(base, turn) {
|
|
|
1784
2224
|
var echoTool = defineTool({
|
|
1785
2225
|
name: "echo",
|
|
1786
2226
|
description: "Echo text back verbatim. Use when asked to repeat or echo something.",
|
|
1787
|
-
parameters:
|
|
2227
|
+
parameters: z6.object({ text: z6.string().describe("the text to echo") }),
|
|
1788
2228
|
execute: ({ text: text2 }) => Promise.resolve({ echoed: text2 })
|
|
1789
2229
|
});
|
|
1790
2230
|
function parseSkillArgs() {
|
|
@@ -1795,10 +2235,10 @@ function parseSkillArgs() {
|
|
|
1795
2235
|
}
|
|
1796
2236
|
var currentSessionId = `cli-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
1797
2237
|
async function runCli() {
|
|
1798
|
-
const thinyDir =
|
|
1799
|
-
|
|
2238
|
+
const thinyDir = join2(homedir2(), ".thiny");
|
|
2239
|
+
mkdirSync2(thinyDir, { recursive: true });
|
|
1800
2240
|
const envLogFile = process.env.THINY_LOG_FILE?.trim();
|
|
1801
|
-
const logFile = envLogFile && envLogFile.length > 0 ? envLogFile :
|
|
2241
|
+
const logFile = envLogFile && envLogFile.length > 0 ? envLogFile : join2(thinyDir, "cli.log");
|
|
1802
2242
|
const fileLogger = pinoLogger({ level: process.env.LOG_LEVEL ?? "info", file: logFile });
|
|
1803
2243
|
const turn = { inputTokens: 0, outputTokens: 0, toolCalls: 0, modelCalls: 0 };
|
|
1804
2244
|
const session = { inputTokens: 0, outputTokens: 0, toolCalls: 0, turns: 0 };
|
|
@@ -1827,7 +2267,9 @@ async function runCli() {
|
|
|
1827
2267
|
namespace: process.env.MEMWAL_NAMESPACE ?? userId
|
|
1828
2268
|
}) : walrusMemoryPlugin({
|
|
1829
2269
|
client: walrus,
|
|
1830
|
-
|
|
2270
|
+
// Stable per-user location (~/.thiny) so cross-session memory works no matter which
|
|
2271
|
+
// directory `thiny` is launched from — a cwd-relative file would fragment per folder.
|
|
2272
|
+
pointers: filePointerStore(process.env.WALRUS_POINTERS ?? join2(thinyDir, "thiny-pointers.json")),
|
|
1831
2273
|
userId,
|
|
1832
2274
|
onStoreStart: () => pendingWrites += 1,
|
|
1833
2275
|
onStore: (ref) => {
|
|
@@ -1843,13 +2285,68 @@ async function runCli() {
|
|
|
1843
2285
|
process.env
|
|
1844
2286
|
);
|
|
1845
2287
|
const persona = process.env.THINY_PERSONA_NAME ? { name: process.env.THINY_PERSONA_NAME, description: process.env.THINY_PERSONA_DESCRIPTION } : void 0;
|
|
2288
|
+
const allowMainnet = process.env.SUI_ALLOW_MAINNET === "1";
|
|
2289
|
+
const suiNetwork = process.env.SUI_NETWORK === "mainnet" ? "mainnet" : "testnet";
|
|
2290
|
+
const suiKey0 = process.env.SUI_SECRET_KEY ?? process.env.THINY_SUI_SECRET_KEY;
|
|
2291
|
+
let suiSignerRef = suiKey0 ? suiSigner({ network: suiNetwork, secretKey: suiKey0, allowMainnet }) : null;
|
|
2292
|
+
const suiPlugins = [];
|
|
2293
|
+
if (process.env.MCP_URL) {
|
|
2294
|
+
try {
|
|
2295
|
+
suiPlugins.push(await mcpHttpPlugin({ url: process.env.MCP_URL, name: "rill" }));
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
renderWarning(`Rill MCP unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const suiSetupTool = defineTool({
|
|
2301
|
+
name: "sui_setup",
|
|
2302
|
+
description: "Set up or change the agent's Sui wallet so it can read balances and sign transactions. Use when the user asks to enable Sui, create/import a wallet, or switch network. Modes: generate (new local key), import (a suiprivkey\u2026), rill (use a Rill MCP signer URL). Takes effect immediately; Rill's builder tools connect on the next start. Always remind the user to fund the returned address.",
|
|
2303
|
+
sensitive: true,
|
|
2304
|
+
parameters: z6.object({
|
|
2305
|
+
network: z6.enum(["testnet", "mainnet"]).default("testnet"),
|
|
2306
|
+
wallet: z6.enum(["generate", "import", "rill"]).describe("generate a new key, import a suiprivkey\u2026, or use a Rill MCP signer"),
|
|
2307
|
+
secretKey: z6.string().optional().describe("suiprivkey\u2026 \u2014 required when wallet=import"),
|
|
2308
|
+
rillMcpUrl: z6.string().optional().describe("Rill MCP URL \u2014 used when wallet=rill")
|
|
2309
|
+
}),
|
|
2310
|
+
execute: async ({ network: net, wallet, secretKey, rillMcpUrl }) => {
|
|
2311
|
+
const network2 = net === "mainnet" ? "mainnet" : "testnet";
|
|
2312
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
2313
|
+
let sk;
|
|
2314
|
+
let address;
|
|
2315
|
+
if (wallet === "import") {
|
|
2316
|
+
if (!secretKey?.startsWith("suiprivkey")) {
|
|
2317
|
+
throw new Error("sui_setup: import requires a `secretKey` starting with suiprivkey\u2026");
|
|
2318
|
+
}
|
|
2319
|
+
sk = secretKey;
|
|
2320
|
+
address = Ed25519Keypair2.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
2321
|
+
} else {
|
|
2322
|
+
const kp = Ed25519Keypair2.generate();
|
|
2323
|
+
sk = kp.getSecretKey();
|
|
2324
|
+
address = kp.getPublicKey().toSuiAddress();
|
|
2325
|
+
}
|
|
2326
|
+
const cfg = loadConfig() ?? {};
|
|
2327
|
+
cfg.sui = {
|
|
2328
|
+
network: network2,
|
|
2329
|
+
address,
|
|
2330
|
+
wallet: { type: wallet === "import" ? "imported" : "generated", secretKey: sk }
|
|
2331
|
+
};
|
|
2332
|
+
if (wallet === "rill" && rillMcpUrl) cfg.sui.rillMcpUrl = rillMcpUrl;
|
|
2333
|
+
saveConfig(cfg);
|
|
2334
|
+
suiSignerRef = suiSigner({ network: network2, secretKey: sk, allowMainnet });
|
|
2335
|
+
return {
|
|
2336
|
+
ok: true,
|
|
2337
|
+
network: network2,
|
|
2338
|
+
address,
|
|
2339
|
+
note: `Sui wallet ready on ${network2} at ${address}. The user MUST fund this address before sending transactions` + (network2 === "testnet" ? " (faucet: https://faucet.sui.io)." : ".") + (wallet === "rill" ? " Rill MCP URL saved \u2014 restart thiny to connect its builder tools." : "")
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
1846
2343
|
const budget = budgetMiddleware({ maxCalls: 50, logger });
|
|
1847
2344
|
const agent = await createAgent({
|
|
1848
2345
|
model,
|
|
1849
2346
|
logger: agentLogger,
|
|
1850
2347
|
persona,
|
|
1851
|
-
systemPrompt: "You are a helpful AI assistant. Use tools when they help you answer better. Be concise.\n\nMEMORY: You have persistent long-term memory across sessions, stored on Walrus. What you already know about the user is injected automatically at the start of each conversation under \u201C[User Memory \u2026]\u201D. When the user shares anything durable about themselves \u2014 their name, role, preferences, projects, or goals, even casually \u2014 immediately call remember_fact to save it. If asked what you remember, answer from the injected user memory (or call recall_memory). You DO remember across sessions \u2014 never say you lack memory or that each session starts fresh.\n\nFor multi-step work, call update_plan to track steps and delegate_task to hand focused sub-problems to a sub-agent.",
|
|
1852
|
-
tools: [echoTool],
|
|
2348
|
+
systemPrompt: "You are a helpful AI assistant. Use tools when they help you answer better. Be concise.\n\nMEMORY: You have persistent long-term memory across sessions, stored on Walrus. What you already know about the user is injected automatically at the start of each conversation under \u201C[User Memory \u2026]\u201D. When the user shares anything durable about themselves \u2014 their name, role, preferences, projects, or goals, even casually \u2014 immediately call remember_fact to save it. If asked what you remember, answer from the injected user memory (or call recall_memory). You DO remember across sessions \u2014 never say you lack memory or that each session starts fresh.\n\nFor multi-step work, call update_plan to track steps and delegate_task to hand focused sub-problems to a sub-agent.\n\nSUI: You can operate on the Sui blockchain. " + (suiSignerRef ? `A wallet is configured on ${suiNetwork} at ${suiSignerRef.address ?? "(unknown)"}. ` : "No wallet is set up yet \u2014 when the user wants Sui, call sui_setup (generate / import / rill) to create or connect one, then tell them to fund the returned address. ") + "Use sui_balance to read balances and sui_object to inspect objects. To SEND a transaction you need an UNSIGNED PTB built by a builder (e.g. a connected Rill MCP tool); pass it to sui_execute_ptb, which simulates, applies caps, and signs. Never claim you cannot use Sui \u2014 you can.",
|
|
2349
|
+
tools: [echoTool, suiSetupTool],
|
|
1853
2350
|
plugins: [
|
|
1854
2351
|
{
|
|
1855
2352
|
name: "observability",
|
|
@@ -1858,6 +2355,10 @@ async function runCli() {
|
|
|
1858
2355
|
},
|
|
1859
2356
|
agentsPlugin(),
|
|
1860
2357
|
memoryPlugin,
|
|
2358
|
+
suiPlugin({ signer: () => suiSignerRef }),
|
|
2359
|
+
// always present; tools guide setup if no wallet
|
|
2360
|
+
...suiPlugins,
|
|
2361
|
+
// Rill MCP builder tools (if connected)
|
|
1861
2362
|
...skillPlugins
|
|
1862
2363
|
]
|
|
1863
2364
|
});
|
|
@@ -1898,7 +2399,13 @@ async function runCli() {
|
|
|
1898
2399
|
);
|
|
1899
2400
|
if (walrusAudit)
|
|
1900
2401
|
renderInfo(`Walrus audit: ON (${network}) \u2014 each turn's action log is stored + verifiable`);
|
|
2402
|
+
if (suiSignerRef)
|
|
2403
|
+
renderInfo(
|
|
2404
|
+
`Sui: ${suiNetwork} \xB7 ${suiSignerRef.address ?? "?"}${process.env.MCP_URL ? " \xB7 Rill MCP connected" : ""}`
|
|
2405
|
+
);
|
|
2406
|
+
else renderInfo("Sui: no wallet \u2014 ask the agent to set one up, or run `thiny sui init`");
|
|
1901
2407
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
2408
|
+
emitKeypressEvents(stdin);
|
|
1902
2409
|
const spinner = new Spinner();
|
|
1903
2410
|
const flushMemory = memoryPlugin.flush;
|
|
1904
2411
|
const PROMPT = "\x1B[36mYou\x1B[0m \x1B[2m\u203A\x1B[0m ";
|
|
@@ -2011,10 +2518,15 @@ Audit trail ${blobId}
|
|
|
2011
2518
|
continue;
|
|
2012
2519
|
}
|
|
2013
2520
|
renderAgentLabel(personaName);
|
|
2014
|
-
spinner.start("thinking\u2026");
|
|
2521
|
+
spinner.start("thinking\u2026 (esc to cancel)");
|
|
2015
2522
|
budget.reset();
|
|
2016
2523
|
resetTurn(turn);
|
|
2017
2524
|
const startedAt = Date.now();
|
|
2525
|
+
const ac = new AbortController();
|
|
2526
|
+
const onKey = (_s, key) => {
|
|
2527
|
+
if (key?.name === "escape") ac.abort();
|
|
2528
|
+
};
|
|
2529
|
+
stdin.on("keypress", onKey);
|
|
2018
2530
|
try {
|
|
2019
2531
|
let firstToken = true;
|
|
2020
2532
|
const stream = createMarkdownWriter((s) => stdout.write(s));
|
|
@@ -2026,15 +2538,28 @@ Audit trail ${blobId}
|
|
|
2026
2538
|
spinner.start("running\u2026");
|
|
2027
2539
|
};
|
|
2028
2540
|
agent.events.on("beforeToolCall", toolHandler);
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2541
|
+
let reply;
|
|
2542
|
+
try {
|
|
2543
|
+
reply = await agent.run(trimmed, {
|
|
2544
|
+
sessionId: currentSessionId,
|
|
2545
|
+
signal: ac.signal,
|
|
2546
|
+
onToken: (delta) => {
|
|
2547
|
+
spinner.stop();
|
|
2548
|
+
firstToken = false;
|
|
2549
|
+
stream.push(delta);
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
if (ac.signal.aborted) {
|
|
2032
2554
|
spinner.stop();
|
|
2033
|
-
|
|
2034
|
-
|
|
2555
|
+
stream.end();
|
|
2556
|
+
stdout.write("\n \x1B[2m\u2298 cancelled (Esc)\x1B[0m\n");
|
|
2557
|
+
continue;
|
|
2035
2558
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2559
|
+
throw err;
|
|
2560
|
+
} finally {
|
|
2561
|
+
agent.events.off("beforeToolCall", toolHandler);
|
|
2562
|
+
}
|
|
2038
2563
|
spinner.stop();
|
|
2039
2564
|
if (firstToken) {
|
|
2040
2565
|
stream.push(reply.length > 0 ? reply : "\x1B[2m(model returned empty response)\x1B[0m");
|
|
@@ -2079,6 +2604,8 @@ Audit trail ${blobId}
|
|
|
2079
2604
|
looksLikeModelError ? `${msg}
|
|
2080
2605
|
\u21B3 Check your model, base URL, and API key (run \`thiny init\`, or edit ~/.thiny/config.json / .env).` : msg
|
|
2081
2606
|
);
|
|
2607
|
+
} finally {
|
|
2608
|
+
stdin.off("keypress", onKey);
|
|
2082
2609
|
}
|
|
2083
2610
|
}
|
|
2084
2611
|
} finally {
|
|
@@ -2086,157 +2613,6 @@ Audit trail ${blobId}
|
|
|
2086
2613
|
}
|
|
2087
2614
|
}
|
|
2088
2615
|
|
|
2089
|
-
// src/onboarding.ts
|
|
2090
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
2091
|
-
import { homedir as homedir2 } from "os";
|
|
2092
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
2093
|
-
import { fileURLToPath } from "url";
|
|
2094
|
-
import * as p from "@clack/prompts";
|
|
2095
|
-
var THINY_DIR = join2(homedir2(), ".thiny");
|
|
2096
|
-
var CONFIG = join2(THINY_DIR, "config.json");
|
|
2097
|
-
function version() {
|
|
2098
|
-
try {
|
|
2099
|
-
const pkg = join2(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
2100
|
-
return JSON.parse(readFileSync2(pkg, "utf8")).version ?? "0.0.0";
|
|
2101
|
-
} catch {
|
|
2102
|
-
return "0.0.0";
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
function loadConfig() {
|
|
2106
|
-
return existsSync2(CONFIG) ? JSON.parse(readFileSync2(CONFIG, "utf8")) : null;
|
|
2107
|
-
}
|
|
2108
|
-
function saveConfig(cfg) {
|
|
2109
|
-
mkdirSync2(THINY_DIR, { recursive: true });
|
|
2110
|
-
chmodSync(THINY_DIR, 448);
|
|
2111
|
-
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
|
|
2112
|
-
chmodSync(CONFIG, 384);
|
|
2113
|
-
}
|
|
2114
|
-
function applyConfig(cfg) {
|
|
2115
|
-
if (!cfg) return;
|
|
2116
|
-
const set = (k, v) => {
|
|
2117
|
-
if (v && !process.env[k]) process.env[k] = v;
|
|
2118
|
-
};
|
|
2119
|
-
set("THINY_MODEL", cfg.model);
|
|
2120
|
-
if (cfg.apiKey) {
|
|
2121
|
-
set(cfg.model?.startsWith("anthropic") ? "THINY_ANTHROPIC_API_KEY" : "THINY_OPENAI_API_KEY", cfg.apiKey);
|
|
2122
|
-
}
|
|
2123
|
-
set("THINY_OPENAI_BASE_URL", cfg.baseUrl);
|
|
2124
|
-
set("THINY_PERSONA_NAME", cfg.agentName);
|
|
2125
|
-
set("THINY_USER_ID", cfg.userId);
|
|
2126
|
-
if (cfg.sui?.network) {
|
|
2127
|
-
set("SUI_NETWORK", cfg.sui.network);
|
|
2128
|
-
if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
|
|
2129
|
-
}
|
|
2130
|
-
const sk = cfg.sui?.wallet.secretKey;
|
|
2131
|
-
if (sk) {
|
|
2132
|
-
set("SUI_SECRET_KEY", sk);
|
|
2133
|
-
set("THINY_SUI_SECRET_KEY", sk);
|
|
2134
|
-
}
|
|
2135
|
-
set("MCP_URL", cfg.sui?.rillMcpUrl);
|
|
2136
|
-
}
|
|
2137
|
-
function bail(v) {
|
|
2138
|
-
if (p.isCancel(v)) {
|
|
2139
|
-
p.cancel("Cancelled.");
|
|
2140
|
-
process.exit(0);
|
|
2141
|
-
}
|
|
2142
|
-
return v;
|
|
2143
|
-
}
|
|
2144
|
-
var MODELS = [
|
|
2145
|
-
{ value: "oai-mini", label: "OpenAI \xB7 gpt-4o-mini", hint: "fast, cheap", model: "openai:gpt-4o-mini", needsKey: true },
|
|
2146
|
-
{ value: "oai-4o", label: "OpenAI \xB7 gpt-4o", model: "openai:gpt-4o", needsKey: true },
|
|
2147
|
-
{ value: "claude-haiku", label: "Anthropic \xB7 claude-haiku-4-5", model: "anthropic:claude-haiku-4-5-20251001", needsKey: true },
|
|
2148
|
-
{ value: "claude-sonnet", label: "Anthropic \xB7 claude-sonnet-4-6", model: "anthropic:claude-sonnet-4-6", needsKey: true },
|
|
2149
|
-
{ value: "ollama", label: "Ollama", hint: "local, no key", model: "llama3", baseUrl: "http://localhost:11434/v1", apiKey: "ollama" },
|
|
2150
|
-
{ value: "custom", label: "Custom", hint: "any OpenAI-compatible endpoint", custom: true }
|
|
2151
|
-
];
|
|
2152
|
-
async function baseSetup() {
|
|
2153
|
-
p.intro(`Thiny ${version()} \u2014 setup`);
|
|
2154
|
-
const agentName = bail(
|
|
2155
|
-
await p.text({ message: "Agent name", placeholder: "ThinyAI", defaultValue: "ThinyAI" })
|
|
2156
|
-
);
|
|
2157
|
-
const choice = bail(
|
|
2158
|
-
await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
|
|
2159
|
-
);
|
|
2160
|
-
const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
|
|
2161
|
-
const cfg = { agentName, userId: "default" };
|
|
2162
|
-
if (pick.custom) {
|
|
2163
|
-
cfg.model = bail(
|
|
2164
|
-
await p.text({ message: "Model id", placeholder: "e.g. MiniMax-M3", validate: (v) => v ? void 0 : "Required" })
|
|
2165
|
-
);
|
|
2166
|
-
cfg.baseUrl = bail(
|
|
2167
|
-
await p.text({
|
|
2168
|
-
message: "Base URL (OpenAI-compatible)",
|
|
2169
|
-
placeholder: "https://api.example.com/v1",
|
|
2170
|
-
validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
|
|
2171
|
-
})
|
|
2172
|
-
);
|
|
2173
|
-
cfg.apiKey = bail(await p.password({ message: "API key" }));
|
|
2174
|
-
} else {
|
|
2175
|
-
cfg.model = pick.model;
|
|
2176
|
-
if (pick.baseUrl) cfg.baseUrl = pick.baseUrl;
|
|
2177
|
-
cfg.apiKey = pick.apiKey ?? (pick.needsKey ? bail(await p.password({ message: "API key" })) : void 0);
|
|
2178
|
-
}
|
|
2179
|
-
saveConfig(cfg);
|
|
2180
|
-
p.outro(`Saved ${CONFIG} \u2014 run \`thiny\` to start, or \`thiny sui init\` for Sui.`);
|
|
2181
|
-
return cfg;
|
|
2182
|
-
}
|
|
2183
|
-
async function suiInit() {
|
|
2184
|
-
let cfg = loadConfig();
|
|
2185
|
-
cfg ??= await baseSetup();
|
|
2186
|
-
p.intro("Thiny \u2014 Sui setup");
|
|
2187
|
-
const network = bail(
|
|
2188
|
-
await p.select({
|
|
2189
|
-
message: "Sui network (you can change this later)",
|
|
2190
|
-
options: [
|
|
2191
|
-
{ value: "testnet", label: "Testnet", hint: "recommended for testing" },
|
|
2192
|
-
{ value: "mainnet", label: "Mainnet", hint: "real funds" }
|
|
2193
|
-
]
|
|
2194
|
-
})
|
|
2195
|
-
);
|
|
2196
|
-
const choice = bail(
|
|
2197
|
-
await p.select({
|
|
2198
|
-
message: "Wallet",
|
|
2199
|
-
options: [
|
|
2200
|
-
{ value: "paste", label: "Paste an existing private key", hint: "suiprivkey\u2026" },
|
|
2201
|
-
{ value: "generate", label: "Generate a new key pair locally" },
|
|
2202
|
-
{ value: "rill", label: "Agent wallet with on-chain capabilities", hint: "Rill" }
|
|
2203
|
-
]
|
|
2204
|
-
})
|
|
2205
|
-
);
|
|
2206
|
-
const { Ed25519Keypair } = await import("@mysten/sui/keypairs/ed25519");
|
|
2207
|
-
let wallet;
|
|
2208
|
-
let address;
|
|
2209
|
-
if (choice === "generate" || choice === "rill") {
|
|
2210
|
-
const kp = Ed25519Keypair.generate();
|
|
2211
|
-
wallet = { type: "generated", secretKey: kp.getSecretKey() };
|
|
2212
|
-
address = kp.getPublicKey().toSuiAddress();
|
|
2213
|
-
} else {
|
|
2214
|
-
const sk = bail(
|
|
2215
|
-
await p.password({
|
|
2216
|
-
message: "Private key (suiprivkey\u2026)",
|
|
2217
|
-
validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
|
|
2218
|
-
})
|
|
2219
|
-
);
|
|
2220
|
-
wallet = { type: "imported", secretKey: sk };
|
|
2221
|
-
address = Ed25519Keypair.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
2222
|
-
}
|
|
2223
|
-
cfg.sui = { network, wallet, address };
|
|
2224
|
-
if (choice === "rill") {
|
|
2225
|
-
const url = bail(
|
|
2226
|
-
await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
|
|
2227
|
-
);
|
|
2228
|
-
if (url) cfg.sui.rillMcpUrl = url;
|
|
2229
|
-
}
|
|
2230
|
-
saveConfig(cfg);
|
|
2231
|
-
const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
|
|
2232
|
-
p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
|
|
2233
|
-
p.outro(`Sui configured (${network}).`);
|
|
2234
|
-
}
|
|
2235
|
-
async function ensureSetup() {
|
|
2236
|
-
if (loadConfig() || process.env.THINY_MODEL) return;
|
|
2237
|
-
await baseSetup();
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
2616
|
// src/bin.ts
|
|
2241
2617
|
function help() {
|
|
2242
2618
|
console.log(`thiny ${version()}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinyai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Thiny AI — a beautiful terminal agent: interactive chat, tools, Walrus memory, and Sui execution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@ai-sdk/anthropic": "^1.0.0",
|
|
23
23
|
"@ai-sdk/openai": "^1.3.0",
|
|
24
24
|
"@clack/prompts": "^1.6.0",
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
25
26
|
"@mysten/sui": "^2.19.0",
|
|
26
27
|
"ai": "^4.0.0",
|
|
27
28
|
"chalk": "^5.3.0",
|
|
@@ -36,13 +37,15 @@
|
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"tsup": "^8.5.1",
|
|
38
39
|
"typescript": "^5.5.0",
|
|
40
|
+
"@thiny/memory-memwal": "0.1.0",
|
|
39
41
|
"@thiny/mcp": "0.1.0",
|
|
40
42
|
"@thiny/core": "0.1.0",
|
|
41
43
|
"@thiny/model-aisdk": "0.1.0",
|
|
42
|
-
"@thiny/memory-memwal": "0.1.0",
|
|
43
|
-
"@thiny/logger-pino": "0.1.0",
|
|
44
44
|
"@thiny/walrus": "0.1.0",
|
|
45
|
+
"@thiny/logger-pino": "0.1.0",
|
|
45
46
|
"@thiny/plugin-agents": "0.1.0",
|
|
47
|
+
"@thiny/plugin-sui": "0.1.0",
|
|
48
|
+
"@thiny/signer-sui": "0.1.0",
|
|
46
49
|
"@thiny/skills": "0.1.0"
|
|
47
50
|
},
|
|
48
51
|
"author": "Thiny AI",
|