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.
Files changed (2) hide show
  1. package/dist/bin.js +572 -196
  2. 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 z4 } from "zod";
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
- if (colonIdx === -1) {
836
- if (opts.anthropic?.baseURL) {
837
- return createAnthropic({ baseURL: opts.anthropic.baseURL, apiKey: opts.anthropic.apiKey })(
838
- model
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 (provider === "anthropic") {
849
- return createAnthropic({ baseURL: opts.anthropic?.baseURL, apiKey: opts.anthropic?.apiKey })(
850
- modelId
846
+ if (opts.anthropic?.baseURL) {
847
+ return createAnthropic({ baseURL: opts.anthropic.baseURL, apiKey: opts.anthropic.apiKey })(
848
+ model
851
849
  );
852
850
  }
853
- throw new Error(
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: false });
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: z4.object({ text: z4.string().describe("the text to echo") }),
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 = join(homedir(), ".thiny");
1799
- mkdirSync(thinyDir, { recursive: true });
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 : join(thinyDir, "cli.log");
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
- pointers: filePointerStore(process.env.WALRUS_POINTERS ?? "thiny-pointers.json"),
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
- const reply = await agent.run(trimmed, {
2030
- sessionId: currentSessionId,
2031
- onToken: (delta) => {
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
- firstToken = false;
2034
- stream.push(delta);
2555
+ stream.end();
2556
+ stdout.write("\n \x1B[2m\u2298 cancelled (Esc)\x1B[0m\n");
2557
+ continue;
2035
2558
  }
2036
- });
2037
- agent.events.off("beforeToolCall", toolHandler);
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.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",