thinyai 0.1.2 → 0.1.4
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/README.md +1 -0
- package/dist/bin.js +550 -162
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ thiny # first launch runs setup, then starts chatting
|
|
|
36
36
|
| `thiny` | Start the interactive agent. Runs first-time setup if needed. |
|
|
37
37
|
| `thiny init` | (Re)run base setup — pick a model + agent name + API key. |
|
|
38
38
|
| `thiny sui init` | Add Sui on-chain capabilities — pick a network + wallet. |
|
|
39
|
+
| `thiny update` | Update to the latest published version (auto-detects bun/npm/pnpm). |
|
|
39
40
|
| `thiny help` | Show all commands. |
|
|
40
41
|
| `thiny --version` | Print the version. |
|
|
41
42
|
|
package/dist/bin.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
+
|
|
3
7
|
// src/main.ts
|
|
4
8
|
import { createInterface } from "readline/promises";
|
|
5
9
|
import { clearLine, cursorTo, emitKeypressEvents } from "readline";
|
|
6
10
|
import { stdin, stdout } from "process";
|
|
7
|
-
import { mkdirSync } from "fs";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { z as
|
|
11
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
12
|
+
import { homedir as homedir2 } from "os";
|
|
13
|
+
import { join as join2 } from "path";
|
|
14
|
+
import { z as z6 } from "zod";
|
|
11
15
|
|
|
12
16
|
// ../../packages/core/src/domain/messages.ts
|
|
13
17
|
var systemMessage = (content) => ({ role: "system", content });
|
|
@@ -1310,6 +1314,300 @@ ${context}` : task;
|
|
|
1310
1314
|
return { name: "agents", tools: [delegateTask, updatePlan] };
|
|
1311
1315
|
}
|
|
1312
1316
|
|
|
1317
|
+
// ../../packages/adapters/signer-sui/src/index.ts
|
|
1318
|
+
import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc";
|
|
1319
|
+
import { Transaction } from "@mysten/sui/transactions";
|
|
1320
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
1321
|
+
var ZERO_ADDRESS = `0x${"0".repeat(64)}`;
|
|
1322
|
+
function suiSigner(opts = {}) {
|
|
1323
|
+
const network = opts.network ?? "testnet";
|
|
1324
|
+
const client = opts.client ?? new SuiJsonRpcClient({ url: opts.rpcUrl ?? getJsonRpcFullnodeUrl(network), network });
|
|
1325
|
+
const keypair = opts.signer ?? (opts.secretKey ? Ed25519Keypair.fromSecretKey(opts.secretKey) : void 0);
|
|
1326
|
+
const address = keypair?.getPublicKey().toSuiAddress();
|
|
1327
|
+
return {
|
|
1328
|
+
address,
|
|
1329
|
+
network,
|
|
1330
|
+
client,
|
|
1331
|
+
hasKey: () => keypair !== void 0,
|
|
1332
|
+
devInspect(tx, devOpts) {
|
|
1333
|
+
return client.devInspectTransactionBlock({
|
|
1334
|
+
sender: devOpts?.sender ?? address ?? ZERO_ADDRESS,
|
|
1335
|
+
transactionBlock: tx
|
|
1336
|
+
});
|
|
1337
|
+
},
|
|
1338
|
+
async signAndExecute(tx) {
|
|
1339
|
+
if (!keypair) {
|
|
1340
|
+
throw new Error("suiSigner.signAndExecute: no key configured (pass `secretKey` or `signer`).");
|
|
1341
|
+
}
|
|
1342
|
+
if (network === "mainnet" && opts.allowMainnet !== true) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
"suiSigner: refusing to sign on mainnet. Pass `allowMainnet: true` to opt in."
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const result = await client.signAndExecuteTransaction({
|
|
1348
|
+
signer: keypair,
|
|
1349
|
+
transaction: tx,
|
|
1350
|
+
options: { showEffects: true }
|
|
1351
|
+
});
|
|
1352
|
+
await client.waitForTransaction({ digest: result.digest });
|
|
1353
|
+
return { digest: result.digest, effects: result.effects ?? null };
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// ../../packages/plugins/sui/src/index.ts
|
|
1359
|
+
import { z as z4 } from "zod";
|
|
1360
|
+
import { Transaction as Transaction2 } from "@mysten/sui/transactions";
|
|
1361
|
+
function explorerTxUrl(network, digest) {
|
|
1362
|
+
return `https://suiscan.xyz/${network}/tx/${digest}`;
|
|
1363
|
+
}
|
|
1364
|
+
function suiPlugin(opts) {
|
|
1365
|
+
const sig = opts.signer;
|
|
1366
|
+
const resolve2 = typeof sig === "function" ? sig : () => sig;
|
|
1367
|
+
const signerOrThrow = () => {
|
|
1368
|
+
const s = resolve2();
|
|
1369
|
+
if (!s) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
"No Sui wallet configured yet. Call sui_setup to create or import one (or tell the user to run `thiny sui init`)."
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
return s;
|
|
1375
|
+
};
|
|
1376
|
+
const requireSimSuccess = opts.policy?.requireSimSuccess ?? true;
|
|
1377
|
+
const executeTx = async (tx, toolName, approvalArgs, reason) => {
|
|
1378
|
+
const signer = signerOrThrow();
|
|
1379
|
+
const sim = await signer.devInspect(tx);
|
|
1380
|
+
const status = sim.effects.status.status;
|
|
1381
|
+
if (requireSimSuccess && status !== "success") {
|
|
1382
|
+
throw new Error(`${toolName}: simulation failed (${sim.effects.status.error ?? status})`);
|
|
1383
|
+
}
|
|
1384
|
+
if (opts.policy?.maxGasBudgetMist !== void 0) {
|
|
1385
|
+
const gas = sim.effects.gasUsed;
|
|
1386
|
+
const estGas = BigInt(gas.computationCost) + BigInt(gas.storageCost);
|
|
1387
|
+
if (estGas > opts.policy.maxGasBudgetMist) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`${toolName}: estimated gas ${estGas.toString()} MIST exceeds policy cap ${opts.policy.maxGasBudgetMist.toString()}.`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (opts.approver) {
|
|
1394
|
+
const ok = await opts.approver({ tool: toolName, args: approvalArgs, reason });
|
|
1395
|
+
if (!ok) throw new Error(`${toolName}: rejected by approver.`);
|
|
1396
|
+
}
|
|
1397
|
+
const { digest, effects } = await signer.signAndExecute(tx);
|
|
1398
|
+
return { digest, effects, explorerUrl: explorerTxUrl(signer.network, digest) };
|
|
1399
|
+
};
|
|
1400
|
+
const balance = defineTool({
|
|
1401
|
+
name: "sui_balance",
|
|
1402
|
+
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.",
|
|
1403
|
+
parameters: z4.object({
|
|
1404
|
+
address: z4.string().optional().describe("Owner address (default: the agent's address)."),
|
|
1405
|
+
coinType: z4.string().optional().describe("Coin type, e.g. 0x2::sui::SUI (default: SUI).")
|
|
1406
|
+
}),
|
|
1407
|
+
execute: async ({ address, coinType }) => {
|
|
1408
|
+
const signer = signerOrThrow();
|
|
1409
|
+
const owner = address ?? signer.address;
|
|
1410
|
+
if (owner === void 0) {
|
|
1411
|
+
throw new Error("sui_balance: no address given and the signer has no key/address.");
|
|
1412
|
+
}
|
|
1413
|
+
const bal = await signer.client.getBalance({ owner, ...coinType ? { coinType } : {} });
|
|
1414
|
+
return {
|
|
1415
|
+
owner,
|
|
1416
|
+
coinType: bal.coinType,
|
|
1417
|
+
totalBalanceMist: bal.totalBalance,
|
|
1418
|
+
coins: bal.coinObjectCount
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
const object = defineTool({
|
|
1423
|
+
name: "sui_object",
|
|
1424
|
+
description: "Read a Sui object's type and fields by id.",
|
|
1425
|
+
parameters: z4.object({ objectId: z4.string().min(1).describe("The object id (0x\u2026).") }),
|
|
1426
|
+
execute: async ({ objectId }) => {
|
|
1427
|
+
const signer = signerOrThrow();
|
|
1428
|
+
const res = await signer.client.getObject({
|
|
1429
|
+
id: objectId,
|
|
1430
|
+
options: { showContent: true, showType: true, showOwner: true }
|
|
1431
|
+
});
|
|
1432
|
+
if (!res.data) throw new Error(`sui_object: ${objectId} not found`);
|
|
1433
|
+
const content = res.data.content;
|
|
1434
|
+
return {
|
|
1435
|
+
objectId,
|
|
1436
|
+
type: res.data.type,
|
|
1437
|
+
fields: content?.dataType === "moveObject" ? content.fields : void 0
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
const executePtb = defineTool({
|
|
1442
|
+
name: "sui_execute_ptb",
|
|
1443
|
+
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.",
|
|
1444
|
+
sensitive: true,
|
|
1445
|
+
parameters: z4.object({
|
|
1446
|
+
unsignedPtb: z4.string().min(1).describe("The builder's unsigned PTB \u2014 base64 of a serialized Sui transaction.")
|
|
1447
|
+
}),
|
|
1448
|
+
execute: async ({ unsignedPtb }) => {
|
|
1449
|
+
const tx = Transaction2.from(Buffer.from(unsignedPtb, "base64").toString("utf8"));
|
|
1450
|
+
return await executeTx(tx, "sui_execute_ptb", { unsignedPtb }, "sign and submit a Sui PTB");
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
const transfer = defineTool({
|
|
1454
|
+
name: "sui_transfer",
|
|
1455
|
+
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.",
|
|
1456
|
+
sensitive: true,
|
|
1457
|
+
parameters: z4.object({
|
|
1458
|
+
recipient: z4.string().min(1).describe("Destination address (0x\u2026)."),
|
|
1459
|
+
amountMist: z4.string().min(1).describe('Amount in MIST. e.g. "1000000000" = 1 SUI.'),
|
|
1460
|
+
coinType: z4.string().optional().describe("Coin type (default 0x2::sui::SUI).")
|
|
1461
|
+
}),
|
|
1462
|
+
execute: async ({ recipient, amountMist, coinType }) => {
|
|
1463
|
+
const signer = signerOrThrow();
|
|
1464
|
+
const amount = BigInt(amountMist);
|
|
1465
|
+
const type = coinType ?? "0x2::sui::SUI";
|
|
1466
|
+
const tx = new Transaction2();
|
|
1467
|
+
if (type.endsWith("::sui::SUI")) {
|
|
1468
|
+
const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(amount)]);
|
|
1469
|
+
tx.transferObjects([coin], tx.pure.address(recipient));
|
|
1470
|
+
} else {
|
|
1471
|
+
const owner = signer.address;
|
|
1472
|
+
if (owner === void 0) throw new Error("sui_transfer: signer has no address.");
|
|
1473
|
+
const { data } = await signer.client.getCoins({ owner, coinType: type });
|
|
1474
|
+
const [first, ...rest] = data;
|
|
1475
|
+
if (!first) throw new Error(`sui_transfer: no ${type} coins owned by ${owner}.`);
|
|
1476
|
+
const primary = tx.object(first.coinObjectId);
|
|
1477
|
+
if (rest.length > 0) {
|
|
1478
|
+
tx.mergeCoins(primary, rest.map((c) => tx.object(c.coinObjectId)));
|
|
1479
|
+
}
|
|
1480
|
+
const [coin] = tx.splitCoins(primary, [tx.pure.u64(amount)]);
|
|
1481
|
+
tx.transferObjects([coin], tx.pure.address(recipient));
|
|
1482
|
+
}
|
|
1483
|
+
return executeTx(
|
|
1484
|
+
tx,
|
|
1485
|
+
"sui_transfer",
|
|
1486
|
+
{ recipient, amountMist, coinType: type },
|
|
1487
|
+
`transfer ${amountMist} MIST of ${type} to ${recipient}`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
const moveCall = defineTool({
|
|
1492
|
+
name: "sui_move_call",
|
|
1493
|
+
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.",
|
|
1494
|
+
sensitive: true,
|
|
1495
|
+
parameters: z4.object({
|
|
1496
|
+
target: z4.string().regex(/^0x[0-9a-fA-F]+::[^:]+::[^:]+$/, "must be package::module::function").describe("Fully-qualified function, e.g. 0x2::coin::value."),
|
|
1497
|
+
typeArguments: z4.array(z4.string()).optional().describe('Generic type args, e.g. ["0x2::sui::SUI"].'),
|
|
1498
|
+
args: z4.array(
|
|
1499
|
+
z4.object({
|
|
1500
|
+
kind: z4.enum(["pure", "object", "gas"]).describe("pure value, object id, or the gas coin"),
|
|
1501
|
+
value: z4.string().optional().describe("object: the 0x\u2026 id; pure: the value as a string"),
|
|
1502
|
+
type: z4.string().optional().describe("pure type: u8|u16|u32|u64|u128|u256|bool|address|string (default string)")
|
|
1503
|
+
})
|
|
1504
|
+
).optional().describe("Ordered arguments to the function.")
|
|
1505
|
+
}),
|
|
1506
|
+
execute: async ({ target, typeArguments, args }) => {
|
|
1507
|
+
const tx = new Transaction2();
|
|
1508
|
+
const built = (args ?? []).map((a) => {
|
|
1509
|
+
if (a.kind === "gas") return tx.gas;
|
|
1510
|
+
if (a.kind === "object") {
|
|
1511
|
+
if (a.value === void 0) throw new Error("sui_move_call: object arg needs `value` (id).");
|
|
1512
|
+
return tx.object(a.value);
|
|
1513
|
+
}
|
|
1514
|
+
const v = a.value ?? "";
|
|
1515
|
+
switch (a.type) {
|
|
1516
|
+
case "u8":
|
|
1517
|
+
return tx.pure.u8(Number(v));
|
|
1518
|
+
case "u16":
|
|
1519
|
+
return tx.pure.u16(Number(v));
|
|
1520
|
+
case "u32":
|
|
1521
|
+
return tx.pure.u32(Number(v));
|
|
1522
|
+
case "u64":
|
|
1523
|
+
return tx.pure.u64(BigInt(v));
|
|
1524
|
+
case "u128":
|
|
1525
|
+
return tx.pure.u128(BigInt(v));
|
|
1526
|
+
case "u256":
|
|
1527
|
+
return tx.pure.u256(BigInt(v));
|
|
1528
|
+
case "bool":
|
|
1529
|
+
return tx.pure.bool(v === "true");
|
|
1530
|
+
case "address":
|
|
1531
|
+
return tx.pure.address(v);
|
|
1532
|
+
default:
|
|
1533
|
+
return tx.pure.string(v);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
tx.moveCall({ target, typeArguments: typeArguments ?? [], arguments: built });
|
|
1537
|
+
return await executeTx(tx, "sui_move_call", { target, typeArguments, args }, `Move call ${target}`);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
return { name: "sui", tools: [balance, object, executePtb, transfer, moveCall] };
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// ../../packages/adapters/mcp/src/index.ts
|
|
1544
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1545
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1546
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1547
|
+
|
|
1548
|
+
// ../../packages/adapters/mcp/src/schema.ts
|
|
1549
|
+
import { z as z5 } from "zod";
|
|
1550
|
+
function jsonSchemaToZod(schema) {
|
|
1551
|
+
if (!schema?.type) return z5.unknown();
|
|
1552
|
+
switch (schema.type) {
|
|
1553
|
+
case "string":
|
|
1554
|
+
return schema.enum ? z5.enum(schema.enum) : z5.string();
|
|
1555
|
+
case "number":
|
|
1556
|
+
case "integer":
|
|
1557
|
+
return z5.number();
|
|
1558
|
+
case "boolean":
|
|
1559
|
+
return z5.boolean();
|
|
1560
|
+
case "array":
|
|
1561
|
+
return z5.array(jsonSchemaToZod(schema.items));
|
|
1562
|
+
case "object": {
|
|
1563
|
+
const required = new Set(schema.required ?? []);
|
|
1564
|
+
const shape = {};
|
|
1565
|
+
for (const [key, prop] of Object.entries(schema.properties ?? {})) {
|
|
1566
|
+
const fieldSchema = jsonSchemaToZod(prop);
|
|
1567
|
+
shape[key] = required.has(key) ? fieldSchema : fieldSchema.optional();
|
|
1568
|
+
}
|
|
1569
|
+
return z5.object(shape).passthrough();
|
|
1570
|
+
}
|
|
1571
|
+
default:
|
|
1572
|
+
return z5.unknown();
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// ../../packages/adapters/mcp/src/index.ts
|
|
1577
|
+
async function buildMcpPlugin(transport, prefix) {
|
|
1578
|
+
const client = new Client({ name: "thiny", version: "0.1.0" }, { capabilities: {} });
|
|
1579
|
+
await client.connect(transport);
|
|
1580
|
+
const listed = await client.listTools();
|
|
1581
|
+
const tools = listed.tools.map(
|
|
1582
|
+
(toolDef) => defineTool({
|
|
1583
|
+
name: `${prefix}_${toolDef.name}`,
|
|
1584
|
+
description: toolDef.description ?? toolDef.name,
|
|
1585
|
+
parameters: jsonSchemaToZod(toolDef.inputSchema),
|
|
1586
|
+
execute: async (args) => {
|
|
1587
|
+
const result = await client.callTool({
|
|
1588
|
+
name: toolDef.name,
|
|
1589
|
+
arguments: args ?? {}
|
|
1590
|
+
});
|
|
1591
|
+
return result.content;
|
|
1592
|
+
}
|
|
1593
|
+
})
|
|
1594
|
+
);
|
|
1595
|
+
return {
|
|
1596
|
+
name: `mcp:${prefix}`,
|
|
1597
|
+
tools,
|
|
1598
|
+
async close() {
|
|
1599
|
+
await client.close();
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
async function mcpHttpPlugin(opts) {
|
|
1604
|
+
const transport = new StreamableHTTPClientTransport(
|
|
1605
|
+
new URL(opts.url),
|
|
1606
|
+
opts.headers ? { requestInit: { headers: opts.headers } } : void 0
|
|
1607
|
+
);
|
|
1608
|
+
return buildMcpPlugin(transport, opts.name ?? "mcp");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1313
1611
|
// ../../packages/skills/src/catalog.ts
|
|
1314
1612
|
var BUILTIN_SKILLS = [
|
|
1315
1613
|
{
|
|
@@ -1446,6 +1744,157 @@ var SkillRegistry = class {
|
|
|
1446
1744
|
// ../../packages/skills/src/index.ts
|
|
1447
1745
|
var defaultRegistry = new SkillRegistry();
|
|
1448
1746
|
|
|
1747
|
+
// src/onboarding.ts
|
|
1748
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
1749
|
+
import { homedir } from "os";
|
|
1750
|
+
import { join, dirname as dirname2 } from "path";
|
|
1751
|
+
import { fileURLToPath } from "url";
|
|
1752
|
+
import * as p from "@clack/prompts";
|
|
1753
|
+
var THINY_DIR = join(homedir(), ".thiny");
|
|
1754
|
+
var CONFIG = join(THINY_DIR, "config.json");
|
|
1755
|
+
function version() {
|
|
1756
|
+
try {
|
|
1757
|
+
const pkg = join(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
1758
|
+
return JSON.parse(readFileSync2(pkg, "utf8")).version ?? "0.0.0";
|
|
1759
|
+
} catch {
|
|
1760
|
+
return "0.0.0";
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function loadConfig() {
|
|
1764
|
+
return existsSync2(CONFIG) ? JSON.parse(readFileSync2(CONFIG, "utf8")) : null;
|
|
1765
|
+
}
|
|
1766
|
+
function saveConfig(cfg) {
|
|
1767
|
+
mkdirSync(THINY_DIR, { recursive: true });
|
|
1768
|
+
chmodSync(THINY_DIR, 448);
|
|
1769
|
+
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
|
|
1770
|
+
chmodSync(CONFIG, 384);
|
|
1771
|
+
}
|
|
1772
|
+
function applyConfig(cfg) {
|
|
1773
|
+
if (!cfg) return;
|
|
1774
|
+
const set = (k, v) => {
|
|
1775
|
+
if (v && !process.env[k]) process.env[k] = v;
|
|
1776
|
+
};
|
|
1777
|
+
set("THINY_MODEL", cfg.model);
|
|
1778
|
+
if (cfg.apiKey) {
|
|
1779
|
+
set(cfg.model?.startsWith("anthropic") ? "THINY_ANTHROPIC_API_KEY" : "THINY_OPENAI_API_KEY", cfg.apiKey);
|
|
1780
|
+
}
|
|
1781
|
+
set("THINY_OPENAI_BASE_URL", cfg.baseUrl);
|
|
1782
|
+
set("THINY_PERSONA_NAME", cfg.agentName);
|
|
1783
|
+
set("THINY_USER_ID", cfg.userId);
|
|
1784
|
+
if (cfg.sui?.network) {
|
|
1785
|
+
set("SUI_NETWORK", cfg.sui.network);
|
|
1786
|
+
if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
|
|
1787
|
+
}
|
|
1788
|
+
const sk = cfg.sui?.wallet.secretKey;
|
|
1789
|
+
if (sk) {
|
|
1790
|
+
set("SUI_SECRET_KEY", sk);
|
|
1791
|
+
set("THINY_SUI_SECRET_KEY", sk);
|
|
1792
|
+
}
|
|
1793
|
+
set("MCP_URL", cfg.sui?.rillMcpUrl);
|
|
1794
|
+
}
|
|
1795
|
+
function bail(v) {
|
|
1796
|
+
if (p.isCancel(v)) {
|
|
1797
|
+
p.cancel("Cancelled.");
|
|
1798
|
+
process.exit(0);
|
|
1799
|
+
}
|
|
1800
|
+
return v;
|
|
1801
|
+
}
|
|
1802
|
+
var MODELS = [
|
|
1803
|
+
{ value: "oai-mini", label: "OpenAI \xB7 gpt-4o-mini", hint: "fast, cheap", model: "openai:gpt-4o-mini", needsKey: true },
|
|
1804
|
+
{ value: "oai-4o", label: "OpenAI \xB7 gpt-4o", model: "openai:gpt-4o", needsKey: true },
|
|
1805
|
+
{ value: "claude-haiku", label: "Anthropic \xB7 claude-haiku-4-5", model: "anthropic:claude-haiku-4-5-20251001", needsKey: true },
|
|
1806
|
+
{ value: "claude-sonnet", label: "Anthropic \xB7 claude-sonnet-4-6", model: "anthropic:claude-sonnet-4-6", needsKey: true },
|
|
1807
|
+
{ value: "ollama", label: "Ollama", hint: "local, no key", model: "llama3", baseUrl: "http://localhost:11434/v1", apiKey: "ollama" },
|
|
1808
|
+
{ value: "custom", label: "Custom", hint: "any OpenAI-compatible endpoint", custom: true }
|
|
1809
|
+
];
|
|
1810
|
+
async function baseSetup() {
|
|
1811
|
+
p.intro(`Thiny ${version()} \u2014 setup`);
|
|
1812
|
+
const agentName = bail(
|
|
1813
|
+
await p.text({ message: "Agent name", placeholder: "ThinyAI", defaultValue: "ThinyAI" })
|
|
1814
|
+
);
|
|
1815
|
+
const choice = bail(
|
|
1816
|
+
await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
|
|
1817
|
+
);
|
|
1818
|
+
const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
|
|
1819
|
+
const cfg = { agentName, userId: "default" };
|
|
1820
|
+
if (pick.custom) {
|
|
1821
|
+
cfg.model = bail(
|
|
1822
|
+
await p.text({ message: "Model id", placeholder: "e.g. MiniMax-M3", validate: (v) => v ? void 0 : "Required" })
|
|
1823
|
+
);
|
|
1824
|
+
cfg.baseUrl = bail(
|
|
1825
|
+
await p.text({
|
|
1826
|
+
message: "Base URL (OpenAI-compatible)",
|
|
1827
|
+
placeholder: "https://api.example.com/v1",
|
|
1828
|
+
validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
|
|
1829
|
+
})
|
|
1830
|
+
);
|
|
1831
|
+
cfg.apiKey = bail(await p.password({ message: "API key" }));
|
|
1832
|
+
} else {
|
|
1833
|
+
cfg.model = pick.model;
|
|
1834
|
+
if (pick.baseUrl) cfg.baseUrl = pick.baseUrl;
|
|
1835
|
+
cfg.apiKey = pick.apiKey ?? (pick.needsKey ? bail(await p.password({ message: "API key" })) : void 0);
|
|
1836
|
+
}
|
|
1837
|
+
saveConfig(cfg);
|
|
1838
|
+
p.outro(`Saved ${CONFIG} \u2014 run \`thiny\` to start, or \`thiny sui init\` for Sui.`);
|
|
1839
|
+
return cfg;
|
|
1840
|
+
}
|
|
1841
|
+
async function suiInit() {
|
|
1842
|
+
let cfg = loadConfig();
|
|
1843
|
+
cfg ??= await baseSetup();
|
|
1844
|
+
p.intro("Thiny \u2014 Sui setup");
|
|
1845
|
+
const network = bail(
|
|
1846
|
+
await p.select({
|
|
1847
|
+
message: "Sui network (you can change this later)",
|
|
1848
|
+
options: [
|
|
1849
|
+
{ value: "testnet", label: "Testnet", hint: "recommended for testing" },
|
|
1850
|
+
{ value: "mainnet", label: "Mainnet", hint: "real funds" }
|
|
1851
|
+
]
|
|
1852
|
+
})
|
|
1853
|
+
);
|
|
1854
|
+
const choice = bail(
|
|
1855
|
+
await p.select({
|
|
1856
|
+
message: "Wallet",
|
|
1857
|
+
options: [
|
|
1858
|
+
{ value: "paste", label: "Paste an existing private key", hint: "suiprivkey\u2026" },
|
|
1859
|
+
{ value: "generate", label: "Generate a new key pair locally" },
|
|
1860
|
+
{ value: "rill", label: "Agent wallet with on-chain capabilities", hint: "Rill" }
|
|
1861
|
+
]
|
|
1862
|
+
})
|
|
1863
|
+
);
|
|
1864
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
1865
|
+
let wallet;
|
|
1866
|
+
let address;
|
|
1867
|
+
if (choice === "generate" || choice === "rill") {
|
|
1868
|
+
const kp = Ed25519Keypair2.generate();
|
|
1869
|
+
wallet = { type: "generated", secretKey: kp.getSecretKey() };
|
|
1870
|
+
address = kp.getPublicKey().toSuiAddress();
|
|
1871
|
+
} else {
|
|
1872
|
+
const sk = bail(
|
|
1873
|
+
await p.password({
|
|
1874
|
+
message: "Private key (suiprivkey\u2026)",
|
|
1875
|
+
validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
|
|
1876
|
+
})
|
|
1877
|
+
);
|
|
1878
|
+
wallet = { type: "imported", secretKey: sk };
|
|
1879
|
+
address = Ed25519Keypair2.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
1880
|
+
}
|
|
1881
|
+
cfg.sui = { network, wallet, address };
|
|
1882
|
+
if (choice === "rill") {
|
|
1883
|
+
const url = bail(
|
|
1884
|
+
await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
|
|
1885
|
+
);
|
|
1886
|
+
if (url) cfg.sui.rillMcpUrl = url;
|
|
1887
|
+
}
|
|
1888
|
+
saveConfig(cfg);
|
|
1889
|
+
const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
|
|
1890
|
+
p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
|
|
1891
|
+
p.outro(`Sui configured (${network}).`);
|
|
1892
|
+
}
|
|
1893
|
+
async function ensureSetup() {
|
|
1894
|
+
if (loadConfig() || process.env.THINY_MODEL) return;
|
|
1895
|
+
await baseSetup();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1449
1898
|
// src/skills.ts
|
|
1450
1899
|
async function loadSkills(ids, env = process.env) {
|
|
1451
1900
|
const { satisfied, warnings } = defaultRegistry.checkEnv(ids, env);
|
|
@@ -1779,7 +2228,7 @@ function captureStats(base, turn) {
|
|
|
1779
2228
|
var echoTool = defineTool({
|
|
1780
2229
|
name: "echo",
|
|
1781
2230
|
description: "Echo text back verbatim. Use when asked to repeat or echo something.",
|
|
1782
|
-
parameters:
|
|
2231
|
+
parameters: z6.object({ text: z6.string().describe("the text to echo") }),
|
|
1783
2232
|
execute: ({ text: text2 }) => Promise.resolve({ echoed: text2 })
|
|
1784
2233
|
});
|
|
1785
2234
|
function parseSkillArgs() {
|
|
@@ -1790,10 +2239,10 @@ function parseSkillArgs() {
|
|
|
1790
2239
|
}
|
|
1791
2240
|
var currentSessionId = `cli-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
1792
2241
|
async function runCli() {
|
|
1793
|
-
const thinyDir =
|
|
1794
|
-
|
|
2242
|
+
const thinyDir = join2(homedir2(), ".thiny");
|
|
2243
|
+
mkdirSync2(thinyDir, { recursive: true });
|
|
1795
2244
|
const envLogFile = process.env.THINY_LOG_FILE?.trim();
|
|
1796
|
-
const logFile = envLogFile && envLogFile.length > 0 ? envLogFile :
|
|
2245
|
+
const logFile = envLogFile && envLogFile.length > 0 ? envLogFile : join2(thinyDir, "cli.log");
|
|
1797
2246
|
const fileLogger = pinoLogger({ level: process.env.LOG_LEVEL ?? "info", file: logFile });
|
|
1798
2247
|
const turn = { inputTokens: 0, outputTokens: 0, toolCalls: 0, modelCalls: 0 };
|
|
1799
2248
|
const session = { inputTokens: 0, outputTokens: 0, toolCalls: 0, turns: 0 };
|
|
@@ -1824,7 +2273,7 @@ async function runCli() {
|
|
|
1824
2273
|
client: walrus,
|
|
1825
2274
|
// Stable per-user location (~/.thiny) so cross-session memory works no matter which
|
|
1826
2275
|
// directory `thiny` is launched from — a cwd-relative file would fragment per folder.
|
|
1827
|
-
pointers: filePointerStore(process.env.WALRUS_POINTERS ??
|
|
2276
|
+
pointers: filePointerStore(process.env.WALRUS_POINTERS ?? join2(thinyDir, "thiny-pointers.json")),
|
|
1828
2277
|
userId,
|
|
1829
2278
|
onStoreStart: () => pendingWrites += 1,
|
|
1830
2279
|
onStore: (ref) => {
|
|
@@ -1840,13 +2289,68 @@ async function runCli() {
|
|
|
1840
2289
|
process.env
|
|
1841
2290
|
);
|
|
1842
2291
|
const persona = process.env.THINY_PERSONA_NAME ? { name: process.env.THINY_PERSONA_NAME, description: process.env.THINY_PERSONA_DESCRIPTION } : void 0;
|
|
2292
|
+
const allowMainnet = process.env.SUI_ALLOW_MAINNET === "1";
|
|
2293
|
+
const suiNetwork = process.env.SUI_NETWORK === "mainnet" ? "mainnet" : "testnet";
|
|
2294
|
+
const suiKey0 = process.env.SUI_SECRET_KEY ?? process.env.THINY_SUI_SECRET_KEY;
|
|
2295
|
+
let suiSignerRef = suiKey0 ? suiSigner({ network: suiNetwork, secretKey: suiKey0, allowMainnet }) : null;
|
|
2296
|
+
const suiPlugins = [];
|
|
2297
|
+
if (process.env.MCP_URL) {
|
|
2298
|
+
try {
|
|
2299
|
+
suiPlugins.push(await mcpHttpPlugin({ url: process.env.MCP_URL, name: "rill" }));
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
renderWarning(`Rill MCP unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
const suiSetupTool = defineTool({
|
|
2305
|
+
name: "sui_setup",
|
|
2306
|
+
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.",
|
|
2307
|
+
sensitive: true,
|
|
2308
|
+
parameters: z6.object({
|
|
2309
|
+
network: z6.enum(["testnet", "mainnet"]).default("testnet"),
|
|
2310
|
+
wallet: z6.enum(["generate", "import", "rill"]).describe("generate a new key, import a suiprivkey\u2026, or use a Rill MCP signer"),
|
|
2311
|
+
secretKey: z6.string().optional().describe("suiprivkey\u2026 \u2014 required when wallet=import"),
|
|
2312
|
+
rillMcpUrl: z6.string().optional().describe("Rill MCP URL \u2014 used when wallet=rill")
|
|
2313
|
+
}),
|
|
2314
|
+
execute: async ({ network: net, wallet, secretKey, rillMcpUrl }) => {
|
|
2315
|
+
const network2 = net === "mainnet" ? "mainnet" : "testnet";
|
|
2316
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
2317
|
+
let sk;
|
|
2318
|
+
let address;
|
|
2319
|
+
if (wallet === "import") {
|
|
2320
|
+
if (!secretKey?.startsWith("suiprivkey")) {
|
|
2321
|
+
throw new Error("sui_setup: import requires a `secretKey` starting with suiprivkey\u2026");
|
|
2322
|
+
}
|
|
2323
|
+
sk = secretKey;
|
|
2324
|
+
address = Ed25519Keypair2.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
2325
|
+
} else {
|
|
2326
|
+
const kp = Ed25519Keypair2.generate();
|
|
2327
|
+
sk = kp.getSecretKey();
|
|
2328
|
+
address = kp.getPublicKey().toSuiAddress();
|
|
2329
|
+
}
|
|
2330
|
+
const cfg = loadConfig() ?? {};
|
|
2331
|
+
cfg.sui = {
|
|
2332
|
+
network: network2,
|
|
2333
|
+
address,
|
|
2334
|
+
wallet: { type: wallet === "import" ? "imported" : "generated", secretKey: sk }
|
|
2335
|
+
};
|
|
2336
|
+
if (wallet === "rill" && rillMcpUrl) cfg.sui.rillMcpUrl = rillMcpUrl;
|
|
2337
|
+
saveConfig(cfg);
|
|
2338
|
+
suiSignerRef = suiSigner({ network: network2, secretKey: sk, allowMainnet });
|
|
2339
|
+
return {
|
|
2340
|
+
ok: true,
|
|
2341
|
+
network: network2,
|
|
2342
|
+
address,
|
|
2343
|
+
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." : "")
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
1843
2347
|
const budget = budgetMiddleware({ maxCalls: 50, logger });
|
|
1844
2348
|
const agent = await createAgent({
|
|
1845
2349
|
model,
|
|
1846
2350
|
logger: agentLogger,
|
|
1847
2351
|
persona,
|
|
1848
|
-
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.",
|
|
1849
|
-
tools: [echoTool],
|
|
2352
|
+
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.",
|
|
2353
|
+
tools: [echoTool, suiSetupTool],
|
|
1850
2354
|
plugins: [
|
|
1851
2355
|
{
|
|
1852
2356
|
name: "observability",
|
|
@@ -1855,6 +2359,10 @@ async function runCli() {
|
|
|
1855
2359
|
},
|
|
1856
2360
|
agentsPlugin(),
|
|
1857
2361
|
memoryPlugin,
|
|
2362
|
+
suiPlugin({ signer: () => suiSignerRef }),
|
|
2363
|
+
// always present; tools guide setup if no wallet
|
|
2364
|
+
...suiPlugins,
|
|
2365
|
+
// Rill MCP builder tools (if connected)
|
|
1858
2366
|
...skillPlugins
|
|
1859
2367
|
]
|
|
1860
2368
|
});
|
|
@@ -1895,6 +2403,11 @@ async function runCli() {
|
|
|
1895
2403
|
);
|
|
1896
2404
|
if (walrusAudit)
|
|
1897
2405
|
renderInfo(`Walrus audit: ON (${network}) \u2014 each turn's action log is stored + verifiable`);
|
|
2406
|
+
if (suiSignerRef)
|
|
2407
|
+
renderInfo(
|
|
2408
|
+
`Sui: ${suiNetwork} \xB7 ${suiSignerRef.address ?? "?"}${process.env.MCP_URL ? " \xB7 Rill MCP connected" : ""}`
|
|
2409
|
+
);
|
|
2410
|
+
else renderInfo("Sui: no wallet \u2014 ask the agent to set one up, or run `thiny sui init`");
|
|
1898
2411
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
1899
2412
|
emitKeypressEvents(stdin);
|
|
1900
2413
|
const spinner = new Spinner();
|
|
@@ -2104,157 +2617,6 @@ Audit trail ${blobId}
|
|
|
2104
2617
|
}
|
|
2105
2618
|
}
|
|
2106
2619
|
|
|
2107
|
-
// src/onboarding.ts
|
|
2108
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
2109
|
-
import { homedir as homedir2 } from "os";
|
|
2110
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
2111
|
-
import { fileURLToPath } from "url";
|
|
2112
|
-
import * as p from "@clack/prompts";
|
|
2113
|
-
var THINY_DIR = join2(homedir2(), ".thiny");
|
|
2114
|
-
var CONFIG = join2(THINY_DIR, "config.json");
|
|
2115
|
-
function version() {
|
|
2116
|
-
try {
|
|
2117
|
-
const pkg = join2(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
2118
|
-
return JSON.parse(readFileSync2(pkg, "utf8")).version ?? "0.0.0";
|
|
2119
|
-
} catch {
|
|
2120
|
-
return "0.0.0";
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
function loadConfig() {
|
|
2124
|
-
return existsSync2(CONFIG) ? JSON.parse(readFileSync2(CONFIG, "utf8")) : null;
|
|
2125
|
-
}
|
|
2126
|
-
function saveConfig(cfg) {
|
|
2127
|
-
mkdirSync2(THINY_DIR, { recursive: true });
|
|
2128
|
-
chmodSync(THINY_DIR, 448);
|
|
2129
|
-
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
|
|
2130
|
-
chmodSync(CONFIG, 384);
|
|
2131
|
-
}
|
|
2132
|
-
function applyConfig(cfg) {
|
|
2133
|
-
if (!cfg) return;
|
|
2134
|
-
const set = (k, v) => {
|
|
2135
|
-
if (v && !process.env[k]) process.env[k] = v;
|
|
2136
|
-
};
|
|
2137
|
-
set("THINY_MODEL", cfg.model);
|
|
2138
|
-
if (cfg.apiKey) {
|
|
2139
|
-
set(cfg.model?.startsWith("anthropic") ? "THINY_ANTHROPIC_API_KEY" : "THINY_OPENAI_API_KEY", cfg.apiKey);
|
|
2140
|
-
}
|
|
2141
|
-
set("THINY_OPENAI_BASE_URL", cfg.baseUrl);
|
|
2142
|
-
set("THINY_PERSONA_NAME", cfg.agentName);
|
|
2143
|
-
set("THINY_USER_ID", cfg.userId);
|
|
2144
|
-
if (cfg.sui?.network) {
|
|
2145
|
-
set("SUI_NETWORK", cfg.sui.network);
|
|
2146
|
-
if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
|
|
2147
|
-
}
|
|
2148
|
-
const sk = cfg.sui?.wallet.secretKey;
|
|
2149
|
-
if (sk) {
|
|
2150
|
-
set("SUI_SECRET_KEY", sk);
|
|
2151
|
-
set("THINY_SUI_SECRET_KEY", sk);
|
|
2152
|
-
}
|
|
2153
|
-
set("MCP_URL", cfg.sui?.rillMcpUrl);
|
|
2154
|
-
}
|
|
2155
|
-
function bail(v) {
|
|
2156
|
-
if (p.isCancel(v)) {
|
|
2157
|
-
p.cancel("Cancelled.");
|
|
2158
|
-
process.exit(0);
|
|
2159
|
-
}
|
|
2160
|
-
return v;
|
|
2161
|
-
}
|
|
2162
|
-
var MODELS = [
|
|
2163
|
-
{ value: "oai-mini", label: "OpenAI \xB7 gpt-4o-mini", hint: "fast, cheap", model: "openai:gpt-4o-mini", needsKey: true },
|
|
2164
|
-
{ value: "oai-4o", label: "OpenAI \xB7 gpt-4o", model: "openai:gpt-4o", needsKey: true },
|
|
2165
|
-
{ value: "claude-haiku", label: "Anthropic \xB7 claude-haiku-4-5", model: "anthropic:claude-haiku-4-5-20251001", needsKey: true },
|
|
2166
|
-
{ value: "claude-sonnet", label: "Anthropic \xB7 claude-sonnet-4-6", model: "anthropic:claude-sonnet-4-6", needsKey: true },
|
|
2167
|
-
{ value: "ollama", label: "Ollama", hint: "local, no key", model: "llama3", baseUrl: "http://localhost:11434/v1", apiKey: "ollama" },
|
|
2168
|
-
{ value: "custom", label: "Custom", hint: "any OpenAI-compatible endpoint", custom: true }
|
|
2169
|
-
];
|
|
2170
|
-
async function baseSetup() {
|
|
2171
|
-
p.intro(`Thiny ${version()} \u2014 setup`);
|
|
2172
|
-
const agentName = bail(
|
|
2173
|
-
await p.text({ message: "Agent name", placeholder: "ThinyAI", defaultValue: "ThinyAI" })
|
|
2174
|
-
);
|
|
2175
|
-
const choice = bail(
|
|
2176
|
-
await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
|
|
2177
|
-
);
|
|
2178
|
-
const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
|
|
2179
|
-
const cfg = { agentName, userId: "default" };
|
|
2180
|
-
if (pick.custom) {
|
|
2181
|
-
cfg.model = bail(
|
|
2182
|
-
await p.text({ message: "Model id", placeholder: "e.g. MiniMax-M3", validate: (v) => v ? void 0 : "Required" })
|
|
2183
|
-
);
|
|
2184
|
-
cfg.baseUrl = bail(
|
|
2185
|
-
await p.text({
|
|
2186
|
-
message: "Base URL (OpenAI-compatible)",
|
|
2187
|
-
placeholder: "https://api.example.com/v1",
|
|
2188
|
-
validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
|
|
2189
|
-
})
|
|
2190
|
-
);
|
|
2191
|
-
cfg.apiKey = bail(await p.password({ message: "API key" }));
|
|
2192
|
-
} else {
|
|
2193
|
-
cfg.model = pick.model;
|
|
2194
|
-
if (pick.baseUrl) cfg.baseUrl = pick.baseUrl;
|
|
2195
|
-
cfg.apiKey = pick.apiKey ?? (pick.needsKey ? bail(await p.password({ message: "API key" })) : void 0);
|
|
2196
|
-
}
|
|
2197
|
-
saveConfig(cfg);
|
|
2198
|
-
p.outro(`Saved ${CONFIG} \u2014 run \`thiny\` to start, or \`thiny sui init\` for Sui.`);
|
|
2199
|
-
return cfg;
|
|
2200
|
-
}
|
|
2201
|
-
async function suiInit() {
|
|
2202
|
-
let cfg = loadConfig();
|
|
2203
|
-
cfg ??= await baseSetup();
|
|
2204
|
-
p.intro("Thiny \u2014 Sui setup");
|
|
2205
|
-
const network = bail(
|
|
2206
|
-
await p.select({
|
|
2207
|
-
message: "Sui network (you can change this later)",
|
|
2208
|
-
options: [
|
|
2209
|
-
{ value: "testnet", label: "Testnet", hint: "recommended for testing" },
|
|
2210
|
-
{ value: "mainnet", label: "Mainnet", hint: "real funds" }
|
|
2211
|
-
]
|
|
2212
|
-
})
|
|
2213
|
-
);
|
|
2214
|
-
const choice = bail(
|
|
2215
|
-
await p.select({
|
|
2216
|
-
message: "Wallet",
|
|
2217
|
-
options: [
|
|
2218
|
-
{ value: "paste", label: "Paste an existing private key", hint: "suiprivkey\u2026" },
|
|
2219
|
-
{ value: "generate", label: "Generate a new key pair locally" },
|
|
2220
|
-
{ value: "rill", label: "Agent wallet with on-chain capabilities", hint: "Rill" }
|
|
2221
|
-
]
|
|
2222
|
-
})
|
|
2223
|
-
);
|
|
2224
|
-
const { Ed25519Keypair } = await import("@mysten/sui/keypairs/ed25519");
|
|
2225
|
-
let wallet;
|
|
2226
|
-
let address;
|
|
2227
|
-
if (choice === "generate" || choice === "rill") {
|
|
2228
|
-
const kp = Ed25519Keypair.generate();
|
|
2229
|
-
wallet = { type: "generated", secretKey: kp.getSecretKey() };
|
|
2230
|
-
address = kp.getPublicKey().toSuiAddress();
|
|
2231
|
-
} else {
|
|
2232
|
-
const sk = bail(
|
|
2233
|
-
await p.password({
|
|
2234
|
-
message: "Private key (suiprivkey\u2026)",
|
|
2235
|
-
validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
|
|
2236
|
-
})
|
|
2237
|
-
);
|
|
2238
|
-
wallet = { type: "imported", secretKey: sk };
|
|
2239
|
-
address = Ed25519Keypair.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
2240
|
-
}
|
|
2241
|
-
cfg.sui = { network, wallet, address };
|
|
2242
|
-
if (choice === "rill") {
|
|
2243
|
-
const url = bail(
|
|
2244
|
-
await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
|
|
2245
|
-
);
|
|
2246
|
-
if (url) cfg.sui.rillMcpUrl = url;
|
|
2247
|
-
}
|
|
2248
|
-
saveConfig(cfg);
|
|
2249
|
-
const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
|
|
2250
|
-
p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
|
|
2251
|
-
p.outro(`Sui configured (${network}).`);
|
|
2252
|
-
}
|
|
2253
|
-
async function ensureSetup() {
|
|
2254
|
-
if (loadConfig() || process.env.THINY_MODEL) return;
|
|
2255
|
-
await baseSetup();
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
2620
|
// src/bin.ts
|
|
2259
2621
|
function help() {
|
|
2260
2622
|
console.log(`thiny ${version()}
|
|
@@ -2263,11 +2625,33 @@ Usage:
|
|
|
2263
2625
|
thiny Start the interactive CLI agent (runs setup on first use)
|
|
2264
2626
|
thiny init Re-run setup (model, agent name, key)
|
|
2265
2627
|
thiny sui init Add Sui capabilities (network + wallet)
|
|
2628
|
+
thiny update Update thinyai to the latest version
|
|
2266
2629
|
thiny --version Print version
|
|
2267
2630
|
thiny help Show this help
|
|
2268
2631
|
|
|
2269
2632
|
Config: ~/.thiny/config.json (no .env needed)`);
|
|
2270
2633
|
}
|
|
2634
|
+
function detectPackageManager() {
|
|
2635
|
+
const here = fileURLToPath2(import.meta.url);
|
|
2636
|
+
if (here.includes("/.bun/") || here.includes("\\.bun\\")) return "bun";
|
|
2637
|
+
if (here.includes("pnpm")) return "pnpm";
|
|
2638
|
+
return "npm";
|
|
2639
|
+
}
|
|
2640
|
+
function update() {
|
|
2641
|
+
const pm = detectPackageManager();
|
|
2642
|
+
const args = pm === "npm" ? ["install", "-g", "thinyai@latest"] : ["add", "-g", "thinyai@latest"];
|
|
2643
|
+
console.log(`Updating thinyai via ${pm}\u2026 (${pm} ${args.join(" ")})`);
|
|
2644
|
+
const res = spawnSync(pm, args, { stdio: "inherit" });
|
|
2645
|
+
if (res.status !== 0) {
|
|
2646
|
+
console.error(
|
|
2647
|
+
`
|
|
2648
|
+
Update failed. Run it manually: ${pm} ${args.join(" ")}
|
|
2649
|
+
(or, if you used a different installer: npm i -g thinyai@latest)`
|
|
2650
|
+
);
|
|
2651
|
+
process.exit(res.status ?? 1);
|
|
2652
|
+
}
|
|
2653
|
+
console.log("\n\u2713 Updated. Run `thiny --version` to confirm.");
|
|
2654
|
+
}
|
|
2271
2655
|
async function run() {
|
|
2272
2656
|
const [sub, sub2] = process.argv.slice(2);
|
|
2273
2657
|
switch (sub) {
|
|
@@ -2278,6 +2662,10 @@ async function run() {
|
|
|
2278
2662
|
if (sub2 === "init") await suiInit();
|
|
2279
2663
|
else console.log("Usage: thiny sui init");
|
|
2280
2664
|
return;
|
|
2665
|
+
case "update":
|
|
2666
|
+
case "upgrade":
|
|
2667
|
+
update();
|
|
2668
|
+
return;
|
|
2281
2669
|
case "--version":
|
|
2282
2670
|
case "-v":
|
|
2283
2671
|
console.log(version());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinyai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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,14 +37,16 @@
|
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"tsup": "^8.5.1",
|
|
38
39
|
"typescript": "^5.5.0",
|
|
39
|
-
"@thiny/logger-pino": "0.1.0",
|
|
40
|
-
"@thiny/model-aisdk": "0.1.0",
|
|
41
40
|
"@thiny/core": "0.1.0",
|
|
41
|
+
"@thiny/model-aisdk": "0.1.0",
|
|
42
|
+
"@thiny/walrus": "0.1.0",
|
|
43
|
+
"@thiny/logger-pino": "0.1.0",
|
|
42
44
|
"@thiny/memory-memwal": "0.1.0",
|
|
43
|
-
"@thiny/mcp": "0.1.0",
|
|
44
|
-
"@thiny/plugin-agents": "0.1.0",
|
|
45
45
|
"@thiny/skills": "0.1.0",
|
|
46
|
-
"@thiny/
|
|
46
|
+
"@thiny/plugin-agents": "0.1.0",
|
|
47
|
+
"@thiny/signer-sui": "0.1.0",
|
|
48
|
+
"@thiny/plugin-sui": "0.1.0",
|
|
49
|
+
"@thiny/mcp": "0.1.0"
|
|
47
50
|
},
|
|
48
51
|
"author": "Thiny AI",
|
|
49
52
|
"engines": {
|