thinyai 0.1.6 → 0.1.8

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 +205 -13
  2. package/package.json +8 -8
package/dist/bin.js CHANGED
@@ -1840,13 +1840,38 @@ function applyConfig(cfg) {
1840
1840
  set("SUI_NETWORK", cfg.sui.network);
1841
1841
  if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
1842
1842
  }
1843
- const sk = cfg.sui?.wallet.secretKey;
1844
- if (sk) {
1845
- set("SUI_SECRET_KEY", sk);
1846
- set("THINY_SUI_SECRET_KEY", sk);
1843
+ const active = activeSuiWallet(cfg);
1844
+ if (active) {
1845
+ set("SUI_SECRET_KEY", active.secretKey);
1846
+ set("THINY_SUI_SECRET_KEY", active.secretKey);
1847
1847
  }
1848
1848
  set("MCP_URL", cfg.sui?.rillMcpUrl);
1849
1849
  }
1850
+ function suiWalletsOf(cfg) {
1851
+ const s = cfg?.sui;
1852
+ if (!s) return [];
1853
+ if (s.wallets && s.wallets.length > 0) return s.wallets;
1854
+ if (s.wallet && s.address) {
1855
+ return [{ label: "default", address: s.address, secretKey: s.wallet.secretKey, source: s.wallet.type }];
1856
+ }
1857
+ return [];
1858
+ }
1859
+ function activeSuiWallet(cfg) {
1860
+ const all = suiWalletsOf(cfg);
1861
+ const active = cfg?.sui?.activeAddress ?? cfg?.sui?.address;
1862
+ return all.find((w) => w.address === active) ?? all[0];
1863
+ }
1864
+ function saveSuiWallet(cfg, network, wallet, makeActive) {
1865
+ cfg.sui ??= { network };
1866
+ cfg.sui.network = network;
1867
+ const all = suiWalletsOf(cfg).filter((w) => w.address !== wallet.address);
1868
+ all.push(wallet);
1869
+ cfg.sui.wallets = all;
1870
+ delete cfg.sui.wallet;
1871
+ delete cfg.sui.address;
1872
+ if (makeActive || !cfg.sui.activeAddress) cfg.sui.activeAddress = wallet.address;
1873
+ saveConfig(cfg);
1874
+ }
1850
1875
  function bail(v) {
1851
1876
  if (p.isCancel(v)) {
1852
1877
  p.cancel("Cancelled.");
@@ -1870,7 +1895,8 @@ async function baseSetup() {
1870
1895
  const choice = bail(
1871
1896
  await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
1872
1897
  );
1873
- const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
1898
+ const pick = MODELS.find((m) => m.value === choice);
1899
+ if (!pick) throw new Error(`unknown model choice: ${choice}`);
1874
1900
  const cfg = { agentName, userId: "default" };
1875
1901
  if (pick.custom) {
1876
1902
  cfg.model = bail(
@@ -1880,7 +1906,7 @@ async function baseSetup() {
1880
1906
  await p.text({
1881
1907
  message: "Base URL (OpenAI-compatible)",
1882
1908
  placeholder: "https://api.example.com/v1",
1883
- validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
1909
+ validate: (v) => v && /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
1884
1910
  })
1885
1911
  );
1886
1912
  cfg.apiKey = bail(await p.password({ message: "API key" }));
@@ -1927,20 +1953,22 @@ async function suiInit() {
1927
1953
  const sk = bail(
1928
1954
  await p.password({
1929
1955
  message: "Private key (suiprivkey\u2026)",
1930
- validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
1956
+ validate: (v) => v?.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
1931
1957
  })
1932
1958
  );
1933
1959
  wallet = { type: "imported", secretKey: sk };
1934
1960
  address = Ed25519Keypair2.fromSecretKey(sk).getPublicKey().toSuiAddress();
1935
1961
  }
1936
- cfg.sui = { network, wallet, address };
1962
+ saveSuiWallet(cfg, network, { label: choice, address, secretKey: wallet.secretKey, source: wallet.type }, true);
1937
1963
  if (choice === "rill") {
1938
1964
  const url = bail(
1939
1965
  await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
1940
1966
  );
1941
- if (url) cfg.sui.rillMcpUrl = url;
1967
+ if (url && cfg.sui) {
1968
+ cfg.sui.rillMcpUrl = url;
1969
+ saveConfig(cfg);
1970
+ }
1942
1971
  }
1943
- saveConfig(cfg);
1944
1972
  const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
1945
1973
  p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
1946
1974
  p.outro(`Sui configured (${network}).`);
@@ -2421,6 +2449,125 @@ async function runCli() {
2421
2449
  };
2422
2450
  }
2423
2451
  });
2452
+ const activateSigner = (secretKey) => {
2453
+ suiSignerRef = suiSigner({ network: suiNetwork, secretKey, allowMainnet });
2454
+ };
2455
+ const suiWalletsTool = defineTool({
2456
+ name: "sui_wallets",
2457
+ description: "List every Sui wallet the user has: the local agent wallets (with addresses) and the Rill MCP signer if connected. Use to answer 'what wallets/addresses do I have', or to pick one. Does NOT reveal private keys (use sui_export_wallet for that).",
2458
+ parameters: z7.object({}),
2459
+ execute: () => {
2460
+ const cfg = loadConfig();
2461
+ const active = activeSuiWallet(cfg)?.address;
2462
+ return {
2463
+ network: cfg?.sui?.network ?? suiNetwork,
2464
+ activeAddress: active,
2465
+ agentWallets: suiWalletsOf(cfg).map((w) => ({
2466
+ label: w.label,
2467
+ address: w.address,
2468
+ source: w.source,
2469
+ active: w.address === active
2470
+ })),
2471
+ rill: cfg?.sui?.rillMcpUrl ? { source: "rill", mcpUrl: cfg.sui.rillMcpUrl } : null
2472
+ };
2473
+ }
2474
+ });
2475
+ const suiCreateWalletTool = defineTool({
2476
+ name: "sui_create_wallet",
2477
+ description: "Generate a NEW Sui agent wallet (key pair) locally and save it. Returns the new address \u2014 remind the user to fund it. Use when the user asks for a new/another wallet or address.",
2478
+ sensitive: true,
2479
+ parameters: z7.object({
2480
+ label: z7.string().optional().describe("A name for the wallet (default: wallet-N)."),
2481
+ activate: z7.boolean().optional().describe("Make it the active signing wallet (default true).")
2482
+ }),
2483
+ execute: async ({ label, activate }) => {
2484
+ const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
2485
+ const kp = Ed25519Keypair2.generate();
2486
+ const address = kp.getPublicKey().toSuiAddress();
2487
+ const cfg = loadConfig() ?? {};
2488
+ const makeActive = activate ?? true;
2489
+ const n = suiWalletsOf(cfg).length + 1;
2490
+ saveSuiWallet(
2491
+ cfg,
2492
+ suiNetwork,
2493
+ { label: label ?? `wallet-${String(n)}`, address, secretKey: kp.getSecretKey(), source: "generated" },
2494
+ makeActive
2495
+ );
2496
+ if (makeActive) activateSigner(kp.getSecretKey());
2497
+ return {
2498
+ address,
2499
+ active: makeActive,
2500
+ note: `New wallet on ${suiNetwork}. Fund ${address} before transacting${suiNetwork === "testnet" ? " (faucet: https://faucet.sui.io)" : ""}.`
2501
+ };
2502
+ }
2503
+ });
2504
+ const suiImportWalletTool = defineTool({
2505
+ name: "sui_import_wallet",
2506
+ description: "Import an existing Sui wallet from its private key (suiprivkey\u2026) and save it. Use when the user wants to add/restore a wallet they already have.",
2507
+ sensitive: true,
2508
+ parameters: z7.object({
2509
+ secretKey: z7.string().min(1).describe("The private key, a suiprivkey\u2026 string."),
2510
+ label: z7.string().optional().describe("A name for the wallet."),
2511
+ activate: z7.boolean().optional().describe("Make it the active signing wallet (default true).")
2512
+ }),
2513
+ execute: async ({ secretKey, label, activate }) => {
2514
+ if (!secretKey.startsWith("suiprivkey")) {
2515
+ throw new Error("sui_import_wallet: expected a private key starting with suiprivkey\u2026");
2516
+ }
2517
+ const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
2518
+ const address = Ed25519Keypair2.fromSecretKey(secretKey).getPublicKey().toSuiAddress();
2519
+ const cfg = loadConfig() ?? {};
2520
+ const makeActive = activate ?? true;
2521
+ saveSuiWallet(
2522
+ cfg,
2523
+ suiNetwork,
2524
+ { label: label ?? "imported", address, secretKey, source: "imported" },
2525
+ makeActive
2526
+ );
2527
+ if (makeActive) activateSigner(secretKey);
2528
+ return { address, active: makeActive, note: `Imported wallet ${address} on ${suiNetwork}.` };
2529
+ }
2530
+ });
2531
+ const suiExportWalletTool = defineTool({
2532
+ name: "sui_export_wallet",
2533
+ description: "Reveal the PRIVATE KEY of a saved wallet so the user can back it up or move it elsewhere. Defaults to the active wallet. SENSITIVE \u2014 only when the user explicitly asks to export/back up.",
2534
+ sensitive: true,
2535
+ parameters: z7.object({
2536
+ address: z7.string().optional().describe("Which wallet to export (default: the active one).")
2537
+ }),
2538
+ execute: ({ address }) => {
2539
+ const cfg = loadConfig();
2540
+ const all = suiWalletsOf(cfg);
2541
+ const w = address ? all.find((x) => x.address === address) : activeSuiWallet(cfg);
2542
+ if (!w) throw new Error("sui_export_wallet: no matching wallet found.");
2543
+ return {
2544
+ address: w.address,
2545
+ secretKey: w.secretKey,
2546
+ warning: "Keep this private key secret \u2014 anyone who has it controls the wallet."
2547
+ };
2548
+ }
2549
+ });
2550
+ const suiUseWalletTool = defineTool({
2551
+ name: "sui_use_wallet",
2552
+ description: "Switch the active signing wallet to a saved one by address. Use to send from a different wallet.",
2553
+ parameters: z7.object({ address: z7.string().min(1).describe("Address of the wallet to make active.") }),
2554
+ execute: ({ address }) => {
2555
+ const cfg = loadConfig();
2556
+ const w = suiWalletsOf(cfg).find((x) => x.address === address);
2557
+ if (!w || !cfg?.sui) throw new Error(`sui_use_wallet: no saved wallet ${address}.`);
2558
+ cfg.sui.activeAddress = address;
2559
+ saveConfig(cfg);
2560
+ activateSigner(w.secretKey);
2561
+ return { activeAddress: address, note: `Now signing as ${address}.` };
2562
+ }
2563
+ });
2564
+ const walletTools = [
2565
+ suiWalletsTool,
2566
+ suiCreateWalletTool,
2567
+ suiImportWalletTool,
2568
+ suiExportWalletTool,
2569
+ suiUseWalletTool
2570
+ ];
2424
2571
  const fetchUrlTool = defineTool({
2425
2572
  name: "fetch_url",
2426
2573
  description: "Fetch the contents of an http(s) URL (markdown, text, JSON, HTML). ALWAYS use this when the user shares a link \u2014 e.g. a skill.md, docs page, or an API/MCP endpoint \u2014 instead of saying you can't open URLs. Returns the response text (truncated if very large).",
@@ -2444,14 +2591,56 @@ async function runCli() {
2444
2591
  };
2445
2592
  }
2446
2593
  });
2447
- const webPlugins = process.env.BRAVE_API_KEY ? [webSearchPlugin({ apiKey: process.env.BRAVE_API_KEY })] : [];
2594
+ const exaKey = process.env.EXA_API_KEY;
2595
+ const webPlugins = [];
2596
+ const webTools = [];
2597
+ if (exaKey) {
2598
+ webTools.push(
2599
+ defineTool({
2600
+ name: "web_search",
2601
+ description: "Search the WEB via Exa and get ranked results with text snippets. Use whenever you need current info, prices, docs, or to find a page \u2014 anything you don't already know. This is DIFFERENT from fetch_url: web_search finds pages by query; fetch_url reads one URL you have.",
2602
+ parameters: z7.object({
2603
+ query: z7.string().min(1).describe("The search query."),
2604
+ numResults: z7.number().int().positive().optional().describe("Results to return (default 5).")
2605
+ }),
2606
+ execute: async ({ query, numResults }) => {
2607
+ const res = await fetch("https://api.exa.ai/search", {
2608
+ method: "POST",
2609
+ headers: { "content-type": "application/json", "x-api-key": exaKey },
2610
+ body: JSON.stringify({
2611
+ query,
2612
+ numResults: numResults ?? 5,
2613
+ contents: { text: { maxCharacters: 1200 } }
2614
+ }),
2615
+ signal: AbortSignal.timeout(2e4)
2616
+ });
2617
+ if (!res.ok) throw new Error(`web_search: Exa HTTP ${String(res.status)} ${await res.text()}`);
2618
+ const data = await res.json();
2619
+ return {
2620
+ query,
2621
+ results: (data.results ?? []).map((r) => ({ title: r.title, url: r.url, text: r.text }))
2622
+ };
2623
+ }
2624
+ })
2625
+ );
2626
+ } else if (process.env.BRAVE_API_KEY) {
2627
+ webPlugins.push(webSearchPlugin({ apiKey: process.env.BRAVE_API_KEY }));
2628
+ }
2629
+ const webSearchOn = webTools.length > 0 || webPlugins.length > 0;
2448
2630
  const budget = budgetMiddleware({ maxCalls: 50, logger });
2449
2631
  const agent = await createAgent({
2450
2632
  model,
2451
2633
  logger: agentLogger,
2452
2634
  persona,
2453
- 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\nLINKS: When the user shares any URL (a skill.md, docs, an API or MCP endpoint, etc.), call fetch_url to read it \u2014 never say you can't open links" + (webPlugins.length > 0 ? ". Use web_search to look things up on the web.\n\n" : ".\n\n") + "SUI: You can operate on the Sui blockchain directly with your own tools \u2014 do NOT tell the user to install a browser wallet extension. " + (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. ") + "Your Sui tools: sui_setup (create/import a wallet), sui_balance and sui_object (read), sui_transfer (send SUI or any coin to an address \u2014 amounts in MIST, 1 SUI = 1e9), sui_move_call (call ANY Move function on any package \u2014 the general way to run any on-chain action), and sui_execute_ptb (sign a PTB an external builder/Rill produced). Prefer sui_transfer for sends and sui_move_call for contract calls. Always confirm details and remind the user to fund the wallet. Never claim you cannot transact on Sui \u2014 you can, via these tools.",
2454
- tools: [echoTool, suiSetupTool, fetchUrlTool],
2635
+ systemPrompt: `You are ${persona?.name ?? "ThinyAI"}, a capable assistant with real tools. Be concise.
2636
+
2637
+ HOW TO ACT: When a request maps to one of your tools, CALL THE TOOL automatically \u2014 figure out the right tool yourself; do not ask the user which tool to run, do not ask permission for read-only actions, and never say you can't do something one of your tools covers. Chain tools when needed (e.g. web_search \u2192 fetch_url \u2192 act).
2638
+
2639
+ YOUR TOOLS:
2640
+ \u2022 Memory \u2014 remember_fact, recall_memory: durable memory across sessions (stored on Walrus). Known facts are injected each turn under \u201C[User Memory \u2026]\u201D. Immediately save anything durable the user shares (name, role, preferences, projects, goals). Answer \u201Cwhat do you remember\u201D from it. You DO remember across sessions \u2014 never say otherwise.
2641
+ \u2022 Links \u2014 fetch_url: read ANY URL the user shares (a skill.md, docs, JSON, an API/MCP endpoint). Always fetch shared links instead of saying you can't open URLs.
2642
+ ` + (webSearchOn ? "\u2022 Web search \u2014 web_search: search the web for anything you don't know (news, prices, docs). web_search FINDS pages by query; fetch_url READS a specific URL \u2014 use them together.\n" : "") + "\u2022 Planning \u2014 update_plan (track multi-step work), delegate_task (hand a focused subtask to a sub-agent).\n\u2022 Sui blockchain \u2014 you transact yourself; NEVER tell the user to install a browser wallet. " + (suiSignerRef ? `The active wallet is on ${suiNetwork} at ${suiSignerRef.address ?? "?"}. ` : "No wallet yet \u2014 call sui_create_wallet (or sui_import_wallet) when the user wants Sui, then have them fund the address. ") + "Wallets: sui_wallets (list ALL the user's wallets + addresses \u2014 use this to answer 'what's my address / what wallets do I have'), sui_create_wallet (new key pair), sui_import_wallet (restore from a suiprivkey), sui_export_wallet (reveal a private key \u2014 only when asked), sui_use_wallet (switch the active wallet). On-chain: sui_balance & sui_object (read), sui_transfer (send SUI/any coin \u2014 amounts in MIST, 1 SUI = 1e9), sui_move_call (call ANY Move function), sui_execute_ptb (sign a builder/Rill PTB). Prefer sui_transfer for sends and sui_move_call for contract calls; confirm details before signing.",
2643
+ tools: [echoTool, suiSetupTool, ...walletTools, fetchUrlTool, ...webTools],
2455
2644
  plugins: [
2456
2645
  {
2457
2646
  name: "observability",
@@ -2511,6 +2700,9 @@ async function runCli() {
2511
2700
  `Sui: ${suiNetwork} \xB7 ${suiSignerRef.address ?? "?"}${process.env.MCP_URL ? " \xB7 Rill MCP connected" : ""}`
2512
2701
  );
2513
2702
  else renderInfo("Sui: no wallet \u2014 ask the agent to set one up, or run `thiny sui init`");
2703
+ renderInfo(
2704
+ `Web: fetch_url (any URL)${webSearchOn ? ` \xB7 web_search (${exaKey ? "Exa" : "Brave"})` : " \xB7 web_search off (set EXA_API_KEY)"}`
2705
+ );
2514
2706
  notifyIfUpdate(thinyDir);
2515
2707
  const rl = createInterface({ input: stdin, output: stdout });
2516
2708
  emitKeypressEvents(stdin);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinyai",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
@@ -38,16 +38,16 @@
38
38
  "tsup": "^8.5.1",
39
39
  "typescript": "^5.5.0",
40
40
  "@thiny/memory-memwal": "0.1.0",
41
- "@thiny/walrus": "0.1.0",
42
- "@thiny/plugin-agents": "0.1.0",
43
41
  "@thiny/core": "0.1.0",
44
- "@thiny/model-aisdk": "0.1.0",
45
- "@thiny/logger-pino": "0.1.0",
46
42
  "@thiny/mcp": "0.1.0",
47
- "@thiny/signer-sui": "0.1.0",
43
+ "@thiny/logger-pino": "0.1.0",
44
+ "@thiny/model-aisdk": "0.1.0",
45
+ "@thiny/plugin-agents": "0.1.0",
48
46
  "@thiny/plugin-web-search": "0.1.0",
49
- "@thiny/plugin-sui": "0.1.0",
50
- "@thiny/skills": "0.1.0"
47
+ "@thiny/walrus": "0.1.0",
48
+ "@thiny/signer-sui": "0.1.0",
49
+ "@thiny/skills": "0.1.0",
50
+ "@thiny/plugin-sui": "0.1.0"
51
51
  },
52
52
  "author": "Thiny AI",
53
53
  "engines": {